use axum::extract::State; use axum::http::StatusCode; use axum::response::{Html, IntoResponse}; use axum::routing::get; use axum::Router; use axum_extra::TypedHeader; use headers::CacheControl; use magnetar_common::config::MagnetarConfig; use serde::Serialize; use serde_json::Value; use std::sync::Arc; use std::time::Duration; use tera::{Context, Tera}; use tracing::error; pub fn new_frontend_render_router(frontend_renderer_config: FrontendConfig) -> Router { Router::new() .route( "/*path", get(render_frontend).with_state(frontend_renderer_config.clone()), ) .route( "/", get(render_frontend).with_state(frontend_renderer_config), ) } #[derive(Debug, Clone)] pub struct FrontendConfig { pub magnetar_config: &'static MagnetarConfig, pub templater: Arc, } #[derive(Debug, Clone, Serialize)] struct ClientEntryPoint { file: String, css: Vec, } pub async fn render_frontend( State(FrontendConfig { magnetar_config, templater, }): State, ) -> Result { let mut context = Context::new(); // TODO: Better title context.insert("title", &magnetar_config.branding.name); context.insert("app_name", &magnetar_config.branding.name); context.insert("version", &magnetar_config.branding.version); context.insert( "boot_js", &std::fs::read_to_string("fe_calckey/frontend/assets-be/template/boot.js").map_err( |e| { error!("Failed to read boot JS: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR }, )?, ); context.insert( "style_css", &std::fs::read_to_string("fe_calckey/frontend/assets-be/template/style.css").map_err( |e| { error!("Failed to read boot CSS: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR }, )?, ); context.insert( "base_url", &format!("https://{}", magnetar_config.networking.host), ); context.insert("instance_host", &magnetar_config.networking.host); // TODO: Actual name context.insert("instance_name", &magnetar_config.networking.host); // TODO: Add to config or pull from the backend context.insert("robots", &true); let manifest = std::fs::read_to_string("fe_calckey/frontend/built/_client_dist_/manifest.json") .map_err(|e| { error!("Failed to read manifest: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let manifest_json: Value = serde_json::from_str(&manifest).map_err(|e| { error!("Failed to parse manifest: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let entry_point = manifest_json.get("src/init.ts").ok_or_else(|| { error!("Missing entry point in manifest"); StatusCode::INTERNAL_SERVER_ERROR })?; context.insert( "client_entry", &ClientEntryPoint { file: entry_point .get("file") .and_then(Value::as_str) .ok_or_else(|| { error!("Missing entry point file in manifest"); StatusCode::INTERNAL_SERVER_ERROR })? .to_owned(), css: entry_point .get("css") .and_then(Value::as_array) .map_or_else(Vec::new, |css| { css.iter() .filter_map(Value::as_str) .map(|s| s.to_owned()) .collect::>() }), }, ); context.insert( "timestamp", &format!("v={}", &chrono::Utc::now().timestamp()), ); let html = match templater.render("base.html", &context) { Ok(html) => html, Err(e) => { error!("Failed to render template: {:?}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } }; Ok(( TypedHeader( CacheControl::new() .with_public() .with_max_age(Duration::from_secs(3)), ), Html(html), )) }