use async_trait::async_trait;
use chrono::{Duration, Utc};
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
use uuid::Uuid;
use crate::auth::ports::{AuthProvider, AuthUser, Session};
use crate::error::{AppError, Result};
pub struct InfraAuthProvider {
db: DatabaseConnection,
session_expiration_hours: i64,
}
impl InfraAuthProvider {
pub fn new(db: DatabaseConnection, session_expiration_hours: i64) -> Self {
Self {
db,
session_expiration_hours,
}
}
}
#[async_trait]
impl AuthProvider for InfraAuthProvider {
async fn authenticate(&self, email: &str, password: &str) -> Result<AuthUser> {
use sea_orm::ConnectionTrait;
use sea_orm::Statement;
let sql = "SELECT id, email, password_hash, role FROM infra_users WHERE email = $1 AND is_active = true";
let stmt = Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![email.into()],
);
let result = self.db.query_one(stmt).await?;
let user = result
.ok_or_else(|| AppError::Unauthorized("Invalid email or password".to_string()))?;
let id: i64 = user.try_get("", "id")?;
let stored_hash: String = user.try_get("", "password_hash")?;
let role: String = user.try_get("", "role")?;
let user_email: String = user.try_get("", "email")?;
let is_valid = lmrc_http_common::auth::verify_password(password, &stored_hash)
.map_err(|e| AppError::Auth(format!("Password verification failed: {}", e)))?;
if !is_valid {
return Err(AppError::Unauthorized(
"Invalid email or password".to_string(),
));
}
Ok(AuthUser {
id,
email: user_email,
role,
})
}
async fn create_session(&self, user_id: i64) -> Result<Session> {
use sea_orm::ConnectionTrait;
use sea_orm::Statement;
let token = Uuid::new_v4().to_string();
let expires_at = Utc::now() + Duration::hours(self.session_expiration_hours);
let sql = "INSERT INTO infra_sessions (token, user_id, expires_at, created_at) VALUES ($1, $2, $3, $4)";
let stmt = Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![
token.clone().into(),
user_id.into(),
expires_at.naive_utc().into(),
Utc::now().naive_utc().into(),
],
);
self.db.execute(stmt).await?;
Ok(Session {
token,
user_id,
expires_at: expires_at.naive_utc(),
})
}
async fn validate_session(&self, token: &str) -> Result<Option<AuthUser>> {
use sea_orm::ConnectionTrait;
use sea_orm::Statement;
let sql = r#"
SELECT u.id, u.email, u.role
FROM infra_sessions s
JOIN infra_users u ON s.user_id = u.id
WHERE s.token = $1 AND s.expires_at > $2 AND u.is_active = true
"#;
let stmt = Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![token.into(), Utc::now().naive_utc().into()],
);
let result = self.db.query_one(stmt).await?;
match result {
Some(row) => {
let id: i64 = row.try_get("", "id")?;
let email: String = row.try_get("", "email")?;
let role: String = row.try_get("", "role")?;
Ok(Some(AuthUser { id, email, role }))
}
None => Ok(None),
}
}
async fn destroy_session(&self, token: &str) -> Result<()> {
use sea_orm::ConnectionTrait;
use sea_orm::Statement;
let sql = "DELETE FROM infra_sessions WHERE token = $1";
let stmt = Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![token.into()],
);
self.db.execute(stmt).await?;
Ok(())
}
async fn get_user(&self, user_id: i64) -> Result<Option<AuthUser>> {
use sea_orm::ConnectionTrait;
use sea_orm::Statement;
let sql = "SELECT id, email, role FROM infra_users WHERE id = $1 AND is_active = true";
let stmt = Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![user_id.into()],
);
let result = self.db.query_one(stmt).await?;
match result {
Some(row) => {
let id: i64 = row.try_get("", "id")?;
let email: String = row.try_get("", "email")?;
let role: String = row.try_get("", "role")?;
Ok(Some(AuthUser { id, email, role }))
}
None => Ok(None),
}
}
}