1use axum::{
2 Json,
3 body::Body,
4 extract::State,
5 http::{StatusCode, Uri, header},
6 response::{IntoResponse, Response},
7};
8use rust_embed::RustEmbed;
9use serde::Serialize;
10
11#[derive(RustEmbed)]
12#[folder = "assets/webui"]
13struct Assets;
14
15fn embedded_asset_response(path: &str, content: rust_embed::EmbeddedFile) -> Response {
16 let mime = mime_guess::from_path(path).first_or_octet_stream();
17 let mut response = Body::from(content.data).into_response();
18 let headers = response.headers_mut();
19 headers.insert(
20 header::CONTENT_TYPE,
21 header::HeaderValue::from_str(mime.as_ref())
22 .unwrap_or_else(|_| header::HeaderValue::from_static("application/octet-stream")),
23 );
24 headers.insert(
25 header::CACHE_CONTROL,
26 if path == "index.html" {
27 header::HeaderValue::from_static("no-cache")
28 } else {
29 header::HeaderValue::from_static("public, max-age=31536000, immutable")
30 },
31 );
32 headers.insert(header::X_CONTENT_TYPE_OPTIONS, header::HeaderValue::from_static("nosniff"));
33 headers.insert(header::REFERRER_POLICY, header::HeaderValue::from_static("no-referrer"));
34 if path == "index.html" {
35 headers.insert(
36 header::CONTENT_SECURITY_POLICY,
37 header::HeaderValue::from_static(
38 "default-src 'self'; connect-src 'self'; img-src 'self' data: blob:; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
39 ),
40 );
41 }
42 response
43}
44
45#[derive(Serialize)]
46pub struct RuntimeConfig {
47 #[serde(rename = "backendUrl")]
48 pub backend_url: String,
49}
50
51pub async fn serve_runtime_config(State(config): State<crate::ServerConfig>) -> impl IntoResponse {
52 let backend_url = config.backend_url.unwrap_or_else(|| "/api".to_string());
55
56 Json(RuntimeConfig { backend_url })
57}
58
59pub async fn serve_ui_assets(uri: Uri) -> impl IntoResponse {
60 let mut path = uri.path().trim_start_matches("/ui/").to_string();
61
62 if path.is_empty() {
63 path = "index.html".to_string();
64 }
65
66 match Assets::get(&path) {
67 Some(content) => embedded_asset_response(&path, content),
68 None => {
69 if path.contains('.') {
75 StatusCode::NOT_FOUND.into_response()
76 } else {
77 match Assets::get("index.html") {
79 Some(content) => embedded_asset_response("index.html", content),
80 None => StatusCode::NOT_FOUND.into_response(),
81 }
82 }
83 }
84 }
85}
86
87pub async fn root_redirect() -> impl IntoResponse {
88 axum::response::Redirect::to("/ui/")
89}
90
91pub async fn serve_ui_index() -> impl IntoResponse {
92 match Assets::get("index.html") {
93 Some(content) => embedded_asset_response("index.html", content),
94 None => StatusCode::NOT_FOUND.into_response(),
95 }
96}