use std::sync::Arc; use axum::http::HeaderValue; use axum::routing::{delete, get, post, put}; use axum::Router; use tower_http::set_header::SetResponseHeaderLayer; use crate::error::AppState; /// Content-Security-Policy tuned to the built Trunk frontend. /// /// * `script-src 'unsafe-inline'` — Trunk emits one inline bootstrap module /// in index.html; every real JS file also carries an SRI integrity hash. /// * `'wasm-unsafe-eval'` — compiling the same-origin WASM module. /// * `style-src 'unsafe-inline'` — the context menu sets an inline `style=`. /// * Everything else locked to the same origin; frames/plugins banned. const CSP: &str = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"; mod admin; mod auth; mod common; mod files; mod shares; mod spa; pub fn router(state: Arc) -> Router { Router::new() .route("/api/auth/login", post(auth::login)) .route("/api/auth/logout", post(auth::logout)) .route("/api/auth/me", get(auth::me)) .route("/api/auth/setup", post(auth::setup)) .route("/api/files/{root_id}", get(files::list_root)) .route("/api/files/{root_id}/{*path}", get(files::file_get)) .route("/api/files/{root_id}/{*path}", put(files::file_put)) .route("/api/files/{root_id}", post(files::dispatch_root)) .route("/api/files/{root_id}/{*path}", post(files::dispatch)) .route("/api/files/{root_id}/{*path}", delete(files::delete)) .route("/api/shares", get(shares::list)) .route("/api/shares", post(shares::create)) .route("/api/shares/{id}", delete(shares::delete)) .route("/api/share/{token}", get(shares::resolve)) .route("/api/admin/users", get(admin::list_users)) .route("/api/admin/users", post(admin::create_user)) .route("/api/admin/users/{id}", put(admin::update_user)) .route("/api/admin/users/{id}", delete(admin::delete_user)) .route("/api/admin/settings", get(admin::get_settings)) .route("/api/admin/settings", put(admin::update_settings)) .fallback(spa::fallback) .with_state(state) // Hard security headers on every response (API and static alike). .layer(SetResponseHeaderLayer::overriding( "content-security-policy".parse().unwrap(), HeaderValue::from_static(CSP), )) .layer(SetResponseHeaderLayer::overriding( "x-content-type-options".parse().unwrap(), HeaderValue::from_static("nosniff"), )) .layer(SetResponseHeaderLayer::overriding( "x-frame-options".parse().unwrap(), HeaderValue::from_static("DENY"), )) .layer(SetResponseHeaderLayer::overriding( "referrer-policy".parse().unwrap(), HeaderValue::from_static("no-referrer"), )) }