Files
biggus-dickus/crates/api/src/lib.rs
T

85 lines
2.9 KiB
Rust

//! HTTP API: router, handlers, and OpenAPI document.
mod admin;
mod admin_authorities;
mod admin_objects;
mod admin_search;
mod admin_vocab;
mod config;
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<search::SearchClient>,
/// Instance default UI/content language (from config).
pub default_language: String,
/// Instance default display timezone, IANA name (from config). Storage stays UTC.
pub default_timezone: String,
}
/// 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(config::routes())
.merge(health::routes())
.merge(openapi::routes())
.merge(public::routes())
.merge(admin::routes())
.merge(admin_objects::routes())
.merge(admin_vocab::routes())
.merge(admin_search::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
}