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}