lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
//! Infrastructure authentication provider
//!
//! Authenticates platform administrators against infra_db
//! Uses: infra_users and infra_sessions tables

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};

/// Infrastructure authentication provider
///
/// Manages authentication for platform administrators accessing infra.* subdomain
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> {
        // Note: Using hardcoded entity names until we generate entities
        // In production, these would be generated via: sea-orm-cli generate entity

        // For now, use raw SQL
        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")?;

        // Verify password
        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),
        }
    }
}