use axum::{ Json, extract::{Query, State}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; use super::auth::UserAuth; use super::permissions::Permission; use crate::AppState; use crate::db::{adapt_sql, parse_dt}; use crate::error::AppError; #[derive(Deserialize)] pub struct EventsQuery { pub event_type: Option, pub category: Option, pub severity: Option, pub subject: Option, pub cursor: Option, pub limit: Option, } #[derive(Serialize)] pub struct EventResponse { pub id: String, pub event_type: String, pub severity: String, pub actor_did: Option, pub subject: Option, pub detail: Value, pub created_at: chrono::DateTime, } #[derive(Serialize)] pub struct EventsListResponse { pub events: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// GET /admin/events — list event logs with optional filters and pagination. pub(super) async fn list_events( auth: UserAuth, State(state): State, Query(query): Query, ) -> Result, AppError> { auth.require(Permission::EventsRead).await?; let backend = state.db_backend; let limit = query.limit.unwrap_or(50).clamp(1, 100); let mut sql = String::from( "SELECT id, event_type, severity, actor_did, subject, detail, created_at FROM event_logs WHERE 1=1", ); if query.event_type.is_some() { sql.push_str(" AND event_type = ?"); } if query.category.is_some() { sql.push_str(" AND event_type LIKE ?"); } if query.severity.is_some() { sql.push_str(" AND severity = ?"); } if query.subject.is_some() { sql.push_str(" AND subject = ?"); } if query.cursor.is_some() { sql.push_str(" AND created_at < ?"); } sql.push_str(" ORDER BY created_at DESC LIMIT ?"); let sql = adapt_sql(&sql, backend); #[allow(clippy::type_complexity)] let mut q = sqlx::query_as::< _, ( String, String, String, Option, Option, String, String, ), >(&sql); if let Some(ref event_type) = query.event_type { q = q.bind(event_type); } if let Some(ref category) = query.category { q = q.bind(format!("{category}.%")); } if let Some(ref severity) = query.severity { q = q.bind(severity); } if let Some(ref subject) = query.subject { q = q.bind(subject); } if let Some(ref cursor) = query.cursor { q = q.bind(cursor); } q = q.bind(limit); let rows = q .fetch_all(&state.db) .await .map_err(|e| AppError::Internal(format!("failed to query events: {e}")))?; let events: Vec = rows .into_iter() .map(|row| EventResponse { id: row.0, event_type: row.1, severity: row.2, actor_did: row.3, subject: row.4, detail: serde_json::from_str(&row.5).unwrap_or(Value::Object(Default::default())), created_at: parse_dt(&row.6), }) .collect(); let cursor = events.last().map(|e| e.created_at.to_rfc3339()); Ok(Json(EventsListResponse { events, cursor })) }