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:
Dionisio
2026-02-11 01:08:00 +01:00
parent 1a1dee9179
commit ccd071911d
7 changed files with 951 additions and 230 deletions
@@ -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
// ========================================================================