faucet_cli/serve/handlers/
audit.rs1use crate::serve::error::ServeError;
6use crate::serve::history::{AuditEntry, AuditFilter};
7use crate::serve::state::ServerState;
8use axum::Json;
9use axum::extract::{Query, State};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13const DEFAULT_LIMIT: usize = 100;
14const MAX_LIMIT: usize = 1000;
15
16#[derive(Debug, Deserialize)]
18pub struct AuditQuery {
19 pub principal: Option<String>,
20 pub action: Option<String>,
21 pub since: Option<String>,
22 pub until: Option<String>,
23 pub limit: Option<usize>,
24}
25
26#[derive(Debug, Serialize)]
28pub struct AuditListResponse {
29 pub entries: Vec<AuditEntry>,
30}
31
32fn parse_ts(raw: &str, field: &str) -> Result<DateTime<Utc>, ServeError> {
35 DateTime::parse_from_rfc3339(&raw.replace(' ', "+"))
36 .map(|d| d.to_utc())
37 .map_err(|e| ServeError::BadConfig(format!("invalid `{field}` timestamp: {e}")))
38}
39
40pub async fn list_audit(
42 State(state): State<ServerState>,
43 Query(query): Query<AuditQuery>,
44) -> Result<Json<AuditListResponse>, ServeError> {
45 let filter = AuditFilter {
46 principal: query.principal,
47 action: query.action,
48 since: query
49 .since
50 .as_deref()
51 .map(|s| parse_ts(s, "since"))
52 .transpose()?,
53 until: query
54 .until
55 .as_deref()
56 .map(|s| parse_ts(s, "until"))
57 .transpose()?,
58 limit: query.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT),
59 };
60 let entries = state
61 .history()
62 .list_audit(&filter)
63 .await
64 .map_err(|e| ServeError::Internal(e.to_string()))?;
65 Ok(Json(AuditListResponse { entries }))
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn parse_ts_accepts_z_and_offset() {
74 assert!(parse_ts("2026-01-01T00:00:00Z", "since").is_ok());
75 assert!(parse_ts("2026-01-01T00:00:00 05:30", "since").is_ok());
77 assert!(parse_ts("not-a-date", "since").is_err());
78 }
79}