acme_proxy_admin/webadmin/pages/
misc.rs1use axum::extract::State;
5use axum::response::Html;
6use serde::Deserialize;
7use serde_json::Value;
8
9use crate::webadmin::AdminState;
10use crate::webadmin::handlers::Caller;
11use crate::webadmin::handlers::misc::{apply_cleanup_nonces, profile_rows};
12use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
13use crate::webadmin::pages::error::PageError;
14use crate::webadmin::pages::{chrome, flash, respond, respond_fragment};
15use acme_proxy_store::account::Account;
16use acme_proxy_store::audit::AuditEntry;
17use acme_proxy_store::audit::AuditQuery;
18use acme_proxy_store::eab::Eab;
19use acme_proxy_store::job::Job;
20use acme_proxy_store::job::JobQuery;
21use acme_proxy_store::nonce::Nonce;
22use acme_proxy_store::order::Order;
23use acme_proxy_store::order::OrderQuery;
24use acme_proxy_store::status::JobStatus;
25
26#[derive(Debug, Deserialize, Default)]
28pub struct CleanupForm {
29 #[serde(default, rename = "ttlSeconds")]
31 pub ttl_seconds: String,
32}
33
34const ATTENTION_EXPIRY_DAYS: u64 = 7;
37
38pub async fn get_index(
45 State(state): State<AdminState>,
46 session: PageSession,
47) -> Result<Html<String>, PageError> {
48 let (_, accounts) = Account::search(None, None, 1, 0, &state.database).await?;
49 let (_, orders) = Order::search(
50 &OrderQuery {
51 limit: 1,
52 ..OrderQuery::default()
53 },
54 &state.database,
55 )
56 .await?;
57 let (_, eab) = Eab::search(1, 0, &state.database).await?;
58 let nonces = Nonce::count(&state.database).await?;
59
60 let (_, failed_jobs) = Job::search(
64 &JobQuery {
65 kind: None,
66 status: Some(JobStatus::Failed),
67 limit: 1,
68 offset: 0,
69 },
70 &state.database,
71 )
72 .await?;
73 let (_, expiring_soon, _) = acme_proxy_store::expiring::list_expiring(
76 &acme_proxy_store::expiring::ExpiringQuery {
77 profile: None,
78 before: acme_proxy_store::expiring::expiring_horizon(ATTENTION_EXPIRY_DAYS),
79 include_superseded: true,
80 limit: 1,
81 offset: 0,
82 },
83 state.database.clone(),
84 )
85 .await?;
86 let (_, refusals) = AuditEntry::search(
87 &AuditQuery {
88 outcome: Some("failure".to_string()),
89 since: Some(acme_proxy_store::nonce::now_secs().saturating_sub(24 * 60 * 60)),
90 limit: 1,
91 ..AuditQuery::default()
92 },
93 &state.database,
94 )
95 .await?;
96 let profiles = profile_rows(&state);
97 let any_bypass = profiles
98 .iter()
99 .any(|profile| profile["challengeBypass"] == Value::Bool(true));
100
101 let mut context = chrome(&session, "index", "Overview");
102 context.insert(
103 "stats".to_string(),
104 serde_json::json!({
105 "accounts": accounts,
106 "orders": orders,
107 "eab": eab,
108 "nonces": nonces,
109 "failedJobs": failed_jobs,
110 "expiringSoon": expiring_soon,
111 "expiryDays": ATTENTION_EXPIRY_DAYS,
112 "refusals": refusals,
113 }),
114 );
115 context.insert("any_bypass".to_string(), Value::Bool(any_bypass));
116 context.insert("profiles".to_string(), Value::Array(profiles));
117
118 respond(&state, false, "index.html", "index.html", context)
121}
122
123pub async fn list_profiles(
125 State(state): State<AdminState>,
126 session: PageSession,
127) -> Result<Html<String>, PageError> {
128 let profiles = profile_rows(&state);
129 let any_bypass = profiles
132 .iter()
133 .any(|profile| profile["challengeBypass"] == Value::Bool(true));
134
135 let mut context = chrome(&session, "profiles", "Profiles");
136 context.insert("profiles".to_string(), Value::Array(profiles));
137 context.insert("any_bypass".to_string(), Value::Bool(any_bypass));
138
139 respond(
140 &state,
141 false,
142 "profiles/list.html",
143 "profiles/_table.html",
144 context,
145 )
146}
147
148pub async fn get_nonces(
150 State(state): State<AdminState>,
151 session: PageSession,
152) -> Result<Html<String>, PageError> {
153 let count = Nonce::count(&state.database).await?;
154
155 let mut context = chrome(&session, "nonces", "Nonces");
156 context.insert("count".to_string(), Value::from(count));
157 context.insert(
158 "ttl_seconds".to_string(),
159 Value::from(state.config.nonce.ttl_seconds),
160 );
161
162 respond(
163 &state,
164 session.hx,
165 "nonces/index.html",
166 "nonces/_panel.html",
167 context,
168 )
169}
170
171pub async fn cleanup_nonces(
173 State(state): State<AdminState>,
174 session: PageSessionWrite,
175 request_context: acme_proxy_core::audit::RequestContext,
176 axum::Form(form): axum::Form<CleanupForm>,
177) -> Result<Html<String>, PageError> {
178 let seconds = match form.ttl_seconds.trim() {
179 "" => state.config.nonce.ttl_seconds,
180 raw => raw.parse::<u64>().map_err(|_| {
181 PageError::from(crate::webadmin::error::AdminError::bad_request(format!(
182 "`{raw}` is not a number of seconds"
183 )))
184 })?,
185 };
186
187 let removed = apply_cleanup_nonces(
188 &state,
189 &Caller::ui(&session.auth, &request_context),
190 seconds,
191 )
192 .await?;
193
194 let count = Nonce::count(&state.database).await?;
195 let mut context = super::fragment_context(&session.auth);
196 context.insert("count".to_string(), Value::from(count));
197 context.insert(
198 "ttl_seconds".to_string(),
199 Value::from(state.config.nonce.ttl_seconds),
200 );
201 context.insert(
202 "flash".to_string(),
203 flash("ok", format!("Swept {removed} nonce(s).")),
204 );
205
206 respond_fragment(&state, "nonces/_panel.html", context)
207}