use axum::{
body::Body,
extract::State,
http::{header, StatusCode, Uri},
response::{IntoResponse, Response},
};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "$OUT_DIR/nyx-agent-ui-dist/"]
pub struct UiAssets;
#[derive(Clone, Default)]
pub struct UiBootstrap {
pub auth_token: Option<String>,
}
pub fn resolve(path: &str) -> Option<UiResponse> {
resolve_with(path, &UiBootstrap::default())
}
pub fn resolve_with(path: &str, bootstrap: &UiBootstrap) -> Option<UiResponse> {
let clean = path.trim_start_matches('/');
let candidate = if clean.is_empty() { "index.html" } else { clean };
if let Some(file) = UiAssets::get(candidate) {
return Some(UiResponse::from_embedded(candidate, file, bootstrap));
}
if !candidate.contains('.') {
if let Some(file) = UiAssets::get("index.html") {
return Some(UiResponse::from_embedded("index.html", file, bootstrap));
}
}
None
}
pub struct UiResponse {
body: Vec<u8>,
content_type: String,
}
impl UiResponse {
fn from_embedded(path: &str, file: rust_embed::EmbeddedFile, bootstrap: &UiBootstrap) -> Self {
let mime = mime_guess::from_path(path).first_or_octet_stream();
let body = if path == "index.html" {
inject_bootstrap(&file.data, bootstrap)
} else {
file.data.into_owned()
};
Self { body, content_type: mime.essence_str().to_string() }
}
}
fn inject_bootstrap(html: &[u8], bootstrap: &UiBootstrap) -> Vec<u8> {
let Ok(text) = std::str::from_utf8(html) else {
return html.to_vec();
};
let payload =
format!("<script>window.__NYX_AGENT_BOOTSTRAP__={};</script>", serde_payload(bootstrap));
if let Some(pos) = text.find("<head>") {
let mut out = String::with_capacity(text.len() + payload.len());
out.push_str(&text[..pos + "<head>".len()]);
out.push_str(&payload);
out.push_str(&text[pos + "<head>".len()..]);
out.into_bytes()
} else {
let mut out = payload.into_bytes();
out.extend_from_slice(html);
out
}
}
fn serde_payload(bootstrap: &UiBootstrap) -> String {
match &bootstrap.auth_token {
Some(token) => format!("{{\"authToken\":\"{}\"}}", token.replace('"', "\\\"")),
None => "{}".to_string(),
}
}
impl IntoResponse for UiResponse {
fn into_response(self) -> Response {
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, self.content_type)
.body(Body::from(self.body))
.expect("build static response")
}
}
pub async fn spa_handler(uri: Uri) -> Response {
spa_handler_with(uri, &UiBootstrap::default()).await
}
pub async fn spa_handler_with(uri: Uri, bootstrap: &UiBootstrap) -> Response {
let path = uri.path();
if path.starts_with("/api/") || path == "/api" {
return (StatusCode::NOT_FOUND, "not found").into_response();
}
match resolve_with(path, bootstrap) {
Some(resp) => resp.into_response(),
None => (StatusCode::NOT_FOUND, "not found").into_response(),
}
}
pub async fn spa_handler_stateful(
State(bootstrap): State<std::sync::Arc<UiBootstrap>>,
uri: Uri,
) -> Response {
spa_handler_with(uri, &bootstrap).await
}