use crate::serve::error::ServeError;
use crate::serve::history::{AuditEntry, AuditFilter};
use crate::serve::state::ServerState;
use axum::Json;
use axum::extract::{Query, State};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
const DEFAULT_LIMIT: usize = 100;
const MAX_LIMIT: usize = 1000;
#[derive(Debug, Deserialize)]
pub struct AuditQuery {
pub principal: Option<String>,
pub action: Option<String>,
pub since: Option<String>,
pub until: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct AuditListResponse {
pub entries: Vec<AuditEntry>,
}
fn parse_ts(raw: &str, field: &str) -> Result<DateTime<Utc>, ServeError> {
DateTime::parse_from_rfc3339(&raw.replace(' ', "+"))
.map(|d| d.to_utc())
.map_err(|e| ServeError::BadConfig(format!("invalid `{field}` timestamp: {e}")))
}
pub async fn list_audit(
State(state): State<ServerState>,
Query(query): Query<AuditQuery>,
) -> Result<Json<AuditListResponse>, ServeError> {
let filter = AuditFilter {
principal: query.principal,
action: query.action,
since: query
.since
.as_deref()
.map(|s| parse_ts(s, "since"))
.transpose()?,
until: query
.until
.as_deref()
.map(|s| parse_ts(s, "until"))
.transpose()?,
limit: query.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT),
};
let entries = state
.history()
.list_audit(&filter)
.await
.map_err(|e| ServeError::Internal(e.to_string()))?;
Ok(Json(AuditListResponse { entries }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ts_accepts_z_and_offset() {
assert!(parse_ts("2026-01-01T00:00:00Z", "since").is_ok());
assert!(parse_ts("2026-01-01T00:00:00 05:30", "since").is_ok());
assert!(parse_ts("not-a-date", "since").is_err());
}
}