feat(admin): add admin settings panel for OIDC configuration
- Admin UI at /admin.html with settings management interface - REST API: GET/PUT /api/admin/settings/oidc, POST .../test, GET .../general - DB-backed settings in auth.admin_settings table (PostgreSQL) - OIDC auto-discovery from issuer URL (.well-known/openid-configuration) - Hot-reload: OIDC config changes apply without server restart - Role-based access: admin-only endpoints with 403 for regular users - Client secret stored securely, never exposed in GET responses - Env var override detection shown in admin UI - Clean architecture: repository trait, PG implementation, service, handler
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, put, post},
|
||||
extract::{State, Json},
|
||||
http::{StatusCode, HeaderMap, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::application::dtos::settings_dto::{SaveOidcSettingsDto, TestOidcConnectionDto};
|
||||
use crate::interfaces::errors::AppError;
|
||||
|
||||
/// Admin API routes — all require admin role.
|
||||
pub fn admin_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.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))
|
||||
}
|
||||
|
||||
/// Validate JWT and require admin role. Returns (user_id, role).
|
||||
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, String), AppError> {
|
||||
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())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.ok_or_else(|| AppError::unauthorized("Authorization token required"))?;
|
||||
|
||||
let claims = auth.token_service.validate_token(token)
|
||||
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
|
||||
|
||||
if claims.role != "admin" {
|
||||
return Err(AppError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Admin access required",
|
||||
"Forbidden",
|
||||
));
|
||||
}
|
||||
|
||||
Ok((claims.sub, claims.role))
|
||||
}
|
||||
|
||||
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
||||
async fn get_oidc_settings(
|
||||
State(state): State<AppState>,
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
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"))?;
|
||||
|
||||
svc.save_oidc_settings(dto, &user_id).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(
|
||||
State(state): State<AppState>,
|
||||
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
|
||||
async fn get_general_settings(
|
||||
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 user_count = auth.auth_application_service.count_all_users().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,
|
||||
})))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod admin_handler;
|
||||
pub mod file_handler;
|
||||
pub mod folder_handler;
|
||||
pub mod i18n_handler;
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::interfaces::api::handlers::file_handler::FileHandler;
|
||||
use crate::interfaces::api::handlers::i18n_handler::I18nHandler;
|
||||
use crate::interfaces::api::handlers::chunked_upload_handler::ChunkedUploadHandler;
|
||||
use crate::interfaces::api::handlers::trash_handler;
|
||||
use crate::interfaces::api::handlers::admin_handler;
|
||||
use crate::interfaces::api::handlers::batch_handler::{
|
||||
self, BatchHandlerState
|
||||
};
|
||||
@@ -282,6 +283,11 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
|
||||
// NOTE: CalDAV and CardDAV routes are mounted at top-level (/caldav, /carddav)
|
||||
// in main.rs for protocol compliance, NOT under /api.
|
||||
|
||||
// Admin settings routes (protected by admin_guard inside the handler)
|
||||
let admin_router = admin_handler::admin_routes()
|
||||
.with_state(app_state.clone());
|
||||
router = router.nest("/admin", admin_router);
|
||||
|
||||
router
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
|
||||
Reference in New Issue
Block a user