acme_proxy/webadmin/pages/
orders.rs1use axum::extract::{Path, Query, State};
5use axum::response::{Html, IntoResponse, Response};
6use serde::Deserialize;
7use serde_json::{Map, Value};
8
9use crate::admin;
10use crate::admin::ops::RevokeOutcome;
11use crate::sqlite::order::{Order, OrderQuery};
12use crate::webadmin::AdminState;
13use crate::webadmin::error::AdminError;
14use crate::webadmin::handlers::orders::{OrderListParams, render_orders, revoke_error};
15use crate::webadmin::handlers::paging::PageParams;
16use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
17use crate::webadmin::pages::error::{PageError, redirect};
18use crate::webadmin::pages::{chrome, flash, flash_error, pager, respond, respond_fragment};
19
20#[derive(Debug, Deserialize, Default)]
22pub struct RevokeForm {
23 #[serde(default)]
26 pub reason: String,
27}
28
29pub async fn list_orders(
31 State(state): State<AdminState>,
32 Query(params): Query<OrderListParams>,
33 session: PageSession,
34) -> Result<Html<String>, PageError> {
35 let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
36 let profile = params.profile.clone().unwrap_or_default();
37 let account_id = params.account_id.clone().unwrap_or_default();
38 let status = params.status.clone().unwrap_or_default();
39 let parsed = params
41 .parsed_status()
42 .map_err(|error| PageError::bad_request(error.to_string()))?;
43
44 let (orders, total) = Order::search(
45 &OrderQuery {
46 profile: params.profile.clone(),
47 account_id: params.account_id.clone(),
48 status: parsed,
49 limit: page.limit,
50 offset: page.offset,
51 },
52 &state.database,
53 )
54 .await?;
55 let items = render_orders(&orders, &state).await?;
56
57 let mut context = chrome(&session, "orders", "Orders");
58 context.insert(
59 "page".to_string(),
60 serde_json::json!({ "items": items, "total": total }),
61 );
62 context.insert(
63 "pager".to_string(),
64 pager(
65 page,
66 total,
67 "/ui/orders",
68 &[
69 ("profile", &profile),
70 ("status", &status),
71 ("accountId", &account_id),
72 ],
73 "#orders-table",
74 ),
75 );
76 context.insert(
77 "filters".to_string(),
78 serde_json::json!({
79 "profile": profile,
80 "status": status,
81 "accountId": account_id,
82 }),
83 );
84 context.insert(
85 "profiles".to_string(),
86 Value::Array(crate::webadmin::handlers::misc::profile_rows(&state)),
87 );
88
89 respond(
90 &state,
91 session.hx,
92 "orders/list.html",
93 "orders/_table.html",
94 context,
95 )
96}
97
98pub async fn get_order(
100 State(state): State<AdminState>,
101 Path(id): Path<String>,
102 session: PageSession,
103) -> Result<Html<String>, PageError> {
104 let detail = load(&id, &state).await?;
105
106 let mut context = chrome(&session, "orders", "Order");
107 context.insert("detail".to_string(), detail);
108
109 respond(
110 &state,
111 session.hx,
112 "orders/detail.html",
113 "orders/_card.html",
114 context,
115 )
116}
117
118pub async fn download_chain(
128 State(state): State<AdminState>,
129 Path(id): Path<String>,
130 _session: PageSession,
131) -> Result<Response, PageError> {
132 let order = Order::find_by_id(&id, &state.database)
133 .await?
134 .ok_or_else(|| not_found(&id))?;
135
136 let filename = format!("{}.pem", order.id);
140 let pem = order.certificate.ok_or_else(|| {
141 PageError::not_found(format!("order {id} has no certificate to download"))
142 })?;
143
144 Ok((
145 [
146 (
147 axum::http::header::CONTENT_TYPE,
148 "application/pem-certificate-chain".to_string(),
149 ),
150 (
151 axum::http::header::CONTENT_DISPOSITION,
157 format!("attachment; filename=\"{filename}\""),
158 ),
159 ],
160 pem,
161 )
162 .into_response())
163}
164
165pub async fn revoke_order(
179 State(state): State<AdminState>,
180 Path(id): Path<String>,
181 request_context: crate::audit::RequestContext,
182 session: PageSessionWrite,
183 axum::Form(form): axum::Form<RevokeForm>,
187) -> Result<Html<String>, PageError> {
188 let reason = match form.reason.trim() {
189 "" => None,
190 raw => Some(raw.parse::<u32>().map_err(|_| {
191 PageError::from(AdminError::bad_request(format!(
192 "revocation reason `{raw}` is not a number"
193 )))
194 })?),
195 };
196
197 let banner = match crate::webadmin::handlers::resolve_order_signer(&state, &id).await {
198 Err(error) => flash_error(error.code, error.message),
199 Ok(signer) => {
200 match admin::revoke_order(
202 &id,
203 reason,
204 crate::audit::Actor::admin(&session.auth.user.username),
205 state.audit.client(&request_context).await,
206 state.database.clone(),
207 signer,
208 )
209 .await
210 {
211 Ok(RevokeOutcome::Revoked(order)) => {
212 tracing::info!(event = "admin_order_revoked",
213 outcome = "success",
214 surface = "ui",
215 order_id = %id,
216 profile = %order.profile,
217 reason = ?reason,
218 username = %session.auth.user.username);
219 flash("ok", "Certificate revoked. The CRL has been regenerated.")
220 }
221 Ok(RevokeOutcome::NotFound) => return Err(not_found(&id)),
222 Ok(RevokeOutcome::NotIssued) => flash_error(
223 "order_not_issued",
224 format!("Order {id} has no certificate to revoke."),
225 ),
226 Ok(RevokeOutcome::AlreadyRevoked) => flash_error(
227 "already_revoked",
228 format!("Order {id} was already revoked."),
229 ),
230 Err(error) => {
231 let error = revoke_error(error);
232 if error.status.is_server_error() {
233 return Err(error.into());
234 }
235 flash_error(error.code, error.message)
236 }
237 }
238 }
239 };
240
241 let detail = load(&id, &state).await?;
245 let mut context = Map::new();
246 context.insert(
247 "csrf_token".to_string(),
248 Value::String(session.auth.session.csrf_token.clone()),
249 );
250 context.insert("detail".to_string(), detail);
251 context.insert("flash".to_string(), banner);
252 respond_fragment(&state, "orders/_card.html", context)
253}
254
255pub async fn delete_order(
257 State(state): State<AdminState>,
258 Path(id): Path<String>,
259 session: PageSessionWrite,
260) -> Result<Response, PageError> {
261 let deleted = admin::delete_order(&id, state.database.clone())
262 .await?
263 .ok_or_else(|| not_found(&id))?;
264
265 tracing::info!(event = "admin_order_deleted",
266 outcome = "success",
267 surface = "ui",
268 order_id = %id,
269 username = %session.auth.user.username,
270 cascaded_authorizations = deleted.cascaded);
271
272 Ok(redirect("/ui/orders", session.hx))
273}
274
275async fn load(id: &str, state: &AdminState) -> Result<Value, PageError> {
276 let detail = admin::load_order_detail(id, state.database.clone())
277 .await?
278 .ok_or_else(|| not_found(id))?;
279 Ok(admin::render_order_detail_json(
280 &detail,
281 &state.config.server.base_url,
282 ))
283}
284
285fn not_found(id: &str) -> PageError {
286 PageError::not_found(format!("no such order: {id}"))
287}