3.8 KB
raw
use axum::{http::HeaderValue, middleware as axum_middleware, Router};
use minijinja::Environment;
use sqlx::SqlitePool;
use std::path::PathBuf;
use std::sync::Arc;
use tower_cookies::{CookieManagerLayer, Key};
use tower_http::services::ServeDir;
use tower_http::set_header::SetResponseHeaderLayer;
use crate::pdf::PdfRenderer;
use crate::routes;
use crate::{db, middleware, templates};
#[derive(Clone)]
pub struct AppState {
pub env: Arc<Environment<'static>>,
pub pool: SqlitePool,
pub cookie_key: Key,
pub config: Arc<Config>,
pub pdf_renderer: Arc<PdfRenderer>,
}
#[derive(Debug, Clone)]
pub struct Config {
pub root: PathBuf,
pub data_dir: PathBuf,
pub password: String,
pub base_url: String,
pub alert_email: Option<String>,
pub discord_webhook_url: Option<String>,
}
impl AppState {
pub async fn from_env() -> anyhow::Result<Self> {
let root: PathBuf = std::env::var("STATUS_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
let data_dir = std::env::var("STATUS_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| root.join("data"));
std::fs::create_dir_all(&data_dir)?;
let password =
std::env::var("STATUS_PASSWORD").unwrap_or_else(|_| "admin".to_string());
let base_url = std::env::var("BASE_URL").unwrap_or_default();
let alert_email = std::env::var("ALERT_EMAIL").ok().filter(|s| !s.is_empty());
let discord_webhook_url = std::env::var("DISCORD_WEBHOOK_URL")
.ok()
.filter(|s| !s.is_empty());
let cookie_secret = std::env::var("STATUS_COOKIE_SECRET").unwrap_or_else(|_| {
// 64+ bytes derived from password if no secret provided. For a single-user
// self-hosted app this is fine; setting STATUS_COOKIE_SECRET is preferred.
use sha2::{Digest, Sha512};
let mut h = Sha512::new();
h.update(b"status-cookie:");
h.update(password.as_bytes());
let digest = h.finalize();
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, digest)
});
let cookie_key = Key::from(cookie_secret.as_bytes());
let pool = db::init(&data_dir).await?;
let templates_dir = root.join("templates");
let manifest_path = root.join("dist/.vite/manifest.json");
let env = Arc::new(templates::build_env(&templates_dir, &manifest_path));
let pdf_renderer = Arc::new(PdfRenderer::new(root.clone()));
let config = Arc::new(Config {
root,
data_dir,
password,
base_url,
alert_email,
discord_webhook_url,
});
Ok(Self {
env,
pool,
cookie_key,
config,
pdf_renderer,
})
}
}
pub fn router(state: AppState) -> Router {
let dist_dir = state.config.root.join("dist");
let static_cache = SetResponseHeaderLayer::if_not_present(
axum::http::header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000"),
);
Router::new()
.merge(routes::home::router())
.merge(routes::auth::router())
.merge(routes::seo::router())
.merge(routes::properties::router())
// routes::dashboard holds the UUID `/{property_id}` catch-all; merge
// last so named routes win the match.
.merge(routes::dashboard::router())
.nest_service(
"/static",
tower::ServiceBuilder::new()
.layer(static_cache)
.service(ServeDir::new(&dist_dir)),
)
.fallback(middleware::not_found)
.layer(CookieManagerLayer::new())
.layer(axum_middleware::from_fn(middleware::log_requests))
.with_state(state)
}