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