user_handler.rs 304 lignes · 10566 octets
use axum::{
    Json,
    extract::{Query, State},
    http::StatusCode,
};
use sea_orm::DatabaseConnection;
use serde::Deserialize;
use validator::Validate;

use rustwarden_core::auth::extractors::AuthUser;
use rustwarden_core::config::email::EmailConfig;
use rustwarden_core::error::AppError;
use rustwarden_core::services::{
    email_validation_service::EmailValidationService, password_service::PasswordService,
    role_service::RoleService, user_service::UserService,
};

/// Inline DTO: Create user request
#[derive(Debug, Deserialize, Validate)]
pub struct CreateUserRequest {
    /// Desired login name (3–39 characters, `[a-z0-9-]`, no reserved words).
    #[validate(length(min = 3, max = 39))]
    pub username: String,
    /// Valid email address for the new account.
    #[validate(email)]
    pub email: String,
    /// Initial password, or `None` to generate one automatically.
    pub password: Option<String>,
    /// Roles to assign to the new user.
    pub roles: Vec<String>,
}

/// Inline DTO: User response
#[derive(Debug, serde::Serialize)]
pub struct UserResponse {
    /// User unique identifier.
    pub id: uuid::Uuid,
    /// User login name.
    pub username: String,
    /// User email address.
    pub email: String,
    /// Whether the user must change their password on next login.
    pub must_change_password: bool,
    /// Roles assigned to the user.
    pub roles: Vec<String>,
    /// Timestamp when the account was created.
    pub created_at: chrono::DateTime<chrono::Utc>,
    /// Timestamp of the last account update.
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

/// Inline DTO: User list response
#[derive(Debug, serde::Serialize)]
pub struct UserListResponse {
    /// Total number of users matching the query.
    pub total: usize,
    /// Page of user records.
    pub users: Vec<UserResponse>,
}

/// Query parameters for user list/search
#[derive(Debug, Deserialize)]
pub struct UserListQuery {
    /// Optional full-text search filter.
    pub search: Option<String>,
    /// Maximum number of results to return.
    pub per_page: Option<u64>,
}

/// Handler for POST /api/v1/users - Create a new user (ADMIN ONLY)
/// SEC-H3: Requires admin role
pub async fn create_user(
    _auth_user: AuthUser,
    State(db): State<DatabaseConnection>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
    if !_auth_user.has_role("admin") {
        return Err(AppError::Forbidden);
    }

    payload.validate().map_err(|e| {
        tracing::warn!("Validation failed for create_user: {}", e);
        AppError::Validation(format!("Validation error: {}", e))
    })?;

    if UserService::get_user_by_username(&db, &payload.username)
        .await?
        .is_some()
    {
        return Err(AppError::Validation(format!(
            "Username '{}' already exists",
            payload.username
        )));
    }

    if UserService::get_user_by_email(&db, &payload.email)
        .await?
        .is_some()
    {
        return Err(AppError::Validation(format!(
            "Email '{}' already exists",
            payload.email
        )));
    }

    let (password_hash, must_change_password) = match payload.password {
        Some(pwd) => {
            let hash = PasswordService::hash_password(&pwd).await?;
            (hash, false)
        }
        None => {
            let default_pwd = PasswordService::generate_default_password();
            // SEC-H2: Never log passwords -- only log that a default was generated
            tracing::info!(
                "Generated default password for user '{}' (must change on first login)",
                payload.username
            );
            let hash = PasswordService::hash_password(&default_pwd).await?;
            (hash, true)
        }
    };

    let user = UserService::create_user(
        &db,
        payload.username.clone(),
        payload.email.clone(),
        password_hash,
        must_change_password,
    )
    .await?;

    let mut assigned_roles = Vec::new();
    for role_name in &payload.roles {
        let role = RoleService::get_role_by_name(&db, role_name)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("Role '{}' not found", role_name)))?;

        UserService::assign_role_to_user(&db, user.id, role.id).await?;
        assigned_roles.push(role.name.clone());
        tracing::info!("Assigned role '{}' to user '{}'", role.name, user.username);
    }

    tracing::info!(
        "User '{}' created successfully with roles: {:?}",
        user.username,
        assigned_roles
    );

    if let Ok(email_config) = EmailConfig::from_env() {
        let validation_service = EmailValidationService::new(email_config);
        if let Err(e) = validation_service.send_validation_email(&db, user.id).await {
            tracing::warn!(
                "Failed to send validation email to user '{}': {}",
                user.username,
                e
            );
        } else {
            tracing::info!(
                "Validation email sent to user '{}' at '{}'",
                user.username,
                user.email
            );
        }
    } else {
        tracing::warn!(
            "Email configuration not available - validation email not sent to user '{}'",
            user.username
        );
    }

    // Admin notification for new user registration (simplified - no email queue)
    let is_admin_creator = _auth_user.roles.iter().any(|r| r == "admin");

    if !is_admin_creator {
        if let Ok(email_config) = EmailConfig::from_env() {
            let admin_notifications_enabled = std::env::var("EMAIL_ADMIN_NOTIFICATIONS_ENABLED")
                .unwrap_or_else(|_| "true".to_string())
                .parse::<bool>()
                .unwrap_or(true);

            if admin_notifications_enabled {
                if let Ok(admin_users) = UserService::get_all_admin_users(&db).await {
                    if !admin_users.is_empty() {
                        let registered_at = chrono::Utc::now()
                            .format("%Y-%m-%d %H:%M:%S UTC")
                            .to_string();
                        let app_name = email_config
                            .smtp_from_name
                            .clone()
                            .unwrap_or_else(rustwarden_core::config::app::default_app_name);

                        let template =
                            rustwarden_core::templates::email_templates::AdminNewUserTemplate::new(
                                Some("fr".to_string()),
                                user.username.clone(),
                                user.email.clone(),
                                registered_at,
                                Some(app_name),
                            );

                        if let (Ok(body_html), body_text) =
                            (template.render_html(), template.render_text())
                        {
                            let subject = "Nouvel utilisateur inscrit".to_string();

                            for admin in &admin_users {
                                if let Err(e) = rustwarden_core::services::email_queue_service::EmailQueueService::enqueue(
                                    &db,
                                    admin.email.clone(),
                                    Some(admin.username.clone()),
                                    email_config.smtp_from.clone(),
                                    email_config.smtp_from_name.clone(),
                                    subject.clone(),
                                    body_html.clone(),
                                    body_text.clone(),
                                    "admin_new_user".to_string(),
                                ).await {
                                    tracing::warn!(
                                        "Failed to enqueue admin notification to {}: {}",
                                        admin.username,
                                        e
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        tracing::debug!(
            "Skipping admin notification for user '{}' - created by admin",
            user.username
        );
    }

    let response = UserResponse {
        id: user.id,
        username: user.username,
        email: user.email,
        must_change_password: user.must_change_password,
        roles: assigned_roles,
        created_at: user.created_at,
        updated_at: user.updated_at,
    };

    Ok((StatusCode::CREATED, Json(response)))
}

/// Handler for GET /api/v1/users - List all users (ADMIN ONLY)
/// SEC-H3: Requires admin role
pub async fn list_users(
    _auth_user: AuthUser,
    State(db): State<DatabaseConnection>,
    Query(query): Query<UserListQuery>,
) -> Result<Json<UserListResponse>, AppError> {
    if !_auth_user.has_role("admin") {
        return Err(AppError::Forbidden);
    }

    let users = if let Some(search_term) = query.search {
        use rustwarden_core::models::user;
        use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};

        let search_pattern = format!("%{}%", search_term);
        let query_builder = user::Entity::find().filter(
            sea_orm::Condition::any()
                .add(user::Column::Username.like(&search_pattern))
                .add(user::Column::Email.like(&search_pattern)),
        );

        let limit = query.per_page.unwrap_or(10);
        let filtered_users = query_builder.all(&db).await.map_err(AppError::Database)?;

        filtered_users.into_iter().take(limit as usize).collect()
    } else {
        let all_users = UserService::list_all_users(&db).await?;
        if let Some(limit) = query.per_page {
            all_users.into_iter().take(limit as usize).collect()
        } else {
            all_users
        }
    };

    let user_responses: Vec<UserResponse> = users
        .into_iter()
        .map(|user| UserResponse {
            id: user.id,
            username: user.username,
            email: user.email,
            must_change_password: user.must_change_password,
            roles: vec![],
            created_at: user.created_at,
            updated_at: user.updated_at,
        })
        .collect();

    let response = UserListResponse {
        total: user_responses.len(),
        users: user_responses,
    };

    Ok(Json(response))
}