1use axum::extract::{Path, Query, State};
10use axum::http::StatusCode;
11use axum::response::{Html, IntoResponse, Response};
12use serde::Deserialize;
13use serde_json::{Map, Value};
14
15use crate::admin;
16use crate::webadmin::AdminState;
17use crate::webadmin::error::AdminError;
18use crate::webadmin::handlers::Caller;
19use crate::webadmin::handlers::eab::{
20 DeleteEabParams, apply_create_eab, apply_delete_eab, apply_revoke_eab,
21};
22use crate::webadmin::handlers::paging::{Page, PageParams};
23use crate::webadmin::handlers::params::non_empty;
24use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
25use crate::webadmin::pages::error::{PageError, redirect};
26use crate::webadmin::pages::{chrome, flash, page_value, pager, respond, respond_fragment};
27use acme_proxy_store::account::Account;
28use acme_proxy_store::eab::Eab;
29
30#[derive(Debug, Deserialize, Default)]
33pub struct CreateForm {
34 #[serde(default)]
37 pub label: String,
38 #[serde(default)]
41 pub profile: String,
42}
43
44pub async fn list_eab(
53 State(state): State<AdminState>,
54 Query(params): Query<PageParams>,
55 session: PageSession,
56) -> Result<Html<String>, PageError> {
57 let page = params.resolve(&state.config);
58 let (items, total) = rows(page, &state).await?;
59
60 let mut context = chrome(&session, "eab", "External Account Binding");
61 context.insert("page".to_string(), page_value(items, total));
62 context.insert(
63 "pager".to_string(),
64 pager(page, total, "/ui/eab", &[], "#eab-table"),
65 );
66 context.insert(
67 "profiles".to_string(),
68 Value::Array(crate::webadmin::handlers::misc::profile_rows(&state)),
69 );
70
71 respond(
72 &state,
73 session.hx,
74 "eab/list.html",
75 "eab/_table.html",
76 context,
77 )
78}
79
80pub async fn get_eab(
82 State(state): State<AdminState>,
83 Path(kid): Path<String>,
84 session: PageSession,
85) -> Result<Html<String>, PageError> {
86 let eab = load(&kid, &state).await?;
87
88 let mut context = chrome(&session, "eab", "Credential");
89 context.insert("eab".to_string(), eab);
90 insert_bound_accounts(&mut context, &kid, &state).await?;
91
92 respond(
93 &state,
94 session.hx,
95 "eab/detail.html",
96 "eab/_card.html",
97 context,
98 )
99}
100
101pub async fn create_eab(
107 State(state): State<AdminState>,
108 session: PageSessionWrite,
109 request_context: acme_proxy_core::audit::RequestContext,
110 axum::Form(form): axum::Form<CreateForm>,
113) -> Result<Response, PageError> {
114 let eab = apply_create_eab(
115 &state,
116 &Caller::ui(&session.auth, &request_context),
117 non_empty(&form.label),
118 non_empty(&form.profile),
119 "leave it unset",
120 )
121 .await?;
122
123 let page = PageParams::default().resolve(&state.config);
130 let (items, total) = rows(page, &state).await?;
131
132 let mut context = Map::new();
133 context.insert("eab".to_string(), admin::render_eab_created_json(&eab));
134 context.insert("page".to_string(), page_value(items, total));
135 context.insert(
136 "pager".to_string(),
137 pager(page, total, "/ui/eab", &[], "#eab-table"),
138 );
139 context.insert("oob".to_string(), Value::Bool(true));
143
144 let body = respond_fragment(&state, "eab/_created.html", context)?;
145 Ok((StatusCode::CREATED, body).into_response())
146}
147
148pub async fn revoke_eab(
150 State(state): State<AdminState>,
151 Path(kid): Path<String>,
152 session: PageSessionWrite,
153 request_context: acme_proxy_core::audit::RequestContext,
154) -> Result<Html<String>, PageError> {
155 apply_revoke_eab(&state, &Caller::ui(&session.auth, &request_context), &kid).await?;
156
157 let mut context = card_context(&kid, &state, &session).await?;
158 context.insert(
159 "flash".to_string(),
160 flash(
161 "ok",
162 "Credential revoked. Registrations using it fail from now on.",
163 ),
164 );
165 respond_fragment(&state, "eab/_card.html", context)
166}
167
168pub async fn delete_eab(
175 State(state): State<AdminState>,
176 Path(kid): Path<String>,
177 Query(params): Query<DeleteEabParams>,
178 session: PageSessionWrite,
179 request_context: acme_proxy_core::audit::RequestContext,
180) -> Result<Response, PageError> {
181 let accounts = match params.resolve() {
182 Ok(accounts) => accounts,
183 Err(error) => return refuse(&kid, &state, &session, &error).await,
184 };
185 let caller = Caller::ui(&session.auth, &request_context);
186 match apply_delete_eab(&state, &caller, &kid, accounts).await {
187 Ok(_) => {}
188 Err(error) if error.status == StatusCode::NOT_FOUND || error.status.is_server_error() => {
190 return Err(error.into());
191 }
192 Err(error) => return refuse(&kid, &state, &session, &error).await,
193 }
194
195 Ok(redirect("/ui/eab", session.hx))
196}
197
198async fn refuse(
200 kid: &str,
201 state: &AdminState,
202 session: &PageSessionWrite,
203 error: &AdminError,
204) -> Result<Response, PageError> {
205 let context = card_context(kid, state, session).await?;
206 super::refuse_with_card(state, "eab/_card.html", context, error)
207}
208
209async fn card_context(
212 kid: &str,
213 state: &AdminState,
214 session: &PageSessionWrite,
215) -> Result<Map<String, Value>, PageError> {
216 let mut context = super::fragment_context(&session.auth);
217 context.insert("eab".to_string(), load(kid, state).await?);
218 insert_bound_accounts(&mut context, kid, state).await?;
219 Ok(context)
220}
221
222async fn insert_bound_accounts(
225 context: &mut Map<String, Value>,
226 kid: &str,
227 state: &AdminState,
228) -> Result<(), PageError> {
229 let summary = match acme_proxy_store::id::parse(kid) {
230 Some(kid) => Account::eab_summary(kid, &state.database).await?,
231 None => Default::default(),
232 };
233 context.insert(
234 "bound".to_string(),
235 serde_json::json!({
236 "accounts": summary.accounts,
237 "activeAccounts": summary.active_accounts,
238 "orders": summary.orders,
239 "liveCertificates": summary.live_certificates,
240 }),
241 );
242 Ok(())
243}
244
245async fn rows(page: Page, state: &AdminState) -> Result<(Vec<Value>, i64), PageError> {
246 let (keys, total) = Eab::search(page.limit, page.offset, &state.database).await?;
247 Ok((keys.iter().map(admin::render_eab_json).collect(), total))
248}
249
250async fn load(kid: &str, state: &AdminState) -> Result<Value, PageError> {
251 let eab = Eab::find_any_by_kid(kid, &state.database)
252 .await?
253 .ok_or_else(|| not_found(kid))?;
254 Ok(admin::render_eab_json(&eab))
255}
256
257fn not_found(kid: &str) -> PageError {
258 PageError::not_found(crate::admin::subject::Subject::EabCredential.missing(kid))
259}