bd5ca2dd98
This commit adds 'unsafe-inline' to the script-src CSP directive. The frontend dashboard heavily relies on inline event handlers (e.g., onclick=...) dynamically generated via template literals in its vanilla JavaScript architecture. Without this directive, modern browsers block these handlers, rendering interactive elements like the Config button completely inert.
175 lines
6.0 KiB
Rust
175 lines
6.0 KiB
Rust
// 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<T> {
|
|
success: bool,
|
|
data: Option<T>,
|
|
error: Option<String>,
|
|
}
|
|
|
|
impl<T> ApiResponse<T> {
|
|
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<DashboardState>,
|
|
request: Request,
|
|
next: Next,
|
|
) -> Result<Response, crate::errors::AppError> {
|
|
// 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<HeaderValue> = 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<HeaderValue> = SetResponseHeaderLayer::overriding(
|
|
header::X_FRAME_OPTIONS,
|
|
"DENY".parse().unwrap(),
|
|
);
|
|
let x_content_type_options: SetResponseHeaderLayer<HeaderValue> = SetResponseHeaderLayer::overriding(
|
|
header::X_CONTENT_TYPE_OPTIONS,
|
|
"nosniff".parse().unwrap(),
|
|
);
|
|
let strict_transport_security: SetResponseHeaderLayer<HeaderValue> = 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)
|
|
}
|