Skip to main content

lean_ctx/gateway_server/
admin_ui.rs

1//! Embedded admin dashboard (enterprise#45) — the org monitoring console
2//! served from the gateway's admin port.
3//!
4//! Everything is compiled into the binary (`include_str!`/`include_bytes!`,
5//! same rule as the Context Cockpit): no CDN, no build step, renders offline
6//! and inside airgapped clusters. Fonts and the vendored Chart.js are shared
7//! with the cockpit sources so both surfaces stay visually identical.
8//!
9//! Auth split: this router serves only the *static shell* (login screen) and
10//! is mounted **outside** the Bearer middleware — every number the shell
11//! renders comes from the `/api/admin/*` endpoints, which stay guarded. The
12//! token never appears in a URL; the shell keeps it in `sessionStorage`.
13
14use axum::http::header;
15use axum::response::IntoResponse;
16
17const ADMIN_INDEX_HTML: &str = include_str!("static/index.html");
18const ADMIN_CSS: &str = include_str!("static/admin.css");
19const ADMIN_JS: &str = include_str!("static/admin.js");
20
21// Shared with the cockpit: identical typography and chart engine.
22const FONTS_CSS: &str = include_str!("../dashboard/static/fonts/fonts.css");
23const FONT_INTER_WOFF2: &[u8] = include_bytes!("../dashboard/static/fonts/inter-variable.woff2");
24const FONT_JETBRAINS_WOFF2: &[u8] =
25    include_bytes!("../dashboard/static/fonts/jetbrains-mono-variable.woff2");
26const FONT_SPACE_GROTESK_WOFF2: &[u8] =
27    include_bytes!("../dashboard/static/fonts/space-grotesk-variable.woff2");
28const VENDOR_CHART_JS: &str = include_str!("../dashboard/static/vendor/chart.umd.min.js");
29
30/// Static-shell router. Mounted unguarded (see module docs).
31pub fn router() -> axum::Router {
32    axum::Router::new()
33        .route("/", axum::routing::get(index))
34        .route("/static/admin.css", axum::routing::get(css))
35        .route("/static/admin.js", axum::routing::get(js))
36        .route("/static/fonts/fonts.css", axum::routing::get(fonts_css))
37        .route(
38            "/static/vendor/chart.umd.min.js",
39            axum::routing::get(chart_js),
40        )
41        .route("/static/fonts/{file}", axum::routing::get(font_file))
42}
43
44async fn index() -> impl IntoResponse {
45    (
46        [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
47        ADMIN_INDEX_HTML,
48    )
49}
50
51async fn css() -> impl IntoResponse {
52    (
53        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
54        ADMIN_CSS,
55    )
56}
57
58async fn js() -> impl IntoResponse {
59    (
60        [(
61            header::CONTENT_TYPE,
62            "application/javascript; charset=utf-8",
63        )],
64        ADMIN_JS,
65    )
66}
67
68async fn fonts_css() -> impl IntoResponse {
69    (
70        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
71        FONTS_CSS,
72    )
73}
74
75async fn chart_js() -> impl IntoResponse {
76    (
77        [(
78            header::CONTENT_TYPE,
79            "application/javascript; charset=utf-8",
80        )],
81        VENDOR_CHART_JS,
82    )
83}
84
85async fn font_file(
86    axum::extract::Path(file): axum::extract::Path<String>,
87) -> axum::response::Response {
88    let bytes: &'static [u8] = match file.as_str() {
89        "inter-variable.woff2" => FONT_INTER_WOFF2,
90        "jetbrains-mono-variable.woff2" => FONT_JETBRAINS_WOFF2,
91        "space-grotesk-variable.woff2" => FONT_SPACE_GROTESK_WOFF2,
92        _ => return axum::http::StatusCode::NOT_FOUND.into_response(),
93    };
94    ([(header::CONTENT_TYPE, "font/woff2")], bytes).into_response()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn embedded_assets_are_nonempty_and_wired() {
103        assert!(ADMIN_INDEX_HTML.contains("<!doctype html"));
104        assert!(
105            ADMIN_INDEX_HTML.contains("/static/admin.js"),
106            "shell must load the app script"
107        );
108        assert!(ADMIN_CSS.contains(":root"), "design tokens present");
109        assert!(
110            ADMIN_JS.contains("/api/admin/usage"),
111            "app must talk to the guarded API"
112        );
113        assert!(!VENDOR_CHART_JS.is_empty());
114        assert!(!FONT_INTER_WOFF2.is_empty());
115    }
116
117    #[test]
118    fn shell_never_embeds_credentials() {
119        // The shell is served unguarded — it must not contain tokens or
120        // secret-looking material (the Bearer token arrives via user input).
121        for needle in ["Bearer ", "LEAN_CTX_GATEWAY_ADMIN_TOKEN="] {
122            assert!(
123                !ADMIN_INDEX_HTML.contains(needle),
124                "index.html must not embed {needle}"
125            );
126        }
127        assert!(
128            !ADMIN_JS.contains("localStorage.setItem('leanctx-admin-token'"),
129            "token must live in sessionStorage, not persist in localStorage"
130        );
131    }
132}