Skip to content
Clone
7ab1606
92 lines 2.9 KB Rust
use actix_web::http::header::{CACHE_CONTROL, CONTENT_TYPE};
use actix_web::{HttpRequest, HttpResponse};
pub struct Asset {
pub path: &'static str,
pub content_type: &'static str,
pub bytes: &'static [u8],
}
include!(concat!(env!("OUT_DIR"), "/assets.rs"));
pub fn count() -> usize {
ASSETS.len()
}
/// 非 SPA 的首段路径:这些前缀只服务于接口,拼错时给 JSON 404,
/// 避免把已移除的旧路径回落到 index.html 变成 200。
const API_PREFIXES: [&str; 1] = ["api"];
/// MPA 子应用前缀 → 入口 HTML,与 vite.config.ts 的 APP_ENTRIES 保持一致。
const APP_ENTRIES: [(&str, &str); 2] = [("auth", "auth/index.html"), ("app", "app/index.html")];
/// 未命中任何子应用前缀时回落落地页。
const LANDING_ENTRY: &str = "index.html";
pub async fn serve(request: HttpRequest) -> HttpResponse {
let raw = request.path().trim_start_matches('/');
let path = if raw.is_empty() { "index.html" } else { raw };
let looks_like_api = 'inline_looks_like_api: {
if path.starts_with(".well-known/") {
break 'inline_looks_like_api true;
}
let first = path.split('/').next().unwrap_or_default();
API_PREFIXES.contains(&first)
};
if looks_like_api {
return api_not_found();
}
if lookup(path).is_none()
&& path
.rsplit('/')
.next()
.and_then(|name| name.rsplit_once('.'))
.is_some_and(|(_, ext)| {
!ext.is_empty() && ext.len() <= 5 && ext.chars().all(|c| c.is_ascii_alphanumeric())
})
{
return api_not_found();
}
let fallback = app_entry(path);
match lookup(path).or_else(|| lookup(fallback)) {
Some(asset) => {
let cache_control = if asset.path.starts_with("assets/") {
"public, max-age=31536000, immutable"
} else {
"no-cache"
};
HttpResponse::Ok()
.insert_header((CONTENT_TYPE, asset.content_type))
.insert_header((CACHE_CONTROL, cache_control))
.body(asset.bytes)
}
None => HttpResponse::ServiceUnavailable()
.insert_header((CONTENT_TYPE, "text/plain; charset=utf-8"))
.body("view/dist has no build output; run `just identity-web-build` and rebuild app\n"),
}
}
fn app_entry(path: &str) -> &'static str {
let first = path.split('/').next().unwrap_or_default();
APP_ENTRIES
.iter()
.find(|(prefix, _)| *prefix == first)
.map(|(_, entry)| *entry)
.unwrap_or(LANDING_ENTRY)
}
fn lookup(path: &str) -> Option<&'static Asset> {
ASSETS.iter().find(|asset| asset.path == path)
}
fn api_not_found() -> HttpResponse {
HttpResponse::NotFound().json(
api::rest::AppResponseData::<api::rest::NoData>::err(404, "unknown api route")
.with_error_code("not_found"),
)
}