2025-07-07 23:05:19 +01:00

831 lines
26 KiB
Rust

//! 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<String>,
pub roles: Vec<String>,
pub is_active: bool,
pub is_verified: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
pub avatar_url: Option<String>,
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<String>,
pub roles: Vec<String>,
pub send_invitation: bool,
#[validate(length(min = 8))]
pub temporary_password: Option<String>,
pub is_active: Option<bool>,
}
#[derive(Debug, Deserialize, Validate)]
pub struct UpdateUserRequest {
#[validate(email)]
pub email: Option<String>,
#[validate(length(min = 3, max = 50))]
pub username: Option<String>,
#[validate(length(min = 1, max = 100))]
pub display_name: Option<String>,
pub roles: Option<Vec<String>>,
pub is_active: Option<bool>,
pub is_verified: Option<bool>,
pub avatar_url: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UserQuery {
pub page: Option<u32>,
pub limit: Option<u32>,
pub search: Option<String>,
pub is_active: Option<bool>,
pub role: Option<String>,
pub sort_by: Option<String>,
pub sort_order: Option<String>,
pub is_verified: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct UserListResponse {
pub users: Vec<UserResponse>,
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<UserQuery>,
State(db): State<Database>,
State(auth_repo): State<AuthRepository>,
) -> Result<Json<UserListResponse>, 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<String>,
State(auth_repo): State<AuthRepository>,
) -> Result<Json<UserResponse>, 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<Database>,
State(auth_repo): State<AuthRepository>,
Json(request): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, 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<String>,
State(db): State<Database>,
State(auth_repo): State<AuthRepository>,
Json(request): Json<UpdateUserRequest>,
) -> Result<Json<UserResponse>, 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 &current_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<String>,
State(db): State<Database>,
State(auth_repo): State<AuthRepository>,
) -> Result<StatusCode, StatusCode> {
// 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<String>,
State(db): State<Database>,
State(auth_repo): State<AuthRepository>,
) -> Result<Json<UserResponse>, 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<Database>,
) -> Result<Json<UserStatsResponse>, 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<Vec<UserResponse>> {
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, &params).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<u64> {
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, &params).await?;
Ok(row.get_i64("count")? as u64)
}
async fn get_user_roles_by_id(
conn: &DatabaseConnection,
user_id: &Uuid,
) -> anyhow::Result<Vec<String>> {
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, &params).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<bool> {
let sql = "SELECT COUNT(*) as count FROM roles WHERE name = $1";
let params = vec![role_name.into()];
let row = conn.fetch_one(sql, &params).await?;
Ok(row.get_i64("count")? > 0)
}
async fn get_user_statistics(conn: &DatabaseConnection) -> anyhow::Result<UserStatsResponse> {
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, &params).await?;
Ok(())
}