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::{AccessToken, 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
107 fn new_test_entry(event_type: EventKind, run_id: Option<Uuid>) -> NewAuditLogEntry {
108 NewAuditLogEntry {
109 event_type,
110 payload: json!({}),
111 run_id,
112 step_id: None,
113 user_id: None,
114 }
115 }
116
117 fn test_state() -> AppState {
118 let store = Arc::new(InMemoryStore::new());
119 let provider = Arc::new(ClaudeCodeProvider::new());
120 let engine = Arc::new(Engine::new(store.clone(), provider));
121 let jwt_config = Arc::new(JwtConfig {
122 secret: "test-secret".to_string(),
123 access_token_ttl_secs: 900,
124 refresh_token_ttl_secs: 604800,
125 cookie_domain: None,
126 cookie_secure: false,
127 });
128 let (event_sender, _) = broadcast::channel::<Event>(1);
129 AppState::new(
130 store,
131 engine,
132 jwt_config,
133 "test-worker-token".to_string(),
134 event_sender,
135 )
136 }
137
138 fn make_admin_auth_header(state: &AppState) -> String {
139 let user_id = Uuid::now_v7();
140 let token = AccessToken::for_user(user_id, "admin", true, &state.jwt_config).unwrap();
141 format!("Bearer {}", token.0)
142 }
143
144 fn make_user_auth_header(state: &AppState) -> String {
145 let user_id = Uuid::now_v7();
146 let token = AccessToken::for_user(user_id, "user", false, &state.jwt_config).unwrap();
147 format!("Bearer {}", token.0)
148 }
149
150 #[tokio::test]
151 async fn empty_list() {
152 let state = test_state();
153 let auth_header = make_admin_auth_header(&state);
154 let app = Router::new()
155 .route("/", get(list_audit_logs))
156 .with_state(state);
157
158 let req = Request::builder()
159 .uri("/")
160 .header("authorization", auth_header)
161 .body(Body::empty())
162 .unwrap();
163
164 let resp = app.oneshot(req).await.unwrap();
165 assert_eq!(resp.status(), StatusCode::OK);
166
167 let body = resp.into_body().collect().await.unwrap().to_bytes();
168 let json_val: JsonValue = from_slice(&body).unwrap();
169 assert_eq!(json_val["data"].as_array().unwrap().len(), 0);
170 assert_eq!(json_val["meta"]["total"], 0);
171 }
172
173 #[tokio::test]
174 async fn non_admin_gets_403() {
175 let state = test_state();
176 let auth_header = make_user_auth_header(&state);
177 let app = Router::new()
178 .route("/", get(list_audit_logs))
179 .with_state(state);
180
181 let req = Request::builder()
182 .uri("/")
183 .header("authorization", auth_header)
184 .body(Body::empty())
185 .unwrap();
186
187 let resp = app.oneshot(req).await.unwrap();
188 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
189 }
190
191 #[tokio::test]
192 async fn returns_entries_with_pagination() {
193 let state = test_state();
194 let auth_header = make_admin_auth_header(&state);
195
196 let kinds = [
197 EventKind::RunCreated,
198 EventKind::RunFailed,
199 EventKind::StepCompleted,
200 EventKind::StepFailed,
201 EventKind::UserSignedIn,
202 ];
203 for kind in kinds {
204 state
205 .store
206 .append_audit_log(new_test_entry(kind, None))
207 .await
208 .unwrap();
209 }
210
211 let app = Router::new()
212 .route("/", get(list_audit_logs))
213 .with_state(state);
214
215 let req = Request::builder()
216 .uri("/?page=1&per_page=2")
217 .header("authorization", auth_header)
218 .body(Body::empty())
219 .unwrap();
220
221 let resp = app.oneshot(req).await.unwrap();
222 assert_eq!(resp.status(), StatusCode::OK);
223
224 let body = resp.into_body().collect().await.unwrap().to_bytes();
225 let json_val: JsonValue = from_slice(&body).unwrap();
226 assert_eq!(json_val["data"].as_array().unwrap().len(), 2);
227 assert_eq!(json_val["meta"]["total"], 5);
228 assert_eq!(json_val["meta"]["page"], 1);
229 assert_eq!(json_val["meta"]["per_page"], 2);
230 }
231
232 #[tokio::test]
233 async fn filters_by_event_type() {
234 let state = test_state();
235 let auth_header = make_admin_auth_header(&state);
236
237 state
238 .store
239 .append_audit_log(new_test_entry(EventKind::RunCreated, None))
240 .await
241 .unwrap();
242 state
243 .store
244 .append_audit_log(new_test_entry(EventKind::RunFailed, None))
245 .await
246 .unwrap();
247
248 let app = Router::new()
249 .route("/", get(list_audit_logs))
250 .with_state(state);
251
252 let req = Request::builder()
253 .uri("/?event_type=run_created")
254 .header("authorization", auth_header)
255 .body(Body::empty())
256 .unwrap();
257
258 let resp = app.oneshot(req).await.unwrap();
259 let body = resp.into_body().collect().await.unwrap().to_bytes();
260 let json_val: JsonValue = from_slice(&body).unwrap();
261 assert_eq!(json_val["data"].as_array().unwrap().len(), 1);
262 assert_eq!(json_val["data"][0]["event_type"], "run_created");
263 }
264
265 #[tokio::test]
266 async fn filters_by_run_id() {
267 let state = test_state();
268 let auth_header = make_admin_auth_header(&state);
269 let target_run = Uuid::now_v7();
270
271 state
272 .store
273 .append_audit_log(new_test_entry(EventKind::RunCreated, Some(target_run)))
274 .await
275 .unwrap();
276 state
277 .store
278 .append_audit_log(new_test_entry(EventKind::RunCreated, Some(Uuid::now_v7())))
279 .await
280 .unwrap();
281
282 let app = Router::new()
283 .route("/", get(list_audit_logs))
284 .with_state(state);
285
286 let req = Request::builder()
287 .uri(format!("/?run_id={target_run}"))
288 .header("authorization", auth_header)
289 .body(Body::empty())
290 .unwrap();
291
292 let resp = app.oneshot(req).await.unwrap();
293 let body = resp.into_body().collect().await.unwrap().to_bytes();
294 let json_val: JsonValue = from_slice(&body).unwrap();
295 assert_eq!(json_val["data"].as_array().unwrap().len(), 1);
296 }
297
298 #[tokio::test]
299 async fn per_page_capped_at_100() {
300 let state = test_state();
301 let auth_header = make_admin_auth_header(&state);
302 let app = Router::new()
303 .route("/", get(list_audit_logs))
304 .with_state(state);
305
306 let req = Request::builder()
307 .uri("/?per_page=500")
308 .header("authorization", auth_header)
309 .body(Body::empty())
310 .unwrap();
311
312 let resp = app.oneshot(req).await.unwrap();
313 let body = resp.into_body().collect().await.unwrap().to_bytes();
314 let json_val: JsonValue = from_slice(&body).unwrap();
315 assert_eq!(json_val["meta"]["per_page"], 100);
316 }
317}