use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use serde_json::Value;
use crate::admin;
use crate::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::Caller;
use crate::webadmin::handlers::paging::{PageParams, page_envelope};
use crate::webadmin::handlers::params::empty_is_absent;
use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
use acme_proxy_store::eab::BoundAccounts;
use acme_proxy_store::eab::DeletedEab;
use acme_proxy_store::eab::Eab;
use acme_proxy_store::eab::EabDeletion;
#[derive(Debug, Deserialize, Default)]
pub struct CreateEab {
#[serde(default, deserialize_with = "empty_is_absent")]
pub label: Option<String>,
#[serde(default, deserialize_with = "empty_is_absent")]
pub profile: Option<String>,
}
#[derive(Debug, Deserialize, Default)]
pub struct DeleteEabParams {
#[serde(default, deserialize_with = "empty_is_absent")]
pub accounts: Option<String>,
}
impl DeleteEabParams {
pub(crate) fn resolve(&self) -> Result<BoundAccounts, AdminError> {
let Some(value) = self.accounts.as_deref() else {
return Ok(BoundAccounts::Keep);
};
BoundAccounts::parse(value).ok_or_else(|| {
let known: Vec<&str> = BoundAccounts::ALL
.iter()
.map(|mode| mode.as_str())
.collect();
AdminError::bad_request(format!(
"unknown accounts mode `{value}`; expected one of: {}",
known.join(", ")
))
})
}
}
pub async fn list_eab(
State(state): State<AdminState>,
Query(params): Query<PageParams>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let page = params.resolve(&state.config);
let (keys, total) = Eab::search(page.limit, page.offset, &state.database).await?;
let items: Vec<Value> = keys.iter().map(admin::render_eab_json).collect();
Ok(Json(page_envelope(items, total, page)))
}
pub async fn get_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let eab = Eab::find_any_by_kid(&kid, &state.database)
.await?
.ok_or_else(|| not_found(&kid))?;
Ok(Json(admin::render_eab_json(&eab)))
}
pub async fn create_eab(
State(state): State<AdminState>,
AuthenticatedWrite(auth): AuthenticatedWrite,
request_context: acme_proxy_core::audit::RequestContext,
body: Option<Json<CreateEab>>,
) -> Result<Response, AdminError> {
let Json(body) = body.unwrap_or_default();
let eab = apply_create_eab(
&state,
&Caller::api(&auth, &request_context),
body.label,
body.profile,
"omit `profile`",
)
.await?;
Ok((
StatusCode::CREATED,
Json(admin::render_eab_created_json(&eab)),
)
.into_response())
}
pub async fn revoke_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
AuthenticatedWrite(auth): AuthenticatedWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<StatusCode, AdminError> {
apply_revoke_eab(&state, &Caller::api(&auth, &request_context), &kid).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn delete_eab(
State(state): State<AdminState>,
Path(kid): Path<String>,
Query(params): Query<DeleteEabParams>,
AuthenticatedWrite(auth): AuthenticatedWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<Json<Value>, AdminError> {
let accounts = params.resolve()?;
let deleted = apply_delete_eab(
&state,
&Caller::api(&auth, &request_context),
&kid,
accounts,
)
.await?;
let orders: u64 = deleted.deleted.iter().map(|(_, orders)| orders).sum();
Ok(Json(serde_json::json!({
"deleted": { "accounts": deleted.deleted.len(), "orders": orders },
"deactivatedAccounts": deleted.deactivated.len(),
"keptAccounts": deleted.remaining,
})))
}
pub(crate) async fn apply_create_eab(
state: &AdminState,
caller: &Caller<'_>,
label: Option<String>,
profile: Option<String>,
hint: &str,
) -> Result<Eab, AdminError> {
require_mounted_profile(state, profile.as_deref(), hint)?;
let eab = Eab::create(label, profile, &state.database).await?;
state
.record_admin_action(caller.request, caller.username(), |actor, client| {
acme_proxy_jobs::auditor::admin::eab_created(
actor,
client,
&eab.kid.to_string(),
eab.profile.as_deref(),
eab.label.as_deref(),
)
})
.await;
tracing::info!(event = "admin_eab_created",
outcome = "success",
surface = caller.surface,
kid = %eab.kid,
profile = ?eab.profile,
username = %caller.username());
Ok(eab)
}
pub(crate) async fn apply_revoke_eab(
state: &AdminState,
caller: &Caller<'_>,
kid: &str,
) -> Result<(), AdminError> {
let subject = Eab::find_any_by_kid(kid, &state.database).await?;
if !Eab::revoke(kid, &state.database).await? {
return Err(not_found(kid));
}
if let Some(eab) = subject.as_ref().filter(|eab| eab.status == "active") {
state
.record_admin_action(caller.request, caller.username(), |actor, client| {
acme_proxy_jobs::auditor::admin::eab_revoked(
actor,
client,
kid,
eab.profile.as_deref(),
)
})
.await;
}
tracing::info!(event = "admin_eab_revoked",
outcome = "success",
surface = caller.surface,
kid = %kid,
username = %caller.username());
Ok(())
}
pub(crate) async fn apply_delete_eab(
state: &AdminState,
caller: &Caller<'_>,
kid: &str,
accounts: BoundAccounts,
) -> Result<DeletedEab, AdminError> {
let deleted = deleted_or_refused(
kid,
admin::delete_eab(kid, accounts, state.database.clone()).await?,
)?;
state
.record_admin_actions(caller.request, caller.username(), |actor, client| {
acme_proxy_jobs::auditor::admin::eab_deleted_records(actor, client, &deleted)
})
.await;
tracing::info!(event = "admin_eab_deleted",
outcome = "success",
surface = caller.surface,
kid = %kid,
accounts = accounts.as_str(),
username = %caller.username());
Ok(deleted)
}
pub(crate) fn deleted_or_refused(
kid: &str,
deletion: EabDeletion,
) -> Result<DeletedEab, AdminError> {
match deletion {
EabDeletion::NotFound => Err(not_found(kid)),
EabDeletion::LiveCertificates {
accounts,
certificates,
} => Err(AdminError::conflict(
"live_certificates",
admin::eab_live_certificates_refusal(kid, accounts, certificates),
)),
EabDeletion::Deleted(deleted) => Ok(deleted),
}
}
pub(crate) fn require_mounted_profile(
state: &AdminState,
profile: Option<&str>,
hint: &str,
) -> Result<(), AdminError> {
match crate::admin::ops::unmounted_profile_refusal(
|name| state.profiles.contains_key(name),
profile,
hint,
) {
Some(message) => Err(AdminError::bad_request(message)),
None => Ok(()),
}
}
fn not_found(kid: &str) -> AdminError {
AdminError::not_found(crate::admin::subject::Subject::EabCredential.missing(kid))
}