//! Admin Users Handler //! //! Provides admin user management endpoints using proper database and auth abstractions use crate::auth::middleware::RequireAuth; use crate::auth::repository::AuthRepository; use crate::database::Database; use crate::database::connection::DatabaseConnection; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::Json, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use shared::auth::{HasPermissions, Permission, Role, User}; use std::collections::HashMap; use uuid::Uuid; use validator::Validate; #[derive(Debug, Serialize, Deserialize)] pub struct UserResponse { pub id: String, pub email: String, pub username: String, pub display_name: Option, pub roles: Vec, pub is_active: bool, pub is_verified: bool, pub created_at: DateTime, pub updated_at: DateTime, pub last_login: Option>, pub avatar_url: Option, pub two_factor_enabled: bool, } #[derive(Debug, Deserialize, Validate)] pub struct CreateUserRequest { #[validate(email)] pub email: String, #[validate(length(min = 3, max = 50))] pub username: String, #[validate(length(min = 1, max = 100))] pub display_name: Option, pub roles: Vec, pub send_invitation: bool, #[validate(length(min = 8))] pub temporary_password: Option, pub is_active: Option, } #[derive(Debug, Deserialize, Validate)] pub struct UpdateUserRequest { #[validate(email)] pub email: Option, #[validate(length(min = 3, max = 50))] pub username: Option, #[validate(length(min = 1, max = 100))] pub display_name: Option, pub roles: Option>, pub is_active: Option, pub is_verified: Option, pub avatar_url: Option, } #[derive(Debug, Deserialize)] pub struct UserQuery { pub page: Option, pub limit: Option, pub search: Option, pub is_active: Option, pub role: Option, pub sort_by: Option, pub sort_order: Option, pub is_verified: Option, } #[derive(Debug, Serialize)] pub struct UserListResponse { pub users: Vec, pub total: u64, pub page: u32, pub limit: u32, pub total_pages: u32, } #[derive(Debug, Serialize)] pub struct UserStatsResponse { pub total_users: u64, pub active_users: u64, pub inactive_users: u64, pub verified_users: u64, pub unverified_users: u64, pub recent_registrations: u64, pub two_factor_enabled: u64, } /// Get all users with pagination and filtering pub async fn get_users( RequireAuth(user): RequireAuth, Query(query): Query, State(db): State, State(auth_repo): State, ) -> Result, StatusCode> { // Check admin permissions if !user.has_permission(Permission::ReadUsers) { return Err(StatusCode::FORBIDDEN); } let conn = db.pool().create_connection(); let page = query.page.unwrap_or(1); let limit = query.limit.unwrap_or(20).min(100); let offset = (page - 1) * limit; // Build the query let users = get_users_with_filters(&conn, &query, limit, offset) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Get total count let total = get_users_count(&conn, &query) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let total_pages = (total + limit as u64 - 1) / limit as u64; Ok(Json(UserListResponse { users, total, page, limit, total_pages: total_pages as u32, })) } /// Get a specific user by ID pub async fn get_user( RequireAuth(user): RequireAuth, Path(user_id): Path, State(auth_repo): State, ) -> Result, StatusCode> { // Check permissions - can read own profile or has read users permission let target_uuid = Uuid::parse_str(&user_id).map_err(|_| StatusCode::BAD_REQUEST)?; if user.id != target_uuid && !user.has_permission(Permission::ReadUsers) { return Err(StatusCode::FORBIDDEN); } let db_user = auth_repo .find_user_by_id(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; let roles = auth_repo .get_user_roles(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(UserResponse { id: db_user.id.to_string(), email: db_user.email, username: db_user.username.unwrap_or_default(), display_name: db_user.display_name, roles, is_active: db_user.is_active, is_verified: db_user.is_verified, created_at: db_user.created_at, updated_at: db_user.updated_at, last_login: db_user.last_login, avatar_url: db_user.avatar_url, two_factor_enabled: db_user.two_factor_enabled, })) } /// Create a new user pub async fn create_user( RequireAuth(user): RequireAuth, State(db): State, State(auth_repo): State, Json(request): Json, ) -> Result, StatusCode> { // Check admin permissions if !user.has_permission(Permission::WriteUsers) { return Err(StatusCode::FORBIDDEN); } // Validate request request.validate().map_err(|_| StatusCode::BAD_REQUEST)?; let conn = db.pool().create_connection(); // Check if user already exists if auth_repo .email_exists(&request.email) .await .unwrap_or(false) { return Err(StatusCode::CONFLICT); } if auth_repo .username_exists(&request.username) .await .unwrap_or(false) { return Err(StatusCode::CONFLICT); } // Validate roles exist for role_name in &request.roles { if !role_exists(&conn, role_name).await.unwrap_or(false) { return Err(StatusCode::BAD_REQUEST); } } // Generate password hash let password = request .temporary_password .unwrap_or_else(generate_temporary_password); let password_hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Create user using auth repository let create_request = crate::database::auth::CreateUserRequest { email: request.email.clone(), password_hash, display_name: request.display_name.clone(), username: Some(request.username.clone()), is_verified: false, is_active: request.is_active.unwrap_or(true), }; let created_user = auth_repo .create_user(&create_request) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Assign roles for role_name in &request.roles { auth_repo .add_user_role(&created_user.id, role_name) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } // Log activity log_admin_activity( &conn, &user.id.to_string(), "create_user", "user", Some(&created_user.id.to_string()), None, None, ) .await .ok(); // Send invitation email if requested if request.send_invitation { // TODO: Implement email sending tracing::info!( "Would send invitation email to {} with temporary password", request.email ); } // Return created user Ok(Json(UserResponse { id: created_user.id.to_string(), email: created_user.email, username: created_user.username.unwrap_or_default(), display_name: created_user.display_name, roles: request.roles, is_active: created_user.is_active, is_verified: created_user.is_verified, created_at: created_user.created_at, updated_at: created_user.updated_at, last_login: created_user.last_login, avatar_url: created_user.avatar_url, two_factor_enabled: created_user.two_factor_enabled, })) } /// Update a user pub async fn update_user( RequireAuth(user): RequireAuth, Path(user_id): Path, State(db): State, State(auth_repo): State, Json(request): Json, ) -> Result, StatusCode> { let target_uuid = Uuid::parse_str(&user_id).map_err(|_| StatusCode::BAD_REQUEST)?; // Check permissions - can update own profile or has write users permission if user.id != target_uuid && !user.has_permission(Permission::WriteUsers) { return Err(StatusCode::FORBIDDEN); } // Validate request request.validate().map_err(|_| StatusCode::BAD_REQUEST)?; let conn = db.pool().create_connection(); // Get existing user let mut existing_user = auth_repo .find_user_by_id(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; // Update fields if let Some(email) = &request.email { if email != &existing_user.email { if auth_repo.email_exists(email).await.unwrap_or(false) { return Err(StatusCode::CONFLICT); } existing_user.email = email.clone(); } } if let Some(username) = &request.username { if Some(username) != existing_user.username.as_ref() { if auth_repo.username_exists(username).await.unwrap_or(false) { return Err(StatusCode::CONFLICT); } existing_user.username = Some(username.clone()); } } if let Some(display_name) = &request.display_name { existing_user.display_name = Some(display_name.clone()); } if let Some(is_active) = request.is_active { // Only admins can change active status if user.has_permission(Permission::WriteUsers) { existing_user.is_active = is_active; } } if let Some(is_verified) = request.is_verified { // Only admins can change verified status if user.has_permission(Permission::WriteUsers) { existing_user.is_verified = is_verified; } } if let Some(avatar_url) = &request.avatar_url { existing_user.avatar_url = Some(avatar_url.clone()); } existing_user.updated_at = Utc::now(); // Update user auth_repo .update_user(&existing_user) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Update roles if provided and user has permission if let Some(roles) = &request.roles { if user.has_permission(Permission::WriteUsers) { // Get current roles let current_roles = auth_repo .get_user_roles(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Remove roles not in new list for role in ¤t_roles { if !roles.contains(role) { auth_repo .remove_user_role(&target_uuid, role) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } } // Add new roles for role in roles { if !current_roles.contains(role) { if role_exists(&conn, role).await.unwrap_or(false) { auth_repo .add_user_role(&target_uuid, role) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } } } } } // Log activity log_admin_activity( &conn, &user.id.to_string(), "update_user", "user", Some(&target_uuid.to_string()), None, None, ) .await .ok(); // Get updated roles let updated_roles = auth_repo .get_user_roles(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(UserResponse { id: existing_user.id.to_string(), email: existing_user.email, username: existing_user.username.unwrap_or_default(), display_name: existing_user.display_name, roles: updated_roles, is_active: existing_user.is_active, is_verified: existing_user.is_verified, created_at: existing_user.created_at, updated_at: existing_user.updated_at, last_login: existing_user.last_login, avatar_url: existing_user.avatar_url, two_factor_enabled: existing_user.two_factor_enabled, })) } /// Delete a user pub async fn delete_user( RequireAuth(user): RequireAuth, Path(user_id): Path, State(db): State, State(auth_repo): State, ) -> Result { // Check admin permissions if !user.has_permission(Permission::DeleteUsers) { return Err(StatusCode::FORBIDDEN); } let target_uuid = Uuid::parse_str(&user_id).map_err(|_| StatusCode::BAD_REQUEST)?; // Prevent self-deletion if user.id == target_uuid { return Err(StatusCode::BAD_REQUEST); } let conn = db.pool().create_connection(); // Check if user exists if auth_repo.find_user_by_id(&target_uuid).await.is_err() { return Err(StatusCode::NOT_FOUND); } // Delete user (this should cascade to sessions and roles) auth_repo .delete_user(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Log activity log_admin_activity( &conn, &user.id.to_string(), "delete_user", "user", Some(&target_uuid.to_string()), None, None, ) .await .ok(); Ok(StatusCode::NO_CONTENT) } /// Toggle user active status pub async fn toggle_user_status( RequireAuth(user): RequireAuth, Path(user_id): Path, State(db): State, State(auth_repo): State, ) -> Result, StatusCode> { // Check admin permissions if !user.has_permission(Permission::WriteUsers) { return Err(StatusCode::FORBIDDEN); } let target_uuid = Uuid::parse_str(&user_id).map_err(|_| StatusCode::BAD_REQUEST)?; // Prevent self-modification if user.id == target_uuid { return Err(StatusCode::BAD_REQUEST); } let conn = db.pool().create_connection(); // Get current user let mut existing_user = auth_repo .find_user_by_id(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; // Toggle status existing_user.is_active = !existing_user.is_active; existing_user.updated_at = Utc::now(); // Update user auth_repo .update_user(&existing_user) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Invalidate all sessions if deactivating if !existing_user.is_active { auth_repo .invalidate_all_user_sessions(target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } // Log activity log_admin_activity( &conn, &user.id.to_string(), "toggle_user_status", "user", Some(&target_uuid.to_string()), None, None, ) .await .ok(); // Get roles for response let roles = auth_repo .get_user_roles(&target_uuid) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(UserResponse { id: existing_user.id.to_string(), email: existing_user.email, username: existing_user.username.unwrap_or_default(), display_name: existing_user.display_name, roles, is_active: existing_user.is_active, is_verified: existing_user.is_verified, created_at: existing_user.created_at, updated_at: existing_user.updated_at, last_login: existing_user.last_login, avatar_url: existing_user.avatar_url, two_factor_enabled: existing_user.two_factor_enabled, })) } /// Get user statistics pub async fn get_user_stats( RequireAuth(user): RequireAuth, State(db): State, ) -> Result, StatusCode> { // Check admin permissions if !user.has_permission(Permission::ReadUsers) { return Err(StatusCode::FORBIDDEN); } let conn = db.pool().create_connection(); let stats = get_user_statistics(&conn) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(stats)) } // Helper functions async fn get_users_with_filters( conn: &DatabaseConnection, query: &UserQuery, limit: u32, offset: u32, ) -> anyhow::Result> { let mut sql = "SELECT u.id, u.email, u.username, u.display_name, u.is_active, u.is_verified, u.created_at, u.updated_at, u.last_login, u.avatar_url, u.two_factor_enabled FROM users u WHERE 1=1".to_string(); let mut params = Vec::new(); let mut param_index = 1; // Add search filter if let Some(search) = &query.search { let search_pattern = format!("%{}%", search); sql.push_str(&format!( " AND (u.email LIKE ${} OR u.username LIKE ${} OR u.display_name LIKE ${})", param_index, param_index + 1, param_index + 2 )); params.push(search_pattern.clone().into()); params.push(search_pattern.clone().into()); params.push(search_pattern.into()); param_index += 3; } // Add active filter if let Some(is_active) = query.is_active { sql.push_str(&format!(" AND u.is_active = ${}", param_index)); params.push(is_active.into()); param_index += 1; } // Add verified filter if let Some(is_verified) = query.is_verified { sql.push_str(&format!(" AND u.is_verified = ${}", param_index)); params.push(is_verified.into()); param_index += 1; } // Add role filter if provided if let Some(role) = &query.role { sql.push_str(&format!(" AND EXISTS (SELECT 1 FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = u.id AND r.name = ${})", param_index)); params.push(role.into()); param_index += 1; } // Add sorting let sort_by = query.sort_by.as_deref().unwrap_or("created_at"); let sort_order = query.sort_order.as_deref().unwrap_or("desc"); match sort_by { "email" | "username" | "display_name" | "created_at" | "updated_at" | "last_login" => { sql.push_str(&format!(" ORDER BY u.{} {}", sort_by, sort_order)); } _ => { sql.push_str(" ORDER BY u.created_at DESC"); } } // Add pagination sql.push_str(&format!( " LIMIT ${} OFFSET ${}", param_index, param_index + 1 )); params.push((limit as i64).into()); params.push((offset as i64).into()); let rows = conn.fetch_all(&sql, ¶ms).await?; let mut users = Vec::new(); for row in rows { let user_id = Uuid::parse_str(&row.get_string("id")?)?; let roles = get_user_roles_by_id(conn, &user_id) .await .unwrap_or_default(); users.push(UserResponse { id: row.get_string("id")?, email: row.get_string("email")?, username: row.get_optional_string("username")?.unwrap_or_default(), display_name: row.get_optional_string("display_name")?, roles, is_active: row.get_bool("is_active")?, is_verified: row.get_bool("is_verified")?, created_at: row.get_datetime("created_at")?, updated_at: row.get_datetime("updated_at")?, last_login: row.get_optional_datetime("last_login")?, avatar_url: row.get_optional_string("avatar_url")?, two_factor_enabled: row.get_bool("two_factor_enabled")?, }); } Ok(users) } async fn get_users_count(conn: &DatabaseConnection, query: &UserQuery) -> anyhow::Result { let mut sql = "SELECT COUNT(*) as count FROM users u WHERE 1=1".to_string(); let mut params = Vec::new(); let mut param_index = 1; // Add same filters as get_users_with_filters but for counting if let Some(search) = &query.search { let search_pattern = format!("%{}%", search); sql.push_str(&format!( " AND (u.email LIKE ${} OR u.username LIKE ${} OR u.display_name LIKE ${})", param_index, param_index + 1, param_index + 2 )); params.push(search_pattern.clone().into()); params.push(search_pattern.clone().into()); params.push(search_pattern.into()); param_index += 3; } if let Some(is_active) = query.is_active { sql.push_str(&format!(" AND u.is_active = ${}", param_index)); params.push(is_active.into()); param_index += 1; } if let Some(is_verified) = query.is_verified { sql.push_str(&format!(" AND u.is_verified = ${}", param_index)); params.push(is_verified.into()); param_index += 1; } if let Some(role) = &query.role { sql.push_str(&format!(" AND EXISTS (SELECT 1 FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = u.id AND r.name = ${})", param_index)); params.push(role.into()); } let row = conn.fetch_one(&sql, ¶ms).await?; Ok(row.get_i64("count")? as u64) } async fn get_user_roles_by_id( conn: &DatabaseConnection, user_id: &Uuid, ) -> anyhow::Result> { let sql = "SELECT r.name FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = $1"; let params = vec![user_id.to_string().into()]; let rows = conn.fetch_all(sql, ¶ms).await?; let mut roles = Vec::new(); for row in rows { roles.push(row.get_string("name")?); } Ok(roles) } async fn role_exists(conn: &DatabaseConnection, role_name: &str) -> anyhow::Result { let sql = "SELECT COUNT(*) as count FROM roles WHERE name = $1"; let params = vec![role_name.into()]; let row = conn.fetch_one(sql, ¶ms).await?; Ok(row.get_i64("count")? > 0) } async fn get_user_statistics(conn: &DatabaseConnection) -> anyhow::Result { let sql = match conn.database_type() { crate::database::DatabaseType::PostgreSQL => { "SELECT COUNT(*) as total_users, COUNT(CASE WHEN is_active = true THEN 1 END) as active_users, COUNT(CASE WHEN is_active = false THEN 1 END) as inactive_users, COUNT(CASE WHEN is_verified = true THEN 1 END) as verified_users, COUNT(CASE WHEN is_verified = false THEN 1 END) as unverified_users, COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as recent_registrations, COUNT(CASE WHEN two_factor_enabled = true THEN 1 END) as two_factor_enabled FROM users" } crate::database::DatabaseType::SQLite => { "SELECT COUNT(*) as total_users, COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_users, COUNT(CASE WHEN is_active = 0 THEN 1 END) as inactive_users, COUNT(CASE WHEN is_verified = 1 THEN 1 END) as verified_users, COUNT(CASE WHEN is_verified = 0 THEN 1 END) as unverified_users, COUNT(CASE WHEN created_at > datetime('now', '-30 days') THEN 1 END) as recent_registrations, COUNT(CASE WHEN two_factor_enabled = 1 THEN 1 END) as two_factor_enabled FROM users" } }; let row = conn.fetch_one(sql, &[]).await?; Ok(UserStatsResponse { total_users: row.get_i64("total_users")? as u64, active_users: row.get_i64("active_users")? as u64, inactive_users: row.get_i64("inactive_users")? as u64, verified_users: row.get_i64("verified_users")? as u64, unverified_users: row.get_i64("unverified_users")? as u64, recent_registrations: row.get_i64("recent_registrations")? as u64, two_factor_enabled: row.get_i64("two_factor_enabled")? as u64, }) } fn generate_temporary_password() -> String { use rand::Rng; let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ abcdefghijklmnopqrstuvwxyz\ 0123456789\ !@#$%^&*"; let mut rng = rand::thread_rng(); (0..16) .map(|_| { let idx = rng.gen_range(0..charset.len()); charset[idx] as char }) .collect() } async fn log_admin_activity( conn: &DatabaseConnection, user_id: &str, action: &str, resource_type: &str, resource_id: Option<&str>, ip_address: Option<&str>, user_agent: Option<&str>, ) -> anyhow::Result<()> { let sql = match conn.database_type() { crate::database::DatabaseType::PostgreSQL => { "INSERT INTO activity_logs (id, user_id, action, resource_type, resource_id, ip_address, user_agent, timestamp, status) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, NOW(), 'success')" } crate::database::DatabaseType::SQLite => { "INSERT INTO activity_logs (id, user_id, action, resource_type, resource_id, ip_address, user_agent, timestamp, status) VALUES (hex(randomblob(16)), $1, $2, $3, $4, $5, $6, datetime('now'), 'success')" } }; let params = vec![ user_id.into(), action.into(), resource_type.into(), resource_id.unwrap_or("").into(), ip_address.unwrap_or("").into(), user_agent.unwrap_or("").into(), ]; conn.execute(sql, ¶ms).await?; Ok(()) }