831 lines
26 KiB
Rust
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 ¤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<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, ¶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<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, ¶ms).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, ¶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<bool> {
|
|
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<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, ¶ms).await?;
|
|
Ok(())
|
|
}
|