construct/gateway/
static_files.rs1use axum::{
6 extract::State,
7 http::{StatusCode, Uri, header},
8 response::{IntoResponse, Response},
9};
10use rust_embed::Embed;
11
12use super::AppState;
13
14#[derive(Embed)]
15#[folder = "web/dist/"]
16struct WebAssets;
17
18pub async fn handle_static(uri: Uri) -> Response {
20 let path = uri
21 .path()
22 .strip_prefix("/_app/")
23 .unwrap_or(uri.path())
24 .trim_start_matches('/');
25
26 serve_embedded_file(path)
27}
28
29pub async fn handle_spa_fallback(State(state): State<AppState>) -> Response {
32 let Some(content) = WebAssets::get("index.html") else {
33 return (
34 StatusCode::SERVICE_UNAVAILABLE,
35 "Web dashboard not available. Build it with: cd web && npm ci && npm run build",
36 )
37 .into_response();
38 };
39
40 let html = String::from_utf8_lossy(&content.data);
41
42 let html = if state.path_prefix.is_empty() {
44 html.into_owned()
45 } else {
46 let pfx = &state.path_prefix;
47 let json_pfx = serde_json::to_string(pfx).unwrap_or_else(|_| "\"\"".to_string());
49 let script = format!("<script>window.__CONSTRUCT_BASE__={json_pfx};</script>");
50 html.replace("/_app/", &format!("{pfx}/_app/"))
52 .replace("<head>", &format!("<head>{script}"))
53 };
54
55 (
56 StatusCode::OK,
57 [
58 (header::CONTENT_TYPE, "text/html; charset=utf-8".to_string()),
59 (header::CACHE_CONTROL, "no-cache".to_string()),
60 ],
61 html,
62 )
63 .into_response()
64}
65
66fn serve_embedded_file(path: &str) -> Response {
67 match WebAssets::get(path) {
68 Some(content) => {
69 let mime = mime_guess::from_path(path)
70 .first_or_octet_stream()
71 .to_string();
72
73 (
74 StatusCode::OK,
75 [
76 (header::CONTENT_TYPE, mime),
77 (
78 header::CACHE_CONTROL,
79 if path.contains("assets/") {
80 "public, max-age=31536000, immutable".to_string()
82 } else {
83 "no-cache".to_string()
85 },
86 ),
87 ],
88 content.data.to_vec(),
89 )
90 .into_response()
91 }
92 None => (StatusCode::NOT_FOUND, "Not found").into_response(),
93 }
94}