113 lines
3.0 KiB
Rust
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)
|
|
}
|
|
}
|