//! Authenticated documentation Reader and OpenAPI/Swagger UI on the admin listener. //! Documentation and vendor assets are embedded at compile time; no runtime filesystem or CDN access. use axum::{ response::{Html, IntoResponse, Response}, Json, }; use http::StatusCode; use serde_json::{json, Value}; use std::sync::OnceLock; // Keep Reader input independent from build-script output. `include_str!` makes every // document a direct compiler input, so a clean or partially removed target directory // cannot leave the crate pointing at a missing OUT_DIR artifact. mod documents; use documents::READER_DOCUMENTS; pub(super) async fn index() -> Html<&'static str> { Html(include_str!("web/docs.html")) } pub(super) async fn script() -> impl IntoResponse { ( [("content-type", "text/javascript; charset=utf-8")], include_str!("web/docs.js"), ) } pub(super) async fn style() -> impl IntoResponse { ( [("content-type", "text/css; charset=utf-8")], include_str!("web/docs.css"), ) } fn reader_bundle() -> &'static [u8] { static BUNDLE: OnceLock> = OnceLock::new(); BUNDLE .get_or_init(|| { let documents: Vec<_> = READER_DOCUMENTS .iter() .copied() .map(|(path, category, content)| { let file_name = path.rsplit('/').next().unwrap_or(path); let title = content .lines() .find_map(|line| line.strip_prefix("# ")) .filter(|title| !title.trim().is_empty()) .unwrap_or_else(|| file_name.trim_end_matches(".md")); json!({ "path": path, "title": title, "category": category, "content": content, }) }) .collect(); serde_json::to_vec(&json!({ "documents": documents })) .expect("embedded documentation must serialize") }) .as_slice() } pub(super) async fn content() -> impl IntoResponse { ( [("content-type", "application/json; charset=utf-8")], reader_bundle(), ) } pub(super) async fn schema() -> Json { static DOCUMENT: OnceLock = OnceLock::new(); Json( DOCUMENT .get_or_init(|| { let mut value: Value = serde_json::from_str(include_str!("openapi.json")) .expect("validated embedded OpenAPI"); value["info"]["version"] = Value::String(env!("CARGO_PKG_VERSION").into()); value }) .clone(), ) } pub(super) fn asset(name: &'static str) -> Response { // Bytes are embedded for debug AND release, and always served through the // authenticated router. Never read a file or fetch a CDN on an HTTP request. let (content_type, bytes): (&'static str, &'static [u8]) = match name { "swagger-ui-bundle.js" => ( "text/javascript; charset=utf-8", include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/swagger-ui-bundle.js")), ), "swagger-ui.css" => ( "text/css; charset=utf-8", include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/swagger-ui.css")), ), "licenses.txt" => ( "text/plain; charset=utf-8", include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/licenses.txt")), ), _ => return StatusCode::NOT_FOUND.into_response(), }; ([("content-type", content_type)], bytes).into_response() } #[cfg(test)] include!("documentation/tests.rs");