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/// The shape is [`admin::render_nonce_stats_json`], which `nonce count --json`
25/// also answers with: a count is not worth two spellings.
26pub async fn get_nonces(
27    State(state): State<AdminState>,
28    _auth: Authenticated,
29) -> Result<Json<Value>, AdminError> {
30    let count = Nonce::count(&state.database).await?;
31    Ok(Json(admin::render_nonce_stats_json(
32        count,
33        state.config.nonce.ttl_seconds,
34    )))
35}
36
37/// `POST /api/nonces/cleanup` — sweep now, rather than waiting for the reaper.
38pub async fn cleanup_nonces(
39    State(state): State<AdminState>,
40    AuthenticatedWrite(auth): AuthenticatedWrite,
41    body: Option<Json<CleanupRequest>>,
42) -> Result<Json<Value>, AdminError> {
43    let seconds = body
44        .and_then(|Json(body)| body.ttl_seconds)
45        .unwrap_or(state.config.nonce.ttl_seconds);
46
47    let removed =
48        admin::cleanup_nonces(Duration::from_secs(seconds), state.database.clone()).await?;
49    tracing::info!(event = "admin_nonces_cleaned",
50                   outcome = "success",
51                   surface = "api",
52                   rows_removed = removed,
53                   ttl_seconds = seconds,
54                   username = %auth.user.username);
55    Ok(Json(json!({ "removed": removed })))
56}
57
58/// `GET /api/profiles` — the endpoints this process is serving.
59///
60/// Read straight from the mounted [`crate::Profile`]s rather than from
61/// configuration, so it describes what is actually running: a profile parked
62/// with `enabled = false` is absent here, which is the honest answer.
63pub async fn list_profiles(State(state): State<AdminState>, _auth: Authenticated) -> Json<Value> {
64    Json(Value::Array(profile_rows(&state)))
65}
66
67/// The mounted endpoints, name-sorted.
68///
69/// Shared with the `/ui` pages, which show the same list on the overview, on
70/// its own page, and in the profile filter of every list -- one assembly, so
71/// the two front ends cannot come to describe an endpoint differently.
72pub(crate) fn profile_rows(state: &AdminState) -> Vec<Value> {
73    let mut names: Vec<&String> = state.profiles.keys().collect();
74    names.sort();
75
76    names
77        .into_iter()
78        .filter_map(|name| state.profiles.get(name))
79        .map(|profile| profile_row(profile))
80        .collect()
81}
82
83/// One endpoint, as every surface describes it.
84///
85/// Split out of [`profile_rows`] for the filter-policy page, which shows one
86/// endpoint rather than the list and still has to say the same things about it
87/// -- `challengeBypass` above all, since that page is where the warning
88/// matters most.
89///
90/// The document itself is [`admin::render_profile_json`], which
91/// `acme-proxy profile list` renders too. The two reach it from opposite
92/// directions -- a mounted profile here, a resolved configuration there -- and
93/// [`admin::ProfileSummary`] is where that difference is written down.
94pub(crate) fn profile_row(profile: &crate::Profile) -> Value {
95    admin::render_profile_json(&admin::ProfileSummary::mounted(profile))
96}