tandem_server/webui/
mod.rs1use axum::body::Body;
2use axum::http::header;
3use axum::http::{HeaderValue, StatusCode};
4use axum::response::{IntoResponse, Response};
5use axum::routing::get;
6use axum::Router;
7
8static ADMIN_HTML: &str = include_str!("admin.html");
9
10const CSP_HEADER: &str = "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src data:; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
11
12pub fn web_ui_router<S>(prefix: &str) -> Router<S>
13where
14 S: Clone + Send + Sync + 'static,
15{
16 let base = normalize_prefix(prefix);
17 let wildcard = format!("{}/{{*path}}", base);
18 Router::new()
19 .route(&base, get(serve_index))
20 .route(&format!("{}/", base), get(serve_index))
21 .route(&wildcard, get(serve_index))
22}
23
24async fn serve_index() -> impl IntoResponse {
25 let mut response = Response::new(Body::from(ADMIN_HTML));
26 *response.status_mut() = StatusCode::OK;
27 let headers = response.headers_mut();
28 headers.insert(
29 header::CONTENT_TYPE,
30 HeaderValue::from_static("text/html; charset=utf-8"),
31 );
32 headers.insert(
33 header::CACHE_CONTROL,
34 HeaderValue::from_static("no-store, max-age=0"),
35 );
36 headers.insert(
37 header::HeaderName::from_static("content-security-policy"),
38 HeaderValue::from_static(CSP_HEADER),
39 );
40 headers.insert(
41 header::HeaderName::from_static("x-frame-options"),
42 HeaderValue::from_static("DENY"),
43 );
44 headers.insert(
45 header::HeaderName::from_static("x-content-type-options"),
46 HeaderValue::from_static("nosniff"),
47 );
48 headers.insert(
49 header::HeaderName::from_static("referrer-policy"),
50 HeaderValue::from_static("no-referrer"),
51 );
52 response
53}
54
55fn normalize_prefix(prefix: &str) -> String {
56 let raw = prefix.trim();
57 if raw.is_empty() || raw == "/" {
58 return "/admin".to_string();
59 }
60 let with_leading = if raw.starts_with('/') {
61 raw.to_string()
62 } else {
63 format!("/{raw}")
64 };
65 with_leading.trim_end_matches('/').to_string()
66}