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)) }
GitRust