use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Response};
use serde::Deserialize;
use serde_json::{Map, Value};
use crate::admin;
use crate::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::Caller;
use crate::webadmin::handlers::eab::{
DeleteEabParams, apply_create_eab, apply_delete_eab, apply_revoke_eab,
};
use crate::webadmin::handlers::paging::{Page, PageParams};
use crate::webadmin::handlers::params::non_empty;
use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
use crate::webadmin::pages::error::{PageError, redirect};
use crate::webadmin::pages::{chrome, flash, page_value, pager, respond, respond_fragment};
use acme_proxy_store::account::Account;
use acme_proxy_store::eab::Eab;
#[derive(Debug, Deserialize, Default)]
pub struct CreateForm {
#[serde(default)]
pub label: String,
#[serde(default)]
pub profile: String,
}
pub async fn list_eab(
State(state): State<AdminState>,
Query(params): Query<PageParams>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let page = params.resolve(&state.config);
let (items, total) = rows(page, &state).await?;
let mut context = chrome(&session, "eab", "External Account Binding");
context.insert("page".to_string(), page_value(items, total));
context.insert(
"pager".to_string(),
pager(page, total, "/ui/eab", &[], "#eab-table"),
);
context.insert(
"profiles".to_string(),
Value::Array(crate::webadmin::handlers::misc::profile_rows(&state)),
);
respond(
&state,
session.hx,
"eab/list.html",
"eab/_table.html",
context,
)
}
pub async fn get_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let eab = load(&kid, &state).await?;
let mut context = chrome(&session, "eab", "Credential");
context.insert("eab".to_string(), eab);
insert_bound_accounts(&mut context, &kid, &state).await?;
respond(
&state,
session.hx,
"eab/detail.html",
"eab/_card.html",
context,
)
}
pub async fn create_eab(
State(state): State<AdminState>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
axum::Form(form): axum::Form<CreateForm>,
) -> Result<Response, PageError> {
let eab = apply_create_eab(
&state,
&Caller::ui(&session.auth, &request_context),
non_empty(&form.label),
non_empty(&form.profile),
"leave it unset",
)
.await?;
let page = PageParams::default().resolve(&state.config);
let (items, total) = rows(page, &state).await?;
let mut context = Map::new();
context.insert("eab".to_string(), admin::render_eab_created_json(&eab));
context.insert("page".to_string(), page_value(items, total));
context.insert(
"pager".to_string(),
pager(page, total, "/ui/eab", &[], "#eab-table"),
);
context.insert("oob".to_string(), Value::Bool(true));
let body = respond_fragment(&state, "eab/_created.html", context)?;
Ok((StatusCode::CREATED, body).into_response())
}
pub async fn revoke_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<Html<String>, PageError> {
apply_revoke_eab(&state, &Caller::ui(&session.auth, &request_context), &kid).await?;
let mut context = card_context(&kid, &state, &session).await?;
context.insert(
"flash".to_string(),
flash(
"ok",
"Credential revoked. Registrations using it fail from now on.",
),
);
respond_fragment(&state, "eab/_card.html", context)
}
pub async fn delete_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
Query(params): Query<DeleteEabParams>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<Response, PageError> {
let accounts = match params.resolve() {
Ok(accounts) => accounts,
Err(error) => return refuse(&kid, &state, &session, &error).await,
};
let caller = Caller::ui(&session.auth, &request_context);
match apply_delete_eab(&state, &caller, &kid, accounts).await {
Ok(_) => {}
Err(error) if error.status == StatusCode::NOT_FOUND || error.status.is_server_error() => {
return Err(error.into());
}
Err(error) => return refuse(&kid, &state, &session, &error).await,
}
Ok(redirect("/ui/eab", session.hx))
}
async fn refuse(
kid: &str,
state: &AdminState,
session: &PageSessionWrite,
error: &AdminError,
) -> Result<Response, PageError> {
let context = card_context(kid, state, session).await?;
super::refuse_with_card(state, "eab/_card.html", context, error)
}
async fn card_context(
kid: &str,
state: &AdminState,
session: &PageSessionWrite,
) -> Result<Map<String, Value>, PageError> {
let mut context = super::fragment_context(&session.auth);
context.insert("eab".to_string(), load(kid, state).await?);
insert_bound_accounts(&mut context, kid, state).await?;
Ok(context)
}
async fn insert_bound_accounts(
context: &mut Map<String, Value>,
kid: &str,
state: &AdminState,
) -> Result<(), PageError> {
let summary = match acme_proxy_store::id::parse(kid) {
Some(kid) => Account::eab_summary(kid, &state.database).await?,
None => Default::default(),
};
context.insert(
"bound".to_string(),
serde_json::json!({
"accounts": summary.accounts,
"activeAccounts": summary.active_accounts,
"orders": summary.orders,
"liveCertificates": summary.live_certificates,
}),
);
Ok(())
}
async fn rows(page: Page, state: &AdminState) -> Result<(Vec<Value>, i64), PageError> {
let (keys, total) = Eab::search(page.limit, page.offset, &state.database).await?;
Ok((keys.iter().map(admin::render_eab_json).collect(), total))
}
async fn load(kid: &str, state: &AdminState) -> Result<Value, PageError> {
let eab = Eab::find_any_by_kid(kid, &state.database)
.await?
.ok_or_else(|| not_found(kid))?;
Ok(admin::render_eab_json(&eab))
}
fn not_found(kid: &str) -> PageError {
PageError::not_found(crate::admin::subject::Subject::EabCredential.missing(kid))
}