acme_proxy/webadmin/handlers/
orders.rs1use axum::Json;
4use axum::extract::{Path, Query, State};
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::Deserialize;
8use serde_json::{Value, json};
9
10use crate::admin;
11use crate::admin::ops::{RevokeError, RevokeOutcome};
12use crate::sqlite::authz::Authorization;
13use crate::sqlite::order::{Order, OrderQuery};
14use crate::sqlite::status::{OrderStatus, UnknownStatus};
15use crate::webadmin::AdminState;
16use crate::webadmin::error::AdminError;
17use crate::webadmin::handlers::paging::{PageParams, page_envelope};
18use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
19
20#[derive(Debug, Deserialize, Default)]
23pub struct OrderListParams {
24 pub profile: Option<String>,
25 #[serde(rename = "accountId")]
26 pub account_id: Option<String>,
27 pub status: Option<String>,
28 pub limit: Option<i64>,
29 pub offset: Option<i64>,
30}
31
32impl OrderListParams {
33 pub fn parsed_status(&self) -> Result<Option<OrderStatus>, UnknownStatus> {
40 self.status.as_deref().map(str::parse).transpose()
41 }
42}
43
44#[derive(Debug, Deserialize, Default)]
45pub struct RevokeRequest {
46 #[serde(default)]
48 pub reason: Option<u32>,
49}
50
51fn bad_status(error: UnknownStatus) -> AdminError {
53 AdminError::with_code(StatusCode::BAD_REQUEST, "invalid_status", error.to_string())
54}
55
56pub async fn list_orders(
58 State(state): State<AdminState>,
59 Query(params): Query<OrderListParams>,
60 _auth: Authenticated,
61) -> Result<Json<Value>, AdminError> {
62 let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
63 let status = params.parsed_status().map_err(bad_status)?;
65 let query = OrderQuery {
66 profile: params.profile,
67 account_id: params.account_id,
68 status,
69 limit: page.limit,
70 offset: page.offset,
71 };
72 let (orders, total) = Order::search(&query, &state.database).await?;
73
74 let items = render_orders(&orders, &state).await?;
75 Ok(Json(page_envelope(items, total, page)))
76}
77
78pub async fn get_order(
80 State(state): State<AdminState>,
81 Path(id): Path<String>,
82 _auth: Authenticated,
83) -> Result<Json<Value>, AdminError> {
84 let detail = admin::load_order_detail(&id, state.database.clone())
85 .await?
86 .ok_or_else(|| not_found(&id))?;
87 Ok(Json(admin::render_order_detail_json(
88 &detail,
89 &state.config.server.base_url,
90 )))
91}
92
93pub async fn revoke_order(
103 State(state): State<AdminState>,
104 Path(id): Path<String>,
105 request_context: crate::audit::RequestContext,
106 AuthenticatedWrite(auth): AuthenticatedWrite,
107 body: Option<Json<RevokeRequest>>,
108) -> Result<Json<Value>, AdminError> {
109 let reason = body.and_then(|Json(body)| body.reason);
110
111 let signer = resolve_order_signer(&state, &id).await?;
115
116 let outcome = admin::revoke_order(
120 &id,
121 reason,
122 crate::audit::Actor::admin(&auth.user.username),
123 state.audit.client(&request_context).await,
124 state.database.clone(),
125 signer,
126 )
127 .await
128 .map_err(revoke_error)?;
129
130 match outcome {
131 RevokeOutcome::NotFound => Err(not_found(&id)),
132 RevokeOutcome::NotIssued => Err(AdminError::conflict(
133 "order_not_issued",
134 format!("order {id} has no certificate to revoke"),
135 )),
136 RevokeOutcome::AlreadyRevoked => Err(AdminError::conflict(
137 "already_revoked",
138 format!("order {id} was already revoked"),
139 )),
140 RevokeOutcome::Revoked(order) => {
141 tracing::info!(event = "admin_order_revoked",
142 outcome = "success",
143 surface = "api",
144 order_id = %id,
145 profile = %order.profile,
146 reason = ?reason,
147 username = %auth.user.username);
148 let authz_ids = authz_ids(&order.id, &state).await?;
149 Ok(Json(admin::render_order_json(
150 &order,
151 &state.config.server.base_url,
152 &authz_ids,
153 )))
154 }
155 }
156}
157
158pub async fn delete_order(
160 State(state): State<AdminState>,
161 Path(id): Path<String>,
162 AuthenticatedWrite(auth): AuthenticatedWrite,
163) -> Result<Response, AdminError> {
164 let deleted = admin::delete_order(&id, state.database.clone())
165 .await?
166 .ok_or_else(|| not_found(&id))?;
167
168 tracing::info!(event = "admin_order_deleted",
169 outcome = "success",
170 surface = "api",
171 order_id = %id,
172 username = %auth.user.username,
173 cascaded_authorizations = deleted.cascaded);
174 Ok((
175 StatusCode::OK,
176 Json(json!({ "deleted": { "authorizations": deleted.cascaded } })),
177 )
178 .into_response())
179}
180
181pub(crate) async fn render_orders(
186 orders: &[Order],
187 state: &AdminState,
188) -> Result<Vec<Value>, AdminError> {
189 let ids: Vec<&str> = orders.iter().map(|order| order.id.as_str()).collect();
192 let mut grouped = Authorization::find_ids_by_orders(&ids, &state.database).await?;
193
194 let items = orders
195 .iter()
196 .map(|order| {
197 admin::render_order_json(
198 order,
199 &state.config.server.base_url,
200 &grouped.remove(&order.id).unwrap_or_default(),
201 )
202 })
203 .collect();
204 Ok(items)
205}
206
207async fn authz_ids(order_id: &str, state: &AdminState) -> Result<Vec<String>, AdminError> {
210 Ok(Authorization::find_by_order(order_id, &state.database)
211 .await?
212 .into_iter()
213 .map(|authz| authz.id)
214 .collect())
215}
216
217pub(crate) fn revoke_error(error: RevokeError) -> AdminError {
224 match error {
225 RevokeError::BadReason(reason) => AdminError::bad_request(format!(
226 "unsupported revocation reason code {reason} (RFC 5280 §5.3.1)"
227 )),
228 RevokeError::Signer(_) => {
229 tracing::error!(event = "admin_revoke_signer_failed", outcome = "failure", error = %error);
230 AdminError::signer_failed("the signer backend refused the revocation; retry")
231 }
232 RevokeError::Database(inner) => AdminError::from(inner),
233 RevokeError::Internal(detail) => {
234 tracing::error!(event = "admin_revoke_internal_error", outcome = "failure", error = %detail);
235 AdminError::internal()
236 }
237 }
238}
239
240pub(crate) async fn resolve_order_signer(
251 state: &AdminState,
252 id: &str,
253) -> Result<std::sync::Arc<dyn crate::signer::SignerBackend>, AdminError> {
254 let order = Order::find_by_id(id, &state.database)
255 .await?
256 .ok_or_else(|| not_found(id))?;
257 let profile = state.profiles.get(&order.profile).ok_or_else(|| {
258 AdminError::conflict(
259 "profile_not_mounted",
260 format!(
261 "order {id} belongs to profile `{}`, which this configuration does not mount",
262 order.profile
263 ),
264 )
265 })?;
266 Ok(profile.signer.clone())
267}
268
269fn not_found(id: &str) -> AdminError {
270 AdminError::not_found(format!("no such order: {id}"))
271}