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}