// Dashboard module for LLM Proxy Gateway mod auth; mod clients; mod models; mod providers; pub mod sessions; mod system; mod usage; mod users; mod websocket; use axum::{ extract::{Request, State}, middleware::Next, response::Response, Router, routing::{delete, get, post, put}, }; use axum::http::{header, HeaderValue}; use serde::Serialize; use tower_http::{ limit::RequestBodyLimitLayer, set_header::SetResponseHeaderLayer, }; use crate::state::AppState; use sessions::SessionManager; // Dashboard state #[derive(Clone)] struct DashboardState { app_state: AppState, session_manager: SessionManager, } // API Response types #[derive(Serialize)] struct ApiResponse { success: bool, data: Option, error: Option, } impl ApiResponse { fn success(data: T) -> Self { Self { success: true, data: Some(data), error: None, } } fn error(error: String) -> Self { Self { success: false, data: None, error: Some(error), } } } /// Rate limiting middleware for dashboard routes async fn dashboard_rate_limit_middleware( State(_dashboard_state): State, request: Request, next: Next, ) -> Result { // Bypass rate limiting for dashboard routes to prevent "Failed to load statistics" // when the UI makes many concurrent requests on load. // Dashboard endpoints are already secured via auth::require_admin. Ok(next.run(request).await) } // Dashboard routes pub fn router(state: AppState) -> Router { let session_manager = SessionManager::new(24); // 24-hour session TTL let dashboard_state = DashboardState { app_state: state, session_manager, }; // Security headers let csp_header: SetResponseHeaderLayer = SetResponseHeaderLayer::overriding( header::CONTENT_SECURITY_POLICY, "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com; font-src 'self' https://cdnjs.cloudflare.com https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' ws:;" .parse() .unwrap(), ); let x_frame_options: SetResponseHeaderLayer = SetResponseHeaderLayer::overriding( header::X_FRAME_OPTIONS, "DENY".parse().unwrap(), ); let x_content_type_options: SetResponseHeaderLayer = SetResponseHeaderLayer::overriding( header::X_CONTENT_TYPE_OPTIONS, "nosniff".parse().unwrap(), ); let strict_transport_security: SetResponseHeaderLayer = SetResponseHeaderLayer::overriding( header::STRICT_TRANSPORT_SECURITY, "max-age=31536000; includeSubDomains".parse().unwrap(), ); Router::new() // Static file serving .fallback_service(tower_http::services::ServeDir::new("static")) // WebSocket endpoint .route("/ws", get(websocket::handle_websocket)) // API endpoints .route("/api/auth/login", post(auth::handle_login)) .route("/api/auth/status", get(auth::handle_auth_status)) .route("/api/auth/logout", post(auth::handle_logout)) .route("/api/auth/change-password", post(auth::handle_change_password)) .route( "/api/users", get(users::handle_get_users).post(users::handle_create_user), ) .route( "/api/users/{id}", put(users::handle_update_user).delete(users::handle_delete_user), ) .route("/api/usage/summary", get(usage::handle_usage_summary)) .route("/api/usage/time-series", get(usage::handle_time_series)) .route("/api/usage/clients", get(usage::handle_clients_usage)) .route("/api/usage/providers", get(usage::handle_providers_usage)) .route("/api/usage/detailed", get(usage::handle_detailed_usage)) .route("/api/analytics/breakdown", get(usage::handle_analytics_breakdown)) .route("/api/models", get(models::handle_get_models)) .route("/api/models/{id}", put(models::handle_update_model)) .route( "/api/clients", get(clients::handle_get_clients).post(clients::handle_create_client), ) .route( "/api/clients/{id}", get(clients::handle_get_client) .put(clients::handle_update_client) .delete(clients::handle_delete_client), ) .route("/api/clients/{id}/usage", get(clients::handle_client_usage)) .route( "/api/clients/{id}/tokens", get(clients::handle_get_client_tokens).post(clients::handle_create_client_token), ) .route( "/api/clients/{id}/tokens/{token_id}", delete(clients::handle_delete_client_token), ) .route("/api/providers", get(providers::handle_get_providers)) .route( "/api/providers/{name}", get(providers::handle_get_provider).put(providers::handle_update_provider), ) .route("/api/providers/{name}/test", post(providers::handle_test_provider)) .route("/api/system/health", get(system::handle_system_health)) .route("/api/system/metrics", get(system::handle_system_metrics)) .route("/api/system/logs", get(system::handle_system_logs)) .route("/api/system/backup", post(system::handle_system_backup)) .route( "/api/system/settings", get(system::handle_get_settings).post(system::handle_update_settings), ) // Security layers .layer(RequestBodyLimitLayer::new(10 * 1024 * 1024)) // 10 MB limit .layer(csp_header) .layer(x_frame_options) .layer(x_content_type_options) .layer(strict_transport_security) // Rate limiting middleware .layer(axum::middleware::from_fn_with_state( dashboard_state.clone(), dashboard_rate_limit_middleware, )) .with_state(dashboard_state) }