magnetar/fe_calckey/src/frontend_render.rs

141 lines
4.1 KiB
Rust

use axum::extract::State;
use axum::headers::CacheControl;
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse};
use axum::routing::get;
use axum::{Router, TypedHeader};
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<Tera>,
}
#[derive(Debug, Clone, Serialize)]
struct ClientEntryPoint {
file: String,
css: Vec<String>,
}
pub async fn render_frontend(
State(FrontendConfig {
magnetar_config,
templater,
}): State<FrontendConfig>,
) -> Result<impl IntoResponse, StatusCode> {
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::<Vec<String>>()
}),
},
);
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),
))
}