Skip to main content

acme_proxy/webadmin/handlers/
misc.rs

1//! `/api/nonces` and `/api/profiles` — the two small read surfaces.
2
3use axum::Json;
4use axum::extract::State;
5use serde::Deserialize;
6use serde_json::{Value, json};
7use std::time::Duration;
8
9use crate::admin;
10use crate::sqlite::nonce::Nonce;
11use crate::webadmin::AdminState;
12use crate::webadmin::error::AdminError;
13use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
14
15#[derive(Debug, Deserialize, Default)]
16pub struct CleanupRequest {
17    /// Age past which a nonce is swept. Absent means `nonce.ttl_seconds`.
18    #[serde(rename = "ttlSeconds")]
19    pub ttl_seconds: Option<u64>,
20}
21
22/// `GET /api/nonces` — how many rows the table holds.
23///
24/// A count and nothing else: a nonce is a bearer credential until it is
25/// consumed, so listing values would put live ones on a screen. The count is
26/// the useful part — it should sit near the request rate times the TTL, and a
27/// number far above that says the reaper is not running.
28pub async fn get_nonces(
29    State(state): State<AdminState>,
30    _auth: Authenticated,
31) -> Result<Json<Value>, AdminError> {
32    let count = Nonce::count(&state.database).await?;
33    Ok(Json(json!({
34        "count": count,
35        "ttlSeconds": state.config.nonce.ttl_seconds,
36    })))
37}
38
39/// `POST /api/nonces/cleanup` — sweep now, rather than waiting for the reaper.
40pub async fn cleanup_nonces(
41    State(state): State<AdminState>,
42    AuthenticatedWrite(auth): AuthenticatedWrite,
43    body: Option<Json<CleanupRequest>>,
44) -> Result<Json<Value>, AdminError> {
45    let seconds = body
46        .and_then(|Json(body)| body.ttl_seconds)
47        .unwrap_or(state.config.nonce.ttl_seconds);
48
49    let removed =
50        admin::cleanup_nonces(Duration::from_secs(seconds), state.database.clone()).await?;
51    tracing::info!(event = "admin_nonces_cleaned",
52                   outcome = "success",
53                   surface = "api",
54                   rows_removed = removed,
55                   ttl_seconds = seconds,
56                   username = %auth.user.username);
57    Ok(Json(json!({ "removed": removed })))
58}
59
60/// `GET /api/profiles` — the endpoints this process is serving.
61///
62/// Read straight from the mounted [`crate::Profile`]s rather than from
63/// configuration, so it describes what is actually running: a profile parked
64/// with `enabled = false` is absent here, which is the honest answer.
65pub async fn list_profiles(State(state): State<AdminState>, _auth: Authenticated) -> Json<Value> {
66    Json(Value::Array(profile_rows(&state)))
67}
68
69/// The mounted endpoints, name-sorted.
70///
71/// Shared with the `/ui` pages, which show the same list on the overview, on
72/// its own page, and in the profile filter of every list -- one assembly, so
73/// the two front ends cannot come to describe an endpoint differently.
74pub(crate) fn profile_rows(state: &AdminState) -> Vec<Value> {
75    let mut names: Vec<&String> = state.profiles.keys().collect();
76    names.sort();
77
78    names
79        .into_iter()
80        .filter_map(|name| state.profiles.get(name))
81        .map(|profile| {
82            json!({
83                "name": profile.name,
84                "baseUrl": profile.base_url,
85                "directory": profile.directory_url(),
86                "challengeBypass": profile.challenges.is_bypassed(),
87                "eabEnabled": profile.eab.enabled,
88            })
89        })
90        .collect()
91}