1use axum::extract::{Path, Query, State};
10use axum::http::StatusCode;
11use axum::response::{Html, Response};
12use serde::Deserialize;
13use serde_json::{Map, Value};
14
15use crate::admin;
16use crate::webadmin::AdminState;
17use crate::webadmin::handlers::Caller;
18use crate::webadmin::handlers::accounts::{
19 AccountListParams, apply_deactivate_account, apply_delete_account, apply_update_account_contact,
20};
21use crate::webadmin::handlers::orders::render_orders;
22use crate::webadmin::handlers::paging::PageParams;
23use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
24use crate::webadmin::pages::error::{PageError, redirect};
25use crate::webadmin::pages::{
26 ListFilters, chrome, flash, page_value, pager, respond, respond_fragment,
27};
28use acme_proxy_store::account::Account;
29use acme_proxy_store::order::Order;
30use acme_proxy_store::order::OrderQuery;
31
32#[derive(Debug, Deserialize)]
34pub struct ContactForm {
35 pub contact: String,
39}
40
41pub async fn list_accounts(
43 State(state): State<AdminState>,
44 Query(params): Query<AccountListParams>,
45 session: PageSession,
46) -> Result<Html<String>, PageError> {
47 let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
48 let filters = ListFilters::new()
49 .with("profile", params.profile.as_deref())
50 .with("eabKid", params.eab_kid.as_deref());
51
52 let (accounts, total) = Account::search(
53 params.profile.as_deref(),
54 params.eab_kid.as_deref(),
55 page.limit,
56 page.offset,
57 &state.database,
58 )
59 .await?;
60
61 let items: Vec<Value> = accounts
62 .iter()
63 .map(|account| admin::render_account_json(account, &state.config.server.base_url))
64 .collect();
65
66 let mut context = chrome(&session, "accounts", "Accounts");
67 context.insert("page".to_string(), page_value(items, total));
68 context.insert(
69 "pager".to_string(),
70 pager(
71 page,
72 total,
73 "/ui/accounts",
74 &filters.pairs(),
75 "#accounts-table",
76 ),
77 );
78 context.insert("filters".to_string(), filters.to_value());
79 context.insert(
80 "profiles".to_string(),
81 Value::Array(crate::webadmin::handlers::misc::profile_rows(&state)),
82 );
83
84 respond(
85 &state,
86 session.hx,
87 "accounts/list.html",
88 "accounts/_table.html",
89 context,
90 )
91}
92
93pub async fn get_account(
99 State(state): State<AdminState>,
100 Path(id): Path<String>,
101 Query(params): Query<PageParams>,
102 session: PageSession,
103) -> Result<Html<String>, PageError> {
104 let context = account_page_context(&state, &id, params, &session).await?;
105 respond(
106 &state,
107 session.hx,
108 "accounts/detail.html",
109 "accounts/_card.html",
110 context,
111 )
112}
113
114pub async fn list_account_orders(
123 State(state): State<AdminState>,
124 Path(id): Path<String>,
125 Query(params): Query<PageParams>,
126 session: PageSession,
127) -> Result<Html<String>, PageError> {
128 let context = account_page_context(&state, &id, params, &session).await?;
129 respond(
130 &state,
131 session.hx,
132 "accounts/detail.html",
133 "orders/_table.html",
134 context,
135 )
136}
137
138async fn account_page_context(
141 state: &AdminState,
142 id: &str,
143 params: PageParams,
144 session: &PageSession,
145) -> Result<Map<String, Value>, PageError> {
146 let account = load(id, state).await?;
147 let page = params.resolve(&state.config);
148
149 let (orders, total) = Order::search(
150 &OrderQuery {
151 account_id: Some(id.to_string()),
152 limit: page.limit,
153 offset: page.offset,
154 ..OrderQuery::default()
155 },
156 &state.database,
157 )
158 .await?;
159 let items = render_orders(&orders, state).await?;
160
161 let mut pager = pager(
162 page,
163 total,
164 &format!("/ui/accounts/{id}/orders"),
165 &[],
166 "#orders-table",
167 );
168 pager["push"] = Value::Bool(false);
169
170 let mut context = chrome(session, "accounts", "Account");
171 context.insert("account".to_string(), account);
172 context.insert("page".to_string(), page_value(items, total));
173 context.insert("order_count".to_string(), Value::from(total));
174 context.insert(
175 "live_certificates".to_string(),
176 Value::from(live_certificates(id, state).await?),
177 );
178 context.insert("pager".to_string(), pager);
179 Ok(context)
180}
181
182pub async fn post_account_contact(
184 State(state): State<AdminState>,
185 Path(id): Path<String>,
186 session: PageSessionWrite,
187 request_context: acme_proxy_core::audit::RequestContext,
188 axum::Form(form): axum::Form<ContactForm>,
189) -> Result<Html<String>, PageError> {
190 let contact: Vec<String> = form
191 .contact
192 .lines()
193 .map(str::trim)
194 .filter(|line| !line.is_empty())
195 .map(str::to_string)
196 .collect();
197
198 match apply_update_account_contact(
202 &state,
203 &Caller::ui(&session.auth, &request_context),
204 &id,
205 contact,
206 )
207 .await
208 {
209 Ok(account) => {
210 let rendered = admin::render_account_json(&account, &state.config.server.base_url);
211 card(&state, &session, rendered, flash("ok", "Contact updated.")).await
212 }
213 Err(error) if error.status == StatusCode::BAD_REQUEST => {
214 let account = load(&id, &state).await?;
215 card(
216 &state,
217 &session,
218 account,
219 super::flash_error(error.code, error.message),
220 )
221 .await
222 }
223 Err(error) => Err(error.into()),
224 }
225}
226
227pub async fn deactivate_account(
229 State(state): State<AdminState>,
230 Path(id): Path<String>,
231 session: PageSessionWrite,
232 request_context: acme_proxy_core::audit::RequestContext,
233) -> Result<Html<String>, PageError> {
234 let account =
235 apply_deactivate_account(&state, &Caller::ui(&session.auth, &request_context), &id).await?;
236
237 let rendered = admin::render_account_json(&account, &state.config.server.base_url);
238 card(
239 &state,
240 &session,
241 rendered,
242 flash(
243 "ok",
244 "Account deactivated. It can no longer request issuance.",
245 ),
246 )
247 .await
248}
249
250pub async fn delete_account(
255 State(state): State<AdminState>,
256 Path(id): Path<String>,
257 session: PageSessionWrite,
258 request_context: acme_proxy_core::audit::RequestContext,
259) -> Result<Response, PageError> {
260 match apply_delete_account(&state, &Caller::ui(&session.auth, &request_context), &id).await {
261 Ok(_) => {}
262 Err(error) if error.status == StatusCode::CONFLICT => {
266 let context = card_context(&state, &session, load(&id, &state).await?).await?;
267 return super::refuse_with_card(&state, "accounts/_card.html", context, &error);
268 }
269 Err(error) => return Err(error.into()),
270 }
271
272 Ok(redirect("/ui/accounts", session.hx))
273}
274
275async fn card(
278 state: &AdminState,
279 session: &PageSessionWrite,
280 account: Value,
281 banner: Value,
282) -> Result<Html<String>, PageError> {
283 let mut context = card_context(state, session, account).await?;
284 context.insert("flash".to_string(), banner);
285 respond_fragment(state, "accounts/_card.html", context)
286}
287
288async fn card_context(
290 state: &AdminState,
291 session: &PageSessionWrite,
292 account: Value,
293) -> Result<Map<String, Value>, PageError> {
294 let id = account["id"].as_str().unwrap_or_default().to_string();
295 let mut context = super::fragment_context(&session.auth);
296 context.insert("account".to_string(), account);
297 context.insert(
298 "order_count".to_string(),
299 Value::from(order_count(&id, state).await?),
300 );
301 context.insert(
302 "live_certificates".to_string(),
303 Value::from(live_certificates(&id, state).await?),
304 );
305 Ok(context)
306}
307
308async fn live_certificates(id: &str, state: &AdminState) -> Result<u64, PageError> {
312 let Some(account_id) = acme_proxy_store::id::parse(id) else {
313 return Ok(0);
314 };
315 Ok(Account::count_live_certificates(account_id, &state.database).await?)
316}
317
318async fn order_count(id: &str, state: &AdminState) -> Result<i64, PageError> {
321 let (_, total) = Order::search(
322 &OrderQuery {
323 account_id: Some(id.to_string()),
324 limit: 1,
325 ..OrderQuery::default()
326 },
327 &state.database,
328 )
329 .await?;
330 Ok(total)
331}
332
333async fn load(id: &str, state: &AdminState) -> Result<Value, PageError> {
334 let account = Account::find_any_by_id(id, &state.database)
335 .await?
336 .ok_or_else(|| not_found(id))?;
337 Ok(admin::render_account_json(
338 &account,
339 &state.config.server.base_url,
340 ))
341}
342
343fn not_found(id: &str) -> PageError {
344 PageError::not_found(crate::admin::subject::Subject::Account.missing(id))
345}