1use axum::extract::{Query, State};
4use axum::response::IntoResponse;
5use chrono::{DateTime, Utc};
6use serde::Deserialize;
7use uuid::Uuid;
8
9use ironflow_auth::extractor::Authenticated;
10#[cfg(feature = "openapi")]
11use ironflow_store::entities::AuditLogEntry;
12use ironflow_store::entities::{AuditLogFilter, EventKind};
13
14use crate::error::ApiError;
15use crate::response::ok_paged;
16use crate::state::AppState;
17
18#[derive(Debug, Deserialize)]
20#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
21pub struct ListAuditLogsQuery {
22 pub event_type: Option<EventKind>,
24 pub run_id: Option<Uuid>,
26 pub from: Option<DateTime<Utc>>,
28 pub to: Option<DateTime<Utc>>,
30 pub page: Option<u32>,
32 pub per_page: Option<u32>,
34}
35
36#[cfg_attr(
41 feature = "openapi",
42 utoipa::path(
43 get,
44 path = "/api/v1/audit-logs",
45 tags = ["audit"],
46 params(ListAuditLogsQuery),
47 responses(
48 (status = 200, description = "List of audit log entries with pagination", body = Vec<AuditLogEntry>),
49 (status = 401, description = "Unauthorized"),
50 (status = 403, description = "Forbidden - admin only")
51 ),
52 security(("Bearer" = []))
53 )
54)]
55pub async fn list_audit_logs(
56 auth: Authenticated,
57 State(state): State<AppState>,
58 Query(params): Query<ListAuditLogsQuery>,
59) -> Result<impl IntoResponse, ApiError> {
60 if !auth.is_admin() {
61 return Err(ApiError::Forbidden);
62 }
63
64 let page = params.page.unwrap_or(1).max(1);
65 let per_page = params.per_page.unwrap_or(50).min(100);
66
67 let filter = AuditLogFilter {
68 event_type: params.event_type,
69 run_id: params.run_id,
70 from: params.from,
71 to: params.to,
72 };
73
74 let page_result = state.store.list_audit_logs(filter, page, per_page).await?;
75
76 Ok(ok_paged(
77 page_result.items,
78 page,
79 per_page,
80 page_result.total,
81 ))
82}
83
84#[cfg(test)]
85mod tests {
86 use std::sync::Arc;
87
88 use axum::Router;
89 use axum::body::Body;
90 use axum::http::{Request, StatusCode};
91 use axum::routing::get;
92 use http_body_util::BodyExt;
93 use serde_json::{Value as JsonValue, from_slice, json};
94 use tokio::sync::broadcast;
95 use tower::ServiceExt;
96 use uuid::Uuid;
97
98 use ironflow_auth::jwt::JwtConfig;
99 use ironflow_core::providers::claude::ClaudeCodeProvider;
100 use ironflow_engine::engine::Engine;
101 use ironflow_engine::notify::Event;
102 use ironflow_store::entities::{EventKind, NewAuditLogEntry};
103 use ironflow_store::memory::InMemoryStore;
104
105 use super::*;
106 use crate::routes::test_helpers::create_user_auth_header;
107
108 fn new_test_entry(event_type: EventKind, run_id: Option<Uuid>) -> NewAuditLogEntry {
109 NewAuditLogEntry {
110 event_type,
111 payload: json!({}),
112 run_id,
113 step_id: None,
114 user_id: None,
115 }
116 }
117
118 fn test_state() -> AppState {
119 let store = Arc::new(InMemoryStore::new());
120 let provider = Arc::new(ClaudeCodeProvider::new());
121 let engine = Arc::new(Engine::new(store.clone(), provider));
122 let jwt_config = Arc::new(JwtConfig {
123 secret: "test-secret".to_string(),
124 access_token_ttl_secs: 900,
125 refresh_token_ttl_secs: 604800,
126 cookie_domain: None,
127 cookie_secure: false,
128 });
129 let (event_sender, _) = broadcast::channel::<Event>(1);
130 AppState::new(
131 store,
132 engine,
133 jwt_config,
134 "test-worker-token".to_string(),
135 event_sender,
136 )
137 }
138
139 #[tokio::test]
140 async fn empty_list() {
141 let state = test_state();
142 let auth_header = create_user_auth_header(&state, "admin", true).await;
143 let app = Router::new()
144 .route("/", get(list_audit_logs))
145 .with_state(state);
146
147 let req = Request::builder()
148 .uri("/")
149 .header("authorization", auth_header)
150 .body(Body::empty())
151 .unwrap();
152
153 let resp = app.oneshot(req).await.unwrap();
154 assert_eq!(resp.status(), StatusCode::OK);
155
156 let body = resp.into_body().collect().await.unwrap().to_bytes();
157 let json_val: JsonValue = from_slice(&body).unwrap();
158 assert_eq!(json_val["data"].as_array().unwrap().len(), 0);
159 assert_eq!(json_val["meta"]["total"], 0);
160 }
161
162 #[tokio::test]
163 async fn non_admin_gets_403() {
164 let state = test_state();
165 let auth_header = create_user_auth_header(&state, "user", false).await;
166 let app = Router::new()
167 .route("/", get(list_audit_logs))
168 .with_state(state);
169
170 let req = Request::builder()
171 .uri("/")
172 .header("authorization", auth_header)
173 .body(Body::empty())
174 .unwrap();
175
176 let resp = app.oneshot(req).await.unwrap();
177 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
178 }
179
180 #[tokio::test]
181 async fn returns_entries_with_pagination() {
182 let state = test_state();
183 let auth_header = create_user_auth_header(&state, "admin", true).await;
184
185 let kinds = [
186 EventKind::RunCreated,
187 EventKind::RunFailed,
188 EventKind::StepCompleted,
189 EventKind::StepFailed,
190 EventKind::UserSignedIn,
191 ];
192 for kind in kinds {
193 state
194 .store
195 .append_audit_log(new_test_entry(kind, None))
196 .await
197 .unwrap();
198 }
199
200 let app = Router::new()
201 .route("/", get(list_audit_logs))
202 .with_state(state);
203
204 let req = Request::builder()
205 .uri("/?page=1&per_page=2")
206 .header("authorization", auth_header)
207 .body(Body::empty())
208 .unwrap();
209
210 let resp = app.oneshot(req).await.unwrap();
211 assert_eq!(resp.status(), StatusCode::OK);
212
213 let body = resp.into_body().collect().await.unwrap().to_bytes();
214 let json_val: JsonValue = from_slice(&body).unwrap();
215 assert_eq!(json_val["data"].as_array().unwrap().len(), 2);
216 assert_eq!(json_val["meta"]["total"], 5);
217 assert_eq!(json_val["meta"]["page"], 1);
218 assert_eq!(json_val["meta"]["per_page"], 2);
219 }
220
221 #[tokio::test]
222 async fn filters_by_event_type() {
223 let state = test_state();
224 let auth_header = create_user_auth_header(&state, "admin", true).await;
225
226 state
227 .store
228 .append_audit_log(new_test_entry(EventKind::RunCreated, None))
229 .await
230 .unwrap();
231 state
232 .store
233 .append_audit_log(new_test_entry(EventKind::RunFailed, None))
234 .await
235 .unwrap();
236
237 let app = Router::new()
238 .route("/", get(list_audit_logs))
239 .with_state(state);
240
241 let req = Request::builder()
242 .uri("/?event_type=run_created")
243 .header("authorization", auth_header)
244 .body(Body::empty())
245 .unwrap();
246
247 let resp = app.oneshot(req).await.unwrap();
248 let body = resp.into_body().collect().await.unwrap().to_bytes();
249 let json_val: JsonValue = from_slice(&body).unwrap();
250 assert_eq!(json_val["data"].as_array().unwrap().len(), 1);
251 assert_eq!(json_val["data"][0]["event_type"], "run_created");
252 }
253
254 #[tokio::test]
255 async fn filters_by_run_id() {
256 let state = test_state();
257 let auth_header = create_user_auth_header(&state, "admin", true).await;
258 let target_run = Uuid::now_v7();
259
260 state
261 .store
262 .append_audit_log(new_test_entry(EventKind::RunCreated, Some(target_run)))
263 .await
264 .unwrap();
265 state
266 .store
267 .append_audit_log(new_test_entry(EventKind::RunCreated, Some(Uuid::now_v7())))
268 .await
269 .unwrap();
270
271 let app = Router::new()
272 .route("/", get(list_audit_logs))
273 .with_state(state);
274
275 let req = Request::builder()
276 .uri(format!("/?run_id={target_run}"))
277 .header("authorization", auth_header)
278 .body(Body::empty())
279 .unwrap();
280
281 let resp = app.oneshot(req).await.unwrap();
282 let body = resp.into_body().collect().await.unwrap().to_bytes();
283 let json_val: JsonValue = from_slice(&body).unwrap();
284 assert_eq!(json_val["data"].as_array().unwrap().len(), 1);
285 }
286
287 #[tokio::test]
288 async fn per_page_capped_at_100() {
289 let state = test_state();
290 let auth_header = create_user_auth_header(&state, "admin", true).await;
291 let app = Router::new()
292 .route("/", get(list_audit_logs))
293 .with_state(state);
294
295 let req = Request::builder()
296 .uri("/?per_page=500")
297 .header("authorization", auth_header)
298 .body(Body::empty())
299 .unwrap();
300
301 let resp = app.oneshot(req).await.unwrap();
302 let body = resp.into_body().collect().await.unwrap().to_bytes();
303 let json_val: JsonValue = from_slice(&body).unwrap();
304 assert_eq!(json_val["meta"]["per_page"], 100);
305 }
306}