use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, Response};
use serde::Deserialize;
use serde_json::{Map, Value};
use crate::admin;
use crate::webadmin::AdminState;
use crate::webadmin::handlers::Caller;
use crate::webadmin::handlers::accounts::{
AccountListParams, apply_deactivate_account, apply_delete_account, apply_update_account_contact,
};
use crate::webadmin::handlers::orders::render_orders;
use crate::webadmin::handlers::paging::PageParams;
use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
use crate::webadmin::pages::error::{PageError, redirect};
use crate::webadmin::pages::{
ListFilters, chrome, flash, page_value, pager, respond, respond_fragment,
};
use acme_proxy_store::account::Account;
use acme_proxy_store::order::Order;
use acme_proxy_store::order::OrderQuery;
#[derive(Debug, Deserialize)]
pub struct ContactForm {
pub contact: String,
}
pub async fn list_accounts(
State(state): State<AdminState>,
Query(params): Query<AccountListParams>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
let filters = ListFilters::new()
.with("profile", params.profile.as_deref())
.with("eabKid", params.eab_kid.as_deref());
let (accounts, total) = Account::search(
params.profile.as_deref(),
params.eab_kid.as_deref(),
page.limit,
page.offset,
&state.database,
)
.await?;
let items: Vec<Value> = accounts
.iter()
.map(|account| admin::render_account_json(account, &state.config.server.base_url))
.collect();
let mut context = chrome(&session, "accounts", "Accounts");
context.insert("page".to_string(), page_value(items, total));
context.insert(
"pager".to_string(),
pager(
page,
total,
"/ui/accounts",
&filters.pairs(),
"#accounts-table",
),
);
context.insert("filters".to_string(), filters.to_value());
context.insert(
"profiles".to_string(),
Value::Array(crate::webadmin::handlers::misc::profile_rows(&state)),
);
respond(
&state,
session.hx,
"accounts/list.html",
"accounts/_table.html",
context,
)
}
pub async fn get_account(
State(state): State<AdminState>,
Path(id): Path<String>,
Query(params): Query<PageParams>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let context = account_page_context(&state, &id, params, &session).await?;
respond(
&state,
session.hx,
"accounts/detail.html",
"accounts/_card.html",
context,
)
}
pub async fn list_account_orders(
State(state): State<AdminState>,
Path(id): Path<String>,
Query(params): Query<PageParams>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let context = account_page_context(&state, &id, params, &session).await?;
respond(
&state,
session.hx,
"accounts/detail.html",
"orders/_table.html",
context,
)
}
async fn account_page_context(
state: &AdminState,
id: &str,
params: PageParams,
session: &PageSession,
) -> Result<Map<String, Value>, PageError> {
let account = load(id, state).await?;
let page = params.resolve(&state.config);
let (orders, total) = Order::search(
&OrderQuery {
account_id: Some(id.to_string()),
limit: page.limit,
offset: page.offset,
..OrderQuery::default()
},
&state.database,
)
.await?;
let items = render_orders(&orders, state).await?;
let mut pager = pager(
page,
total,
&format!("/ui/accounts/{id}/orders"),
&[],
"#orders-table",
);
pager["push"] = Value::Bool(false);
let mut context = chrome(session, "accounts", "Account");
context.insert("account".to_string(), account);
context.insert("page".to_string(), page_value(items, total));
context.insert("order_count".to_string(), Value::from(total));
context.insert(
"live_certificates".to_string(),
Value::from(live_certificates(id, state).await?),
);
context.insert("pager".to_string(), pager);
Ok(context)
}
pub async fn post_account_contact(
State(state): State<AdminState>,
Path(id): Path<String>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
axum::Form(form): axum::Form<ContactForm>,
) -> Result<Html<String>, PageError> {
let contact: Vec<String> = form
.contact
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect();
match apply_update_account_contact(
&state,
&Caller::ui(&session.auth, &request_context),
&id,
contact,
)
.await
{
Ok(account) => {
let rendered = admin::render_account_json(&account, &state.config.server.base_url);
card(&state, &session, rendered, flash("ok", "Contact updated.")).await
}
Err(error) if error.status == StatusCode::BAD_REQUEST => {
let account = load(&id, &state).await?;
card(
&state,
&session,
account,
super::flash_error(error.code, error.message),
)
.await
}
Err(error) => Err(error.into()),
}
}
pub async fn deactivate_account(
State(state): State<AdminState>,
Path(id): Path<String>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<Html<String>, PageError> {
let account =
apply_deactivate_account(&state, &Caller::ui(&session.auth, &request_context), &id).await?;
let rendered = admin::render_account_json(&account, &state.config.server.base_url);
card(
&state,
&session,
rendered,
flash(
"ok",
"Account deactivated. It can no longer request issuance.",
),
)
.await
}
pub async fn delete_account(
State(state): State<AdminState>,
Path(id): Path<String>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
) -> Result<Response, PageError> {
match apply_delete_account(&state, &Caller::ui(&session.auth, &request_context), &id).await {
Ok(_) => {}
Err(error) if error.status == StatusCode::CONFLICT => {
let context = card_context(&state, &session, load(&id, &state).await?).await?;
return super::refuse_with_card(&state, "accounts/_card.html", context, &error);
}
Err(error) => return Err(error.into()),
}
Ok(redirect("/ui/accounts", session.hx))
}
async fn card(
state: &AdminState,
session: &PageSessionWrite,
account: Value,
banner: Value,
) -> Result<Html<String>, PageError> {
let mut context = card_context(state, session, account).await?;
context.insert("flash".to_string(), banner);
respond_fragment(state, "accounts/_card.html", context)
}
async fn card_context(
state: &AdminState,
session: &PageSessionWrite,
account: Value,
) -> Result<Map<String, Value>, PageError> {
let id = account["id"].as_str().unwrap_or_default().to_string();
let mut context = super::fragment_context(&session.auth);
context.insert("account".to_string(), account);
context.insert(
"order_count".to_string(),
Value::from(order_count(&id, state).await?),
);
context.insert(
"live_certificates".to_string(),
Value::from(live_certificates(&id, state).await?),
);
Ok(context)
}
async fn live_certificates(id: &str, state: &AdminState) -> Result<u64, PageError> {
let Some(account_id) = acme_proxy_store::id::parse(id) else {
return Ok(0);
};
Ok(Account::count_live_certificates(account_id, &state.database).await?)
}
async fn order_count(id: &str, state: &AdminState) -> Result<i64, PageError> {
let (_, total) = Order::search(
&OrderQuery {
account_id: Some(id.to_string()),
limit: 1,
..OrderQuery::default()
},
&state.database,
)
.await?;
Ok(total)
}
async fn load(id: &str, state: &AdminState) -> Result<Value, PageError> {
let account = Account::find_any_by_id(id, &state.database)
.await?
.ok_or_else(|| not_found(id))?;
Ok(admin::render_account_json(
&account,
&state.config.server.base_url,
))
}
fn not_found(id: &str) -> PageError {
PageError::not_found(crate::admin::subject::Subject::Account.missing(id))
}