141 lines
4.1 KiB
Rust
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),
|
|
))
|
|
}
|