Skip to main content

adk_server/
web_ui.rs

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    // Use configured backend URL or default to relative "/api"
53    // Relative URLs work better as they adapt to the actual host/port
54    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 file not found, serve index.html for SPA routing (if we were doing that),
70            // but for static assets, 404 is correct.
71            // However, Angular apps often use HTML5 pushState, so we might need to fallback to index.html
72            // for non-asset paths.
73            // Let's check if it looks like a file extension.
74            if path.contains('.') {
75                StatusCode::NOT_FOUND.into_response()
76            } else {
77                // Fallback to index.html
78                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}