2026-02-14 01:29:34 +01:00
|
|
|
use axum::{
|
|
|
|
|
Router,
|
|
|
|
|
extract::{Json, Path, Query, State},
|
|
|
|
|
http::{HeaderMap, StatusCode, header},
|
|
|
|
|
response::IntoResponse,
|
|
|
|
|
routing::{delete, get, post, put},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use crate::application::dtos::settings_dto::{
|
|
|
|
|
AdminCreateUserDto, AdminResetPasswordDto, DashboardStatsDto, ListUsersQueryDto,
|
|
|
|
|
SaveOidcSettingsDto, TestOidcConnectionDto, UpdateUserActiveDto, UpdateUserQuotaDto,
|
|
|
|
|
UpdateUserRoleDto,
|
|
|
|
|
};
|
2026-03-04 23:55:08 +01:00
|
|
|
use crate::application::ports::auth_ports::TokenServicePort;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
|
|
|
|
use crate::interfaces::errors::AppError;
|
2026-02-24 15:11:56 +01:00
|
|
|
use std::sync::Arc;
|
2026-03-07 14:59:32 +01:00
|
|
|
use uuid::Uuid;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
/// Admin API routes — all require admin role.
|
2026-02-24 15:11:56 +01:00
|
|
|
pub fn admin_routes() -> Router<Arc<AppState>> {
|
2026-02-14 01:29:34 +01:00
|
|
|
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", post(create_user))
|
|
|
|
|
.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))
|
|
|
|
|
.route("/users/{id}/password", put(reset_user_password))
|
|
|
|
|
// Registration control
|
|
|
|
|
.route("/settings/registration", get(get_registration_setting))
|
|
|
|
|
.route("/settings/registration", put(set_registration_setting))
|
2026-04-08 15:14:03 +03:00
|
|
|
// Audio metadata
|
|
|
|
|
.route("/audio/metadata/reextract", post(reextract_audio_metadata))
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate JWT and require admin role. Returns (user_id, role).
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, String), AppError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
|
|
|
|
|
|
|
|
|
let token = headers
|
|
|
|
|
.get(header::AUTHORIZATION)
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
2026-03-04 21:55:06 -05:00
|
|
|
.and_then(|v| v.strip_prefix("Bearer ").map(|s| s.to_string()))
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
crate::interfaces::api::cookie_auth::extract_cookie_value(
|
|
|
|
|
headers,
|
|
|
|
|
crate::interfaces::api::cookie_auth::ACCESS_COOKIE,
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-02-14 01:29:34 +01:00
|
|
|
.ok_or_else(|| AppError::unauthorized("Authorization token required"))?;
|
|
|
|
|
|
|
|
|
|
let claims = auth
|
|
|
|
|
.token_service
|
2026-03-04 21:55:06 -05:00
|
|
|
.validate_token(&token)
|
2026-02-14 01:29:34 +01:00
|
|
|
.map_err(|e| AppError::unauthorized(format!("Invalid token: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
if claims.role != "admin" {
|
|
|
|
|
return Err(AppError::new(
|
|
|
|
|
StatusCode::FORBIDDEN,
|
|
|
|
|
"Admin access required",
|
|
|
|
|
"Forbidden",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 14:34:07 +01:00
|
|
|
Ok((
|
|
|
|
|
Uuid::parse_str(&claims.sub)
|
|
|
|
|
.map_err(|_| AppError::internal_error("Invalid user ID in token"))?,
|
|
|
|
|
claims.role,
|
|
|
|
|
))
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
|
|
|
|
async fn get_oidc_settings(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
|
|
|
|
|
|
|
|
|
let settings = svc
|
|
|
|
|
.get_oidc_settings()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to load settings: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(settings))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// PUT /api/admin/settings/oidc — save OIDC settings + hot-reload
|
|
|
|
|
async fn save_oidc_settings(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<SaveOidcSettingsDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (user_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
svc.save_oidc_settings(dto, user_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to save settings: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"message": "OIDC settings saved and applied successfully"
|
|
|
|
|
})),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// POST /api/admin/settings/oidc/test — test OIDC discovery
|
|
|
|
|
async fn test_oidc_connection(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<TestOidcConnectionDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
|
|
|
|
|
|
|
|
|
let result = svc
|
|
|
|
|
.test_oidc_connection(dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Connection test failed: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(result))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/settings/general — system overview (backward compat)
|
|
|
|
|
async fn get_general_settings(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
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 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!({
|
|
|
|
|
"server_version": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"auth_enabled": true,
|
|
|
|
|
"total_users": user_count,
|
|
|
|
|
"oidc_configured": oidc_configured,
|
|
|
|
|
})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Dashboard / Stats
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/dashboard — full dashboard statistics
|
|
|
|
|
async fn get_dashboard_stats(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
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"),
|
|
|
|
|
registration_enabled: {
|
|
|
|
|
if let Some(svc) = state.admin_settings_service.as_ref() {
|
|
|
|
|
svc.get_registration_enabled().await
|
|
|
|
|
} else {
|
|
|
|
|
true // default: enabled
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(stats))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// User Management
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
|
|
|
|
async fn list_users(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
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(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
|
|
|
|
|
|
|
|
|
let user = auth
|
|
|
|
|
.auth_application_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.get_user_admin(id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.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(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
// 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
|
2026-03-07 14:59:32 +01:00
|
|
|
.delete_user_admin(id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.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(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserRoleDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
// 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
|
2026-03-07 14:59:32 +01:00
|
|
|
.change_user_role(id, &dto.role)
|
2026-02-14 01:29:34 +01:00
|
|
|
.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(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserActiveDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
// 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
|
2026-03-07 14:59:32 +01:00
|
|
|
.set_user_active(id, dto.active)
|
2026-02-14 01:29:34 +01:00
|
|
|
.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(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserQuotaDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
|
|
|
|
|
|
|
|
|
auth.auth_application_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.update_user_quota(id, dto.quota_bytes)
|
2026-02-14 01:29:34 +01:00
|
|
|
.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,
|
|
|
|
|
})),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Admin User Creation & Password Reset
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// POST /api/admin/users — create a new user (admin only)
|
|
|
|
|
async fn create_user(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<AdminCreateUserDto>,
|
|
|
|
|
) -> 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
|
|
|
|
|
.admin_create_user(dto)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::new(
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
format!("Failed to create user: {}", e),
|
|
|
|
|
"CreateUserFailed",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok((StatusCode::CREATED, Json(user)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// PUT /api/admin/users/:id/password — reset a user's password (admin only)
|
|
|
|
|
async fn reset_user_password(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<AdminResetPasswordDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
|
|
|
|
|
|
|
|
|
auth.auth_application_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.admin_reset_password(id, &dto.new_password)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::new(
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
format!("Failed to reset password: {}", e),
|
|
|
|
|
"ResetPasswordFailed",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"message": "Password reset successfully"
|
|
|
|
|
})),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Registration Control
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/settings/registration — check if public registration is enabled
|
|
|
|
|
async fn get_registration_setting(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
|
|
|
|
|
|
|
|
|
let val = svc.get_registration_enabled().await;
|
|
|
|
|
|
|
|
|
|
Ok(Json(serde_json::json!({
|
|
|
|
|
"registration_enabled": val,
|
|
|
|
|
})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// PUT /api/admin/settings/registration — enable/disable public registration
|
|
|
|
|
async fn set_registration_setting(
|
2026-02-24 15:11:56 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2026-02-14 01:29:34 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(body): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let enabled = body
|
|
|
|
|
.get("registration_enabled")
|
|
|
|
|
.and_then(|v| v.as_bool())
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
AppError::new(
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
"Missing boolean field 'registration_enabled'",
|
|
|
|
|
"InvalidInput",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
|
|
|
|
|
2026-03-07 14:59:32 +01:00
|
|
|
svc.set_registration_enabled(enabled, admin_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to save setting: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"message": format!("Public registration {}", if enabled { "enabled" } else { "disabled" }),
|
|
|
|
|
"registration_enabled": enabled,
|
|
|
|
|
})),
|
|
|
|
|
))
|
|
|
|
|
}
|
2026-04-08 15:14:03 +03:00
|
|
|
|
|
|
|
|
async fn reextract_audio_metadata(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let audio_service = state
|
|
|
|
|
.applications
|
|
|
|
|
.audio_metadata_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Audio metadata service not available"))?;
|
|
|
|
|
|
|
|
|
|
let result = audio_service
|
|
|
|
|
.reextract_all_audio_metadata()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to re-extract audio metadata: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(serde_json::json!({
|
|
|
|
|
"message": "Audio metadata extraction complete",
|
|
|
|
|
"total": result.total,
|
|
|
|
|
"processed": result.processed,
|
|
|
|
|
"failed": result.failed,
|
|
|
|
|
})))
|
|
|
|
|
}
|