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
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user