tandem-server 0.4.13

HTTP server for Tandem engine APIs
Documentation
use axum::body::Body;
use axum::http::header;
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;

static ADMIN_HTML: &str = include_str!("admin.html");

const 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'";

pub fn web_ui_router<S>(prefix: &str) -> Router<S>
where
    S: Clone + Send + Sync + 'static,
{
    let base = normalize_prefix(prefix);
    let wildcard = format!("{}/{{*path}}", base);
    Router::new()
        .route(&base, get(serve_index))
        .route(&format!("{}/", base), get(serve_index))
        .route(&wildcard, get(serve_index))
}

async fn serve_index() -> impl IntoResponse {
    let mut response = Response::new(Body::from(ADMIN_HTML));
    *response.status_mut() = StatusCode::OK;
    let headers = response.headers_mut();
    headers.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("text/html; charset=utf-8"),
    );
    headers.insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("no-store, max-age=0"),
    );
    headers.insert(
        header::HeaderName::from_static("content-security-policy"),
        HeaderValue::from_static(CSP_HEADER),
    );
    headers.insert(
        header::HeaderName::from_static("x-frame-options"),
        HeaderValue::from_static("DENY"),
    );
    headers.insert(
        header::HeaderName::from_static("x-content-type-options"),
        HeaderValue::from_static("nosniff"),
    );
    headers.insert(
        header::HeaderName::from_static("referrer-policy"),
        HeaderValue::from_static("no-referrer"),
    );
    response
}

fn normalize_prefix(prefix: &str) -> String {
    let raw = prefix.trim();
    if raw.is_empty() || raw == "/" {
        return "/admin".to_string();
    }
    let with_leading = if raw.starts_with('/') {
        raw.to_string()
    } else {
        format!("/{raw}")
    };
    with_leading.trim_end_matches('/').to_string()
}