calathea/src/server/mod.rs

113 lines
3.0 KiB
Rust

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<T> = std::result::Result<T, error::AppError>;
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<String> {
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<Path>) -> 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<Html<String>> {
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<Html<String>> {
let context = Context::new();
Ok(Html(render_view(
&ctx.tera,
"views/login.html",
&context,
&headers,
)?))
}
async fn get_index(State(ctx): State<crate::Context>, headers: HeaderMap) -> Result<Html<String>> {
if auth::logged_in_user(&ctx.pool, &headers)?.is_some() {
render_home(ctx, &headers).await
} else {
render_login(ctx, &headers)
}
}