Skip to main content

featherbit/admin/
ui.rs

1//! Serves the admin web UI (React SPA node-graph editor) from assets
2//! embedded into the binary at build time via `rust-embed`. Mounted as the
3//! admin router's unauthenticated fallback; the SPA authenticates its own
4//! calls to the admin API.
5
6use axum::http::{header, StatusCode, Uri};
7use axum::response::{Html, IntoResponse, Response};
8use rust_embed::Embed;
9
10/// Compiled UI bundle (`ui/dist/`, produced by the frontend build and
11/// embedded via `build.rs`).
12#[derive(Embed)]
13#[folder = "ui/dist/"]
14struct UiAssets;
15
16/// Fallback handler for all paths not matched by the admin API.
17///
18/// Serves the embedded asset matching the request path with a MIME type
19/// guessed from its extension; if no asset matches, falls back to
20/// `index.html` so client-side routes resolve within the SPA. Returns
21/// `404 Not Found` only when the UI bundle itself is absent from the binary.
22pub async fn serve_ui(uri: Uri) -> Response {
23    let path = uri.path().trim_start_matches('/');
24
25    // Try to serve the exact file
26    if let Some(file) = UiAssets::get(path) {
27        let mime = mime_guess::from_path(path).first_or_octet_stream();
28        return (
29            StatusCode::OK,
30            [(header::CONTENT_TYPE, mime.as_ref())],
31            file.data.to_vec(),
32        )
33            .into_response();
34    }
35
36    // SPA fallback: serve index.html for all non-API, non-file routes
37    if let Some(index) = UiAssets::get("index.html") {
38        return Html(String::from_utf8_lossy(&index.data).to_string()).into_response();
39    }
40
41    (StatusCode::NOT_FOUND, "Not found").into_response()
42}