//! HTTP API: router, handlers, and OpenAPI document. mod admin; mod admin_authorities; mod admin_objects; mod admin_vocab; mod health; mod openapi; mod pagination; mod public; use axum::Router; use db::Db; use time::Duration; use tower_sessions::cookie::SameSite; use tower_sessions::{Expiry, SessionManagerLayer}; use tower_sessions_sqlx_store::PostgresStore; /// Shared application state passed to handlers. #[derive(Clone)] pub struct AppState { /// Database handle for this organization. pub db: Db, /// User-facing product name (from config). Never hardcoded. pub app_name: String, /// Whether the session cookie carries the `Secure` attribute (default true; /// disable only for plain-HTTP self-hosting). pub cookie_secure: bool, /// Search client for on-write index sync. `None` disables indexing (search is a /// best-effort feature; absent when Meilisearch is not configured). pub search: Option, } /// Best-effort: keep the search index in step with a catalogue write that has already /// committed. Re-projects and indexes the object, or removes it if it no longer exists. /// Never fails the request — a search outage must not undo a committed write, and /// `reindex_all` is the recovery path. A no-op when search is not configured. pub(crate) async fn reindex(state: &AppState, id: domain::ObjectId) { let Some(search) = &state.search else { return; }; if let Err(err) = search.sync_object(&state.db, id).await { tracing::error!(?err, object_id = %id, "search reindex after write failed"); } } /// Build the application router from shared state. pub fn build_app(state: AppState) -> Router { let store = PostgresStore::new(state.db.pool().clone()); let session_layer = SessionManagerLayer::new(store) .with_name("id") .with_http_only(true) .with_secure(state.cookie_secure) .with_same_site(SameSite::Strict) .with_expiry(Expiry::OnInactivity(Duration::hours(8))); Router::new() .merge(health::routes()) .merge(openapi::routes()) .merge(public::routes()) .merge(admin::routes()) .merge(admin_objects::routes()) .merge(admin_vocab::routes()) .merge(admin_authorities::routes()) .layer(session_layer) .with_state(state) } /// Create the session store's table if absent. Run once at startup (and in tests /// before exercising auth). Separate from `Db::migrate` — the session library's own /// bookkeeping table. pub async fn migrate_sessions(db: &Db) -> Result<(), sqlx::Error> { PostgresStore::new(db.pool().clone()).migrate().await }