Skip to main content

cortex/frontend/
server.rs

1// Copyright 2015-2025 Deyan Ginev. See the LICENSE
2// file at the top-level directory of this distribution.
3//
4// Licensed under the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>.
5// This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! The library-resident Rocket composition root.
9//!
10//! `server` assembles the per-capability route groups (`management`, `corpora`, `jobs`, …), the
11//! shared fairings, and the managed state (config-file path, database URL, and connection pool)
12//! into a testable app that the binary and the integration tests both build. Route handlers live in
13//! their capability modules; this file only wires them together. As later arms land, their routes
14//! are mounted here too (the binary's legacy routes migrate in incrementally).
15
16use std::collections::HashMap;
17use std::path::PathBuf;
18
19use diesel::Connection;
20use diesel::pg::PgConnection;
21use rocket::{Build, Rocket};
22use rocket_dyn_templates::{Template, tera};
23
24use crate::backend::{DatabaseUrl, build_pool};
25use crate::config::{config, config_file_path};
26use crate::frontend::corpora;
27use crate::frontend::jobs;
28use crate::frontend::management::{self, ConfigFile};
29use crate::frontend::reports;
30use crate::frontend::runs;
31use crate::frontend::services;
32
33/// Tera `group_thousands` filter: comma-groups an integer for readable report screens (e.g.
34/// `2783148` → `2,783,148`). The report globals are stringly-typed (`HashMap<String,String>`), so
35/// it accepts both a JSON number and a numeric string, and passes anything non-numeric through
36/// unchanged — a template filter must never error a render.
37fn group_thousands_filter(
38  value: &tera::Value,
39  _args: &HashMap<String, tera::Value>,
40) -> tera::Result<tera::Value> {
41  let parsed = value
42    .as_i64()
43    .or_else(|| value.as_str().and_then(|s| s.parse::<i64>().ok()));
44  Ok(match parsed {
45    Some(n) => tera::Value::String(crate::frontend::helpers::group_thousands(n)),
46    None => value.clone(),
47  })
48}
49
50/// Mounts the full library API/UI surface from the runtime configuration. The composition root the
51/// binary uses.
52pub fn mount_api(rocket: Rocket<Build>) -> Rocket<Build> {
53  let database_url = config().database.url.clone();
54  // Best-effort: mark jobs left 'running' by a previous process as interrupted (prod startup only;
55  // tests build via mount_api_with, so their in-flight jobs are never touched).
56  if let Ok(mut connection) = PgConnection::establish(&database_url) {
57    crate::jobs::interrupt_orphans(&mut connection);
58  }
59  mount_api_with(rocket, config_file_path(), &database_url)
60}
61
62/// Like [`mount_api`], but with an explicit config-file path and database URL (tests target the
63/// test database and a temporary config file). Builds the connection pool and manages it alongside
64/// the URL, so background jobs open their own connection against the same database.
65pub fn mount_api_with(
66  rocket: Rocket<Build>,
67  config_file: PathBuf,
68  database_url: &str,
69) -> Rocket<Build> {
70  let pool = build_pool(database_url, config().database.pool_size);
71  let rocket = rocket
72    .manage(ConfigFile(config_file))
73    .manage(DatabaseUrl(database_url.to_string()))
74    .manage(pool)
75    // Passkey (WebAuthn) sign-in: the relying-party instance (`None` when disabled) + the in-memory
76    // ceremony store. See `frontend::webauthn`.
77    .manage(crate::frontend::webauthn::build_state(&config().webauthn))
78    .manage(crate::frontend::webauthn::CeremonyStore::new())
79    // Bounds concurrent expensive live (`?all=true`) report aggregations so a burst can't exhaust
80    // the connection pool and 503 other requests (KNOWN_ISSUES P-2).
81    .manage(crate::frontend::concerns::LiveReportLimiter::default())
82    .mount("/", management::routes())
83    .mount("/", corpora::routes())
84    .mount("/", reports::routes())
85    .mount("/", runs::routes())
86    .mount("/", jobs::routes())
87    .mount("/", services::routes())
88    .mount("/", crate::frontend::concerns::routes())
89    .mount("/", crate::frontend::admin::routes())
90    .mount("/", crate::frontend::audit::routes())
91    .mount("/", crate::frontend::sessions::routes())
92    .mount("/", crate::frontend::retention::routes())
93    .mount("/", crate::frontend::metrics::routes())
94    .mount("/", crate::frontend::webauthn::routes())
95    .register("/", crate::frontend::catchers::catchers())
96    .attach(Template::custom(|engines| {
97      engines
98        .tera
99        .register_filter("group_thousands", group_thousands_filter);
100    }))
101    // Accounting (AAA): record every mutating admin request to the `audit_log` (drift-proof —
102    // covers every write route, present and future). See `frontend::audit`.
103    .attach(crate::frontend::audit::AuditFairing);
104  // Mount the generated OpenAPI spec (`/api/openapi.json`), the `#[openapi]`-documented agent
105  // routes, and the RapiDoc browser page (`/api/docs`) — built by rocket_okapi from the routes
106  // themselves.
107  let rocket = crate::frontend::apidoc::mount(rocket);
108  // Snapshot the now-complete route table (legacy binary routes + all library routes) so the `/api`
109  // discovery index introspects the real surface and can never drift.
110  let route_table = management::RouteTable::snapshot(&rocket);
111  rocket.manage(route_table)
112}