feat(admin): add user management, quotas, and stats dashboard to admin panel
- Dashboard tab: total users, active users, admins, storage usage, quota warnings - User management tab: list, edit role, activate/deactivate, update quota, delete - Self-protection: cannot delete/deactivate/demote yourself - Paginated user listing (default 50 per page) - Efficient single-query dashboard stats with direct SQL aggregation - Quota modal with GB/MB/TB unit selector - Backend: added methods to UserStoragePort, UserRepository, PgRepository - Backend: added 7 admin methods to AuthApplicationService - Backend: added 5 new DTOs for admin operations - Backend: 7 new endpoints under /api/admin/ Files modified: - src/application/ports/auth_ports.rs (4 new UserStoragePort methods) - src/domain/repositories/user_repository.rs (StorageStats + 3 methods) - src/infrastructure/repositories/pg/user_pg_repository.rs (implementations) - src/application/services/auth_application_service.rs (admin methods) - src/application/dtos/settings_dto.rs (5 new DTOs) - src/interfaces/api/handlers/admin_handler.rs (7 new endpoints) - static/admin.html (complete UI with 3 tabs: Dashboard, Users, OIDC)
This commit is contained in:
@@ -56,3 +56,53 @@ pub struct OidcTestResultDto {
|
||||
/// Suggested provider name (derived from issuer hostname)
|
||||
pub provider_name_suggestion: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Admin User Management DTOs
|
||||
// ============================================================================
|
||||
|
||||
/// Request body for updating a user's role
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateUserRoleDto {
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// Request body for updating a user's active status
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateUserActiveDto {
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
/// Request body for updating a user's storage quota
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateUserQuotaDto {
|
||||
/// Quota in bytes. Use 0 for unlimited.
|
||||
pub quota_bytes: i64,
|
||||
}
|
||||
|
||||
/// Query parameters for listing users
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListUsersQueryDto {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// Dashboard statistics
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DashboardStatsDto {
|
||||
// System info
|
||||
pub server_version: String,
|
||||
pub auth_enabled: bool,
|
||||
pub oidc_configured: bool,
|
||||
pub quotas_enabled: bool,
|
||||
// User stats
|
||||
pub total_users: i64,
|
||||
pub active_users: i64,
|
||||
pub admin_users: i64,
|
||||
// Storage stats
|
||||
pub total_quota_bytes: i64,
|
||||
pub total_used_bytes: i64,
|
||||
pub storage_usage_percent: f64,
|
||||
pub users_over_80_percent: i64,
|
||||
pub users_over_quota: i64,
|
||||
}
|
||||
|
||||
@@ -97,6 +97,18 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
||||
|
||||
/// Finds a user by OIDC provider + subject pair
|
||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError>;
|
||||
|
||||
/// Activa o desactiva un usuario
|
||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
|
||||
|
||||
/// Cambia el rol de un usuario
|
||||
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Actualiza la cuota de almacenamiento de un usuario
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError>;
|
||||
|
||||
/// Cuenta el número total de usuarios
|
||||
async fn count_users(&self) -> Result<i64, DomainError>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -605,6 +605,69 @@ impl AuthApplicationService {
|
||||
Ok(users.into_iter().map(UserDto::from).collect())
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Admin User Management Methods
|
||||
// ========================================================================
|
||||
|
||||
/// Get a single user by ID (for admin panel)
|
||||
pub async fn get_user_admin(&self, user_id: &str) -> Result<UserDto, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
Ok(UserDto::from(user))
|
||||
}
|
||||
|
||||
/// Delete a user by ID (admin only)
|
||||
pub async fn delete_user_admin(&self, user_id: &str) -> Result<(), DomainError> {
|
||||
// Prevent deleting yourself
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
tracing::info!("Admin deleting user: {} ({})", user.username(), user_id);
|
||||
self.user_storage.delete_user(user_id).await
|
||||
}
|
||||
|
||||
/// Activate or deactivate a user (admin only)
|
||||
pub async fn set_user_active(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
|
||||
self.user_storage.set_user_active_status(user_id, active).await
|
||||
}
|
||||
|
||||
/// Change user role (admin only)
|
||||
pub async fn change_user_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> {
|
||||
if role != "admin" && role != "user" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
format!("Invalid role: {}. Must be 'admin' or 'user'", role),
|
||||
));
|
||||
}
|
||||
self.user_storage.change_role(user_id, role).await
|
||||
}
|
||||
|
||||
/// Update user's storage quota (admin only)
|
||||
pub async fn update_user_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError> {
|
||||
if quota_bytes < 0 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"User",
|
||||
"Quota must be non-negative".to_string(),
|
||||
));
|
||||
}
|
||||
self.user_storage.update_storage_quota(user_id, quota_bytes).await
|
||||
}
|
||||
|
||||
/// Check if a user has enough quota for an upload of the given size
|
||||
pub async fn check_quota(&self, user_id: &str, additional_bytes: i64) -> Result<bool, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
let quota = user.storage_quota_bytes();
|
||||
if quota <= 0 {
|
||||
// 0 or negative means unlimited
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(user.storage_used_bytes() + additional_bytes <= quota)
|
||||
}
|
||||
|
||||
/// Count users efficiently
|
||||
pub async fn count_users_efficient(&self) -> Result<i64, DomainError> {
|
||||
self.user_storage.count_users().await
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OIDC Methods
|
||||
// ========================================================================
|
||||
|
||||
@@ -94,4 +94,24 @@ pub trait UserRepository: Send + Sync + 'static {
|
||||
|
||||
/// Finds a user by OIDC provider + subject pair
|
||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>;
|
||||
|
||||
/// Actualiza la cuota de almacenamiento de un usuario
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>;
|
||||
|
||||
/// Cuenta el número total de usuarios
|
||||
async fn count_users(&self) -> UserRepositoryResult<i64>;
|
||||
|
||||
/// Obtiene estadísticas de almacenamiento agregadas
|
||||
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
|
||||
}
|
||||
|
||||
/// Estadísticas de almacenamiento agregadas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageStats {
|
||||
pub total_users: i64,
|
||||
pub active_users: i64,
|
||||
pub total_quota_bytes: i64,
|
||||
pub total_used_bytes: i64,
|
||||
pub users_over_80_percent: i64,
|
||||
pub users_over_quota: i64,
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult};
|
||||
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult, StorageStats};
|
||||
use crate::application::ports::auth_ports::UserStoragePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||
@@ -547,6 +547,67 @@ impl UserRepository for UserPgRepository {
|
||||
row.get("oidc_subject"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Actualiza la cuota de almacenamiento de un usuario
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET
|
||||
storage_quota_bytes = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(quota_bytes)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cuenta el número total de usuarios
|
||||
async fn count_users(&self) -> UserRepositoryResult<i64> {
|
||||
let row = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM auth.users"
|
||||
)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let count: i64 = row.get("count");
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Obtiene estadísticas de almacenamiento agregadas
|
||||
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(*) FILTER (WHERE active = true) as active_users,
|
||||
COALESCE(SUM(storage_quota_bytes), 0) as total_quota_bytes,
|
||||
COALESCE(SUM(storage_used_bytes), 0) as total_used_bytes,
|
||||
COUNT(*) FILTER (WHERE storage_quota_bytes > 0 AND storage_used_bytes > storage_quota_bytes * 0.8) as users_over_80_percent,
|
||||
COUNT(*) FILTER (WHERE storage_quota_bytes > 0 AND storage_used_bytes > storage_quota_bytes) as users_over_quota
|
||||
FROM auth.users
|
||||
"#
|
||||
)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
Ok(StorageStats {
|
||||
total_users: row.get("total_users"),
|
||||
active_users: row.get("active_users"),
|
||||
total_quota_bytes: row.get("total_quota_bytes"),
|
||||
total_used_bytes: row.get("total_used_bytes"),
|
||||
users_over_80_percent: row.get("users_over_80_percent"),
|
||||
users_over_quota: row.get("users_over_quota"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación del puerto de almacenamiento para la capa de aplicación
|
||||
@@ -603,4 +664,32 @@ impl UserStoragePort for UserPgRepository {
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
|
||||
UserRepository::set_user_active_status(self, user_id, active)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> {
|
||||
let user_role = match role {
|
||||
"admin" => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
UserRepository::change_role(self, user_id, user_role)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError> {
|
||||
UserRepository::update_storage_quota(self, user_id, quota_bytes)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn count_users(&self) -> Result<i64, DomainError> {
|
||||
UserRepository::count_users(self)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,36 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, put, post},
|
||||
extract::{State, Json},
|
||||
routing::{get, put, post, delete},
|
||||
extract::{State, Json, Path, Query},
|
||||
http::{StatusCode, HeaderMap, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::application::dtos::settings_dto::{SaveOidcSettingsDto, TestOidcConnectionDto};
|
||||
use crate::application::dtos::settings_dto::{
|
||||
SaveOidcSettingsDto, TestOidcConnectionDto,
|
||||
UpdateUserRoleDto, UpdateUserActiveDto, UpdateUserQuotaDto,
|
||||
ListUsersQueryDto, DashboardStatsDto,
|
||||
};
|
||||
use crate::interfaces::errors::AppError;
|
||||
|
||||
/// Admin API routes — all require admin role.
|
||||
pub fn admin_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
// OIDC settings
|
||||
.route("/settings/oidc", get(get_oidc_settings))
|
||||
.route("/settings/oidc", put(save_oidc_settings))
|
||||
.route("/settings/oidc/test", post(test_oidc_connection))
|
||||
.route("/settings/general", get(get_general_settings))
|
||||
// Dashboard / stats
|
||||
.route("/dashboard", get(get_dashboard_stats))
|
||||
// User management
|
||||
.route("/users", get(list_users))
|
||||
.route("/users/{id}", get(get_user))
|
||||
.route("/users/{id}", delete(delete_user))
|
||||
.route("/users/{id}/role", put(update_user_role))
|
||||
.route("/users/{id}/active", put(update_user_active))
|
||||
.route("/users/{id}/quota", put(update_user_quota))
|
||||
}
|
||||
|
||||
/// Validate JWT and require admin role. Returns (user_id, role).
|
||||
@@ -96,7 +110,7 @@ async fn test_oidc_connection(
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/admin/settings/general — system overview
|
||||
/// GET /api/admin/settings/general — system overview (backward compat)
|
||||
async fn get_general_settings(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -106,7 +120,7 @@ async fn get_general_settings(
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
let user_count = auth.auth_application_service.count_all_users().await.unwrap_or(0);
|
||||
let user_count = auth.auth_application_service.count_users_efficient().await.unwrap_or(0);
|
||||
let oidc_configured = auth.auth_application_service.oidc_enabled();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
@@ -116,3 +130,224 @@ async fn get_general_settings(
|
||||
"oidc_configured": oidc_configured,
|
||||
})))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dashboard / Stats
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/dashboard — full dashboard statistics
|
||||
async fn get_dashboard_stats(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
let auth_app = &auth.auth_application_service;
|
||||
|
||||
// Get storage stats from repository (single efficient query)
|
||||
let db_pool = state.db_pool.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Database not available"))?;
|
||||
|
||||
// Use direct SQL for aggregated stats — more efficient than loading all users
|
||||
let stats_row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(*)::INT8 as total_users,
|
||||
COUNT(*) FILTER (WHERE active = true)::INT8 as active_users,
|
||||
COUNT(*) FILTER (WHERE role::text = 'admin')::INT8 as admin_users,
|
||||
COALESCE(SUM(storage_quota_bytes)::INT8, 0) as total_quota_bytes,
|
||||
COALESCE(SUM(storage_used_bytes)::INT8, 0) as total_used_bytes,
|
||||
COUNT(*) FILTER (WHERE storage_quota_bytes > 0 AND storage_used_bytes > storage_quota_bytes * 0.8)::INT8 as users_over_80,
|
||||
COUNT(*) FILTER (WHERE storage_quota_bytes > 0 AND storage_used_bytes > storage_quota_bytes)::INT8 as users_over_quota
|
||||
FROM auth.users
|
||||
"#
|
||||
)
|
||||
.fetch_one(db_pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(&format!("Database query failed: {}", e)))?;
|
||||
|
||||
use sqlx::Row;
|
||||
let total_quota: i64 = stats_row.get("total_quota_bytes");
|
||||
let total_used: i64 = stats_row.get("total_used_bytes");
|
||||
let usage_percent = if total_quota > 0 {
|
||||
(total_used as f64 / total_quota as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let stats = DashboardStatsDto {
|
||||
server_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
auth_enabled: true,
|
||||
oidc_configured: auth_app.oidc_enabled(),
|
||||
quotas_enabled: true, // Feature flag could be checked here
|
||||
total_users: stats_row.get("total_users"),
|
||||
active_users: stats_row.get("active_users"),
|
||||
admin_users: stats_row.get("admin_users"),
|
||||
total_quota_bytes: total_quota,
|
||||
total_used_bytes: total_used,
|
||||
storage_usage_percent: (usage_percent * 100.0).round() / 100.0,
|
||||
users_over_80_percent: stats_row.get("users_over_80"),
|
||||
users_over_quota: stats_row.get("users_over_quota"),
|
||||
};
|
||||
|
||||
Ok(Json(stats))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// User Management
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
||||
async fn list_users(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ListUsersQueryDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
let limit = query.limit.unwrap_or(100).min(500);
|
||||
let offset = query.offset.unwrap_or(0);
|
||||
|
||||
let users = auth.auth_application_service.list_users(limit, offset).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to list users: {}", e)))?;
|
||||
|
||||
let total = auth.auth_application_service.count_users_efficient().await.unwrap_or(0);
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"users": users,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})))
|
||||
}
|
||||
|
||||
/// GET /api/admin/users/:id — get single user
|
||||
async fn get_user(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
let user = auth.auth_application_service.get_user_admin(&id).await
|
||||
.map_err(|e| AppError::not_found(&format!("User not found: {}", e)))?;
|
||||
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
/// DELETE /api/admin/users/:id — delete a user
|
||||
async fn delete_user(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||
|
||||
// Prevent self-deletion
|
||||
if admin_id == id {
|
||||
return Err(AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot delete your own account",
|
||||
"SelfDeletion",
|
||||
));
|
||||
}
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
auth.auth_application_service.delete_user_admin(&id).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to delete user: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "User deleted successfully"
|
||||
}))))
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/role — change user role
|
||||
async fn update_user_role(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<UpdateUserRoleDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||
|
||||
// Prevent changing own role
|
||||
if admin_id == id {
|
||||
return Err(AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot change your own role",
|
||||
"SelfRoleChange",
|
||||
));
|
||||
}
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
auth.auth_application_service.change_user_role(&id, &dto.role).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to change role: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": format!("User role updated to '{}'", dto.role)
|
||||
}))))
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/active — activate/deactivate user
|
||||
async fn update_user_active(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<UpdateUserActiveDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||
|
||||
// Prevent deactivating yourself
|
||||
if admin_id == id && !dto.active {
|
||||
return Err(AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot deactivate your own account",
|
||||
"SelfDeactivation",
|
||||
));
|
||||
}
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
auth.auth_application_service.set_user_active(&id, dto.active).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to update user status: {}", e)))?;
|
||||
|
||||
let status = if dto.active { "activated" } else { "deactivated" };
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": format!("User {}", status)
|
||||
}))))
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/quota — update user storage quota
|
||||
async fn update_user_quota(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<UpdateUserQuotaDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
auth.auth_application_service.update_user_quota(&id, dto.quota_bytes).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to update quota: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "User quota updated",
|
||||
"quota_bytes": dto.quota_bytes,
|
||||
}))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user