mod auth; mod error; mod events; mod images; mod plants; pub mod query; use std::path::Path; use axum::{ extract::State, http::{header::VARY, HeaderMap, HeaderValue}, middleware, response::Html, routing::get, Router, }; use tera::Context; use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer}; use crate::db::Plant; pub type Result = std::result::Result; const HX_REQUEST_HEADER: &str = "HX-Request"; const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request"; pub fn should_render_full(headers: &HeaderMap) -> bool { let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some(); let is_hist_restore_req = headers .get(HX_HISTORY_RESTORE_HEADER) .map(|val| val == HeaderValue::from_static("true")) .unwrap_or(false); !is_htmx_req || is_hist_restore_req } pub fn render_view( tera: &tera::Tera, view: &str, ctx: &tera::Context, headers: &HeaderMap, ) -> tera::Result { let view = tera.render(view, ctx)?; if should_render_full(headers) { let mut ctx = tera::Context::new(); ctx.insert("view", &view); tera.render("base.html", &ctx) } else { Ok(view) } } pub fn app(ctx: crate::Context, static_dir: impl AsRef) -> axum::Router { let router = Router::new() .nest("/plants", plants::app()) .nest("/events", events::app()) .nest("/images", images::app()) .layer(middleware::from_fn_with_state( ctx.clone(), auth::auth_middleware, )) .merge(auth::app()) .route("/", get(get_index)) .nest_service("/static", ServeDir::new(static_dir)) .with_state(ctx.clone()); // Routes return either partial or full pages depending on whether the request is done using // HTMX or just as a plain HTTP request. Adding the Vary header ensures caches don't mix // partial and full responses. // https://htmx.org/docs/#caching router.layer(SetResponseHeaderLayer::appending( VARY, HeaderValue::from_static(HX_REQUEST_HEADER), )) } pub async fn render_home(ctx: crate::Context, headers: &HeaderMap) -> Result> { let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool)) .await .unwrap()?; let mut context = Context::new(); context.insert("plants", &plants); Ok(Html(render_view( &ctx.tera, "views/index.html", &context, headers, )?)) } pub fn render_login(ctx: crate::Context, headers: &HeaderMap) -> Result> { let context = Context::new(); Ok(Html(render_view( &ctx.tera, "views/login.html", &context, &headers, )?)) } async fn get_index(State(ctx): State, headers: HeaderMap) -> Result> { if auth::logged_in_user(&ctx.pool, &headers)?.is_some() { render_home(ctx, &headers).await } else { render_login(ctx, &headers) } }