acme_proxy/webadmin/pages/
misc.rs1use axum::extract::State;
5use axum::response::Html;
6use serde::Deserialize;
7use serde_json::{Map, Value};
8use std::time::Duration;
9
10use crate::admin;
11use crate::sqlite::account::Account;
12use crate::sqlite::eab::Eab;
13use crate::sqlite::nonce::Nonce;
14use crate::sqlite::order::{Order, OrderQuery};
15use crate::webadmin::AdminState;
16use crate::webadmin::handlers::misc::profile_rows;
17use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
18use crate::webadmin::pages::error::PageError;
19use crate::webadmin::pages::{chrome, flash, respond, respond_fragment};
20
21#[derive(Debug, Deserialize, Default)]
22pub struct CleanupForm {
23 #[serde(default, rename = "ttlSeconds")]
25 pub ttl_seconds: String,
26}
27
28pub async fn get_index(
34 State(state): State<AdminState>,
35 session: PageSession,
36) -> Result<Html<String>, PageError> {
37 let (_, accounts) = Account::search(None, 1, 0, &state.database).await?;
38 let (_, orders) = Order::search(
39 &OrderQuery {
40 limit: 1,
41 ..OrderQuery::default()
42 },
43 &state.database,
44 )
45 .await?;
46 let (_, eab) = Eab::search(1, 0, &state.database).await?;
47 let nonces = Nonce::count(&state.database).await?;
48
49 let mut context = chrome(&session, "index", "Overview");
50 context.insert(
51 "stats".to_string(),
52 serde_json::json!({
53 "accounts": accounts,
54 "orders": orders,
55 "eab": eab,
56 "nonces": nonces,
57 }),
58 );
59 context.insert("profiles".to_string(), Value::Array(profile_rows(&state)));
60
61 respond(&state, false, "index.html", "index.html", context)
64}
65
66pub async fn list_profiles(
68 State(state): State<AdminState>,
69 session: PageSession,
70) -> Result<Html<String>, PageError> {
71 let profiles = profile_rows(&state);
72 let any_bypass = profiles
75 .iter()
76 .any(|profile| profile["challengeBypass"] == Value::Bool(true));
77
78 let mut context = chrome(&session, "profiles", "Profiles");
79 context.insert("profiles".to_string(), Value::Array(profiles));
80 context.insert("any_bypass".to_string(), Value::Bool(any_bypass));
81
82 respond(
83 &state,
84 false,
85 "profiles/list.html",
86 "profiles/_table.html",
87 context,
88 )
89}
90
91pub async fn get_nonces(
93 State(state): State<AdminState>,
94 session: PageSession,
95) -> Result<Html<String>, PageError> {
96 let count = Nonce::count(&state.database).await?;
97
98 let mut context = chrome(&session, "nonces", "Nonces");
99 context.insert("count".to_string(), Value::from(count));
100 context.insert(
101 "ttl_seconds".to_string(),
102 Value::from(state.config.nonce.ttl_seconds),
103 );
104
105 respond(
106 &state,
107 session.hx,
108 "nonces/index.html",
109 "nonces/_panel.html",
110 context,
111 )
112}
113
114pub async fn cleanup_nonces(
116 State(state): State<AdminState>,
117 session: PageSessionWrite,
118 axum::Form(form): axum::Form<CleanupForm>,
119) -> Result<Html<String>, PageError> {
120 let seconds = match form.ttl_seconds.trim() {
121 "" => state.config.nonce.ttl_seconds,
122 raw => raw.parse::<u64>().map_err(|_| {
123 PageError::from(crate::webadmin::error::AdminError::bad_request(format!(
124 "`{raw}` is not a number of seconds"
125 )))
126 })?,
127 };
128
129 let removed =
130 admin::cleanup_nonces(Duration::from_secs(seconds), state.database.clone()).await?;
131 tracing::info!(event = "admin_nonces_cleaned",
132 outcome = "success",
133 surface = "ui",
134 rows_removed = removed,
135 ttl_seconds = seconds,
136 username = %session.auth.user.username);
137
138 let count = Nonce::count(&state.database).await?;
139 let mut context = Map::new();
140 context.insert(
141 "csrf_token".to_string(),
142 Value::String(session.auth.session.csrf_token.clone()),
143 );
144 context.insert("count".to_string(), Value::from(count));
145 context.insert(
146 "ttl_seconds".to_string(),
147 Value::from(state.config.nonce.ttl_seconds),
148 );
149 context.insert(
150 "flash".to_string(),
151 flash("ok", format!("Swept {removed} nonce(s).")),
152 );
153
154 respond_fragment(&state, "nonces/_panel.html", context)
155}