use axum::extract::State;
use axum::response::Html;
use serde::Deserialize;
use serde_json::Value;
use crate::webadmin::AdminState;
use crate::webadmin::handlers::Caller;
use crate::webadmin::handlers::misc::{apply_cleanup_nonces, profile_rows};
use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
use crate::webadmin::pages::error::PageError;
use crate::webadmin::pages::{chrome, flash, respond, respond_fragment};
use acme_proxy_store::account::Account;
use acme_proxy_store::audit::AuditEntry;
use acme_proxy_store::audit::AuditQuery;
use acme_proxy_store::eab::Eab;
use acme_proxy_store::job::Job;
use acme_proxy_store::job::JobQuery;
use acme_proxy_store::nonce::Nonce;
use acme_proxy_store::order::Order;
use acme_proxy_store::order::OrderQuery;
use acme_proxy_store::status::JobStatus;
#[derive(Debug, Deserialize, Default)]
pub struct CleanupForm {
#[serde(default, rename = "ttlSeconds")]
pub ttl_seconds: String,
}
const ATTENTION_EXPIRY_DAYS: u64 = 7;
pub async fn get_index(
State(state): State<AdminState>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let (_, accounts) = Account::search(None, None, 1, 0, &state.database).await?;
let (_, orders) = Order::search(
&OrderQuery {
limit: 1,
..OrderQuery::default()
},
&state.database,
)
.await?;
let (_, eab) = Eab::search(1, 0, &state.database).await?;
let nonces = Nonce::count(&state.database).await?;
let (_, failed_jobs) = Job::search(
&JobQuery {
kind: None,
status: Some(JobStatus::Failed),
limit: 1,
offset: 0,
},
&state.database,
)
.await?;
let (_, expiring_soon, _) = acme_proxy_store::expiring::list_expiring(
&acme_proxy_store::expiring::ExpiringQuery {
profile: None,
before: acme_proxy_store::expiring::expiring_horizon(ATTENTION_EXPIRY_DAYS),
include_superseded: true,
limit: 1,
offset: 0,
},
state.database.clone(),
)
.await?;
let (_, refusals) = AuditEntry::search(
&AuditQuery {
outcome: Some("failure".to_string()),
since: Some(acme_proxy_store::nonce::now_secs().saturating_sub(24 * 60 * 60)),
limit: 1,
..AuditQuery::default()
},
&state.database,
)
.await?;
let profiles = profile_rows(&state);
let any_bypass = profiles
.iter()
.any(|profile| profile["challengeBypass"] == Value::Bool(true));
let mut context = chrome(&session, "index", "Overview");
context.insert(
"stats".to_string(),
serde_json::json!({
"accounts": accounts,
"orders": orders,
"eab": eab,
"nonces": nonces,
"failedJobs": failed_jobs,
"expiringSoon": expiring_soon,
"expiryDays": ATTENTION_EXPIRY_DAYS,
"refusals": refusals,
}),
);
context.insert("any_bypass".to_string(), Value::Bool(any_bypass));
context.insert("profiles".to_string(), Value::Array(profiles));
respond(&state, false, "index.html", "index.html", context)
}
pub async fn list_profiles(
State(state): State<AdminState>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let profiles = profile_rows(&state);
let any_bypass = profiles
.iter()
.any(|profile| profile["challengeBypass"] == Value::Bool(true));
let mut context = chrome(&session, "profiles", "Profiles");
context.insert("profiles".to_string(), Value::Array(profiles));
context.insert("any_bypass".to_string(), Value::Bool(any_bypass));
respond(
&state,
false,
"profiles/list.html",
"profiles/_table.html",
context,
)
}
pub async fn get_nonces(
State(state): State<AdminState>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let count = Nonce::count(&state.database).await?;
let mut context = chrome(&session, "nonces", "Nonces");
context.insert("count".to_string(), Value::from(count));
context.insert(
"ttl_seconds".to_string(),
Value::from(state.config.nonce.ttl_seconds),
);
respond(
&state,
session.hx,
"nonces/index.html",
"nonces/_panel.html",
context,
)
}
pub async fn cleanup_nonces(
State(state): State<AdminState>,
session: PageSessionWrite,
request_context: acme_proxy_core::audit::RequestContext,
axum::Form(form): axum::Form<CleanupForm>,
) -> Result<Html<String>, PageError> {
let seconds = match form.ttl_seconds.trim() {
"" => state.config.nonce.ttl_seconds,
raw => raw.parse::<u64>().map_err(|_| {
PageError::from(crate::webadmin::error::AdminError::bad_request(format!(
"`{raw}` is not a number of seconds"
)))
})?,
};
let removed = apply_cleanup_nonces(
&state,
&Caller::ui(&session.auth, &request_context),
seconds,
)
.await?;
let count = Nonce::count(&state.database).await?;
let mut context = super::fragment_context(&session.auth);
context.insert("count".to_string(), Value::from(count));
context.insert(
"ttl_seconds".to_string(),
Value::from(state.config.nonce.ttl_seconds),
);
context.insert(
"flash".to_string(),
flash("ok", format!("Swept {removed} nonce(s).")),
);
respond_fragment(&state, "nonces/_panel.html", context)
}