a87501b902
Expose full-text search over catalogue objects via a new admin endpoint backed by the Meilisearch SearchClient. Validates visibility filter values, short-circuits on empty queries, clamps pagination, and returns 503 when search is not configured. Registered in OpenAPI; schema.d.ts regenerated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
2.7 KiB
Rust
79 lines
2.7 KiB
Rust
//! HTTP API: router, handlers, and OpenAPI document.
|
|
|
|
mod admin;
|
|
mod admin_authorities;
|
|
mod admin_objects;
|
|
mod admin_search;
|
|
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<search::SearchClient>,
|
|
}
|
|
|
|
/// 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_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
|
|
}
|