use axum::extract::State;
use axum::response::Html;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::time::Duration;
use crate::admin;
use crate::sqlite::account::Account;
use crate::sqlite::eab::Eab;
use crate::sqlite::nonce::Nonce;
use crate::sqlite::order::{Order, OrderQuery};
use crate::webadmin::AdminState;
use crate::webadmin::handlers::misc::profile_rows;
use crate::webadmin::pages::auth::{PageSession, PageSessionWrite};
use crate::webadmin::pages::error::PageError;
use crate::webadmin::pages::{chrome, flash, respond, respond_fragment};
#[derive(Debug, Deserialize, Default)]
pub struct CleanupForm {
#[serde(default, rename = "ttlSeconds")]
pub ttl_seconds: String,
}
pub async fn get_index(
State(state): State<AdminState>,
session: PageSession,
) -> Result<Html<String>, PageError> {
let (_, accounts) = Account::search(None, 1, 0, &state.database).await?;
let (_, orders) = Order::search(
&OrderQuery {
limit: 1,
..OrderQuery::default()
},
&state.database,
)
.await?;
let eab = Eab::list_all(&state.database).await?.len();
let nonces = Nonce::count(&state.database).await?;
let mut context = chrome(&session, "index", "Overview");
context.insert(
"stats".to_string(),
serde_json::json!({
"accounts": accounts,
"orders": orders,
"eab": eab,
"nonces": nonces,
}),
);
context.insert("profiles".to_string(), Value::Array(profile_rows(&state)));
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,
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 =
admin::cleanup_nonces(Duration::from_secs(seconds), state.database.clone()).await?;
tracing::info!(event = "admin_nonces_cleaned",
outcome = "success",
surface = "ui",
rows_removed = removed,
ttl_seconds = seconds,
username = %session.auth.user.username);
let count = Nonce::count(&state.database).await?;
let mut context = Map::new();
context.insert(
"csrf_token".to_string(),
Value::String(session.auth.session.csrf_token.clone()),
);
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)
}