koprogo_api/infrastructure/web/handlers/
admin_gdpr_handlers.rs1use crate::application::dto::PageRequest;
2use crate::application::ports::AuditLogFilters;
3use crate::infrastructure::web::classification_erreurs;
4use crate::infrastructure::web::{AppState, AuthenticatedUser};
5use actix_web::{delete, get, web, HttpRequest, HttpResponse, Responder};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9fn extract_ip_address(req: &HttpRequest) -> Option<String> {
11 req.headers()
13 .get("X-Forwarded-For")
14 .and_then(|h| h.to_str().ok())
15 .map(|s| s.split(',').next().unwrap_or("").trim().to_string())
16 .filter(|s| !s.is_empty())
17 .or_else(|| {
18 req.headers()
20 .get("X-Real-IP")
21 .and_then(|h| h.to_str().ok())
22 .map(|s| s.to_string())
23 })
24 .or_else(|| {
25 req.peer_addr().map(|addr| addr.ip().to_string())
27 })
28}
29
30fn extract_user_agent(req: &HttpRequest) -> Option<String> {
32 req.headers()
33 .get("User-Agent")
34 .and_then(|h| h.to_str().ok())
35 .map(|s| s.to_string())
36}
37
38#[derive(Debug, Deserialize)]
40pub struct AuditLogQuery {
41 pub page: Option<i64>,
43 pub per_page: Option<i64>,
45 pub user_id: Option<Uuid>,
47 pub organization_id: Option<Uuid>,
49 pub event_type: Option<String>,
51 pub success: Option<bool>,
53 pub start_date: Option<String>,
55 pub end_date: Option<String>,
57}
58
59#[derive(Debug, Serialize)]
61pub struct AuditLogsResponse {
62 pub logs: Vec<AuditLogDto>,
63 pub total: i64,
64 pub page: i64,
65 pub per_page: i64,
66 pub total_pages: i64,
67}
68
69#[derive(Debug, Serialize)]
71pub struct AuditLogDto {
72 pub id: String,
73 pub timestamp: String,
74 pub event_type: String,
75 pub user_id: Option<String>,
76 pub organization_id: Option<String>,
77 pub resource_type: Option<String>,
78 pub resource_id: Option<String>,
79 pub success: bool,
80 pub error_message: Option<String>,
81 pub metadata: Option<serde_json::Value>,
82}
83
84#[get("/admin/gdpr/audit-logs")]
103pub async fn list_audit_logs(
104 data: web::Data<AppState>,
105 auth: AuthenticatedUser,
106 query: web::Query<AuditLogQuery>,
107) -> impl Responder {
108 if !auth.is_superadmin() {
110 return HttpResponse::Forbidden().json(serde_json::json!({
111 "error": "Access denied. SuperAdmin role required."
112 }));
113 }
114
115 let page = query.page.unwrap_or(1).max(1);
117 let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
118 let page_request = PageRequest {
119 page,
120 per_page,
121 sort_by: Some("timestamp".to_string()),
122 order: crate::application::dto::SortOrder::Desc,
123 };
124
125 let mut filters = AuditLogFilters {
127 user_id: query.user_id,
128 organization_id: query.organization_id,
129 success: query.success,
130 ..Default::default()
131 };
132
133 if let Some(ref event_type_str) = query.event_type {
135 filters.event_type = parse_event_type(event_type_str);
136 }
137
138 if let Some(ref start_date_str) = query.start_date {
140 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(start_date_str) {
141 filters.start_date = Some(dt.with_timezone(&chrono::Utc));
142 }
143 }
144 if let Some(ref end_date_str) = query.end_date {
145 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(end_date_str) {
146 filters.end_date = Some(dt.with_timezone(&chrono::Utc));
147 }
148 }
149
150 match data
152 .audit_log_use_cases
153 .find_all_paginated(&page_request, &filters)
154 .await
155 {
156 Ok((logs, total)) => {
157 let total_pages = (total as f64 / per_page as f64).ceil() as i64;
158
159 let logs_dto: Vec<AuditLogDto> = logs
160 .iter()
161 .map(|log| AuditLogDto {
162 id: log.id.to_string(),
163 timestamp: log.timestamp.to_rfc3339(),
164 event_type: format!("{:?}", log.event_type),
165 user_id: log.user_id.map(|id| id.to_string()),
166 organization_id: log.organization_id.map(|id| id.to_string()),
167 resource_type: log.resource_type.clone(),
168 resource_id: log.resource_id.map(|id| id.to_string()),
169 success: log.success,
170 error_message: log.error_message.clone(),
171 metadata: log.metadata.clone(),
172 })
173 .collect();
174
175 HttpResponse::Ok().json(AuditLogsResponse {
176 logs: logs_dto,
177 total,
178 page,
179 per_page,
180 total_pages,
181 })
182 }
183 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
184 "error": format!("Failed to fetch audit logs: {}", e)
185 })),
186 }
187}
188
189fn parse_event_type(s: &str) -> Option<crate::infrastructure::audit::AuditEventType> {
191 use crate::infrastructure::audit::AuditEventType;
192 match s {
193 "UserLogin" => Some(AuditEventType::UserLogin),
194 "UserLogout" => Some(AuditEventType::UserLogout),
195 "UserRegistration" => Some(AuditEventType::UserRegistration),
196 "GdprDataExported" => Some(AuditEventType::GdprDataExported),
197 "GdprDataExportFailed" => Some(AuditEventType::GdprDataExportFailed),
198 "GdprDataErased" => Some(AuditEventType::GdprDataErased),
199 "GdprDataErasureFailed" => Some(AuditEventType::GdprDataErasureFailed),
200 "GdprErasureCheckRequested" => Some(AuditEventType::GdprErasureCheckRequested),
201 _ => None,
202 }
203}
204
205#[get("/admin/gdpr/users/{user_id}/export")]
215pub async fn admin_export_user_data(
216 req: HttpRequest,
217 data: web::Data<AppState>,
218 auth: AuthenticatedUser,
219 path: web::Path<Uuid>,
220) -> impl Responder {
221 if !auth.is_superadmin() {
223 return HttpResponse::Forbidden().json(serde_json::json!({
224 "error": "Access denied. SuperAdmin role required."
225 }));
226 }
227
228 let target_user_id = path.into_inner();
229
230 let ip_address = extract_ip_address(&req);
232 let user_agent = extract_user_agent(&req);
233
234 match data
236 .gdpr_use_cases
237 .export_user_data(target_user_id, auth.user_id, None)
238 .await
239 {
240 Ok(export_data) => {
241 let user_email = export_data.user.email.clone();
243 let user_name = format!(
244 "{} {}",
245 export_data.user.first_name, export_data.user.last_name
246 );
247 let admin_email = auth.email.clone();
248
249 let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
251 crate::infrastructure::audit::AuditEventType::GdprDataExported,
252 Some(auth.user_id),
253 auth.organization_id,
254 )
255 .with_resource("User", target_user_id)
256 .with_client_info(ip_address, user_agent)
257 .with_metadata(serde_json::json!({
258 "total_items": export_data.total_items,
259 "export_date": export_data.export_date,
260 "admin_initiated": true,
261 "target_user_id": target_user_id.to_string()
262 }));
263
264 let audit_logger = data.audit_logger.clone();
265 tokio::spawn(async move {
266 audit_logger.log(&audit_entry).await;
267 });
268
269 let email_service = data.email_service.clone();
271 tokio::spawn(async move {
272 if let Err(e) = email_service
273 .send_admin_gdpr_notification(
274 &user_email,
275 &user_name,
276 "Data Export",
277 &admin_email,
278 )
279 .await
280 {
281 log::error!("Failed to send admin GDPR export email notification: {}", e);
282 }
283 });
284
285 HttpResponse::Ok().json(export_data)
286 }
287 Err(e) => {
288 let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
290 crate::infrastructure::audit::AuditEventType::GdprDataExportFailed,
291 Some(auth.user_id),
292 auth.organization_id,
293 )
294 .with_resource("User", target_user_id)
295 .with_client_info(ip_address, user_agent)
296 .with_error(e.clone())
297 .with_metadata(serde_json::json!({
298 "admin_initiated": true,
299 "target_user_id": target_user_id.to_string()
300 }));
301
302 let audit_logger = data.audit_logger.clone();
303 tokio::spawn(async move {
304 audit_logger.log(&audit_entry).await;
305 });
306
307 if classification_erreurs::est_introuvable(&e) {
308 HttpResponse::NotFound().json(serde_json::json!({
309 "error": e
310 }))
311 } else {
312 HttpResponse::InternalServerError().json(serde_json::json!({
313 "error": format!("Failed to export user data: {}", e)
314 }))
315 }
316 }
317 }
318}
319
320#[delete("/admin/gdpr/users/{user_id}/erase")]
332pub async fn admin_erase_user_data(
333 req: HttpRequest,
334 data: web::Data<AppState>,
335 auth: AuthenticatedUser,
336 path: web::Path<Uuid>,
337) -> impl Responder {
338 if !auth.is_superadmin() {
340 return HttpResponse::Forbidden().json(serde_json::json!({
341 "error": "Access denied. SuperAdmin role required."
342 }));
343 }
344
345 let target_user_id = path.into_inner();
346
347 let ip_address = extract_ip_address(&req);
349 let user_agent = extract_user_agent(&req);
350
351 match data
353 .gdpr_use_cases
354 .erase_user_data(target_user_id, auth.user_id, None, None)
355 .await
356 {
357 Ok(erase_response) => {
358 let user_email = erase_response.user_email.clone();
360 let user_name = format!(
361 "{} {}",
362 erase_response.user_first_name, erase_response.user_last_name
363 );
364 let admin_email = auth.email.clone();
365
366 let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
368 crate::infrastructure::audit::AuditEventType::GdprDataErased,
369 Some(auth.user_id),
370 auth.organization_id,
371 )
372 .with_resource("User", target_user_id)
373 .with_client_info(ip_address, user_agent)
374 .with_metadata(serde_json::json!({
375 "owners_anonymized": erase_response.owners_anonymized,
376 "anonymized_at": erase_response.anonymized_at,
377 "admin_initiated": true,
378 "target_user_id": target_user_id.to_string()
379 }));
380
381 let audit_logger = data.audit_logger.clone();
382 tokio::spawn(async move {
383 audit_logger.log(&audit_entry).await;
384 });
385
386 let email_service = data.email_service.clone();
388 tokio::spawn(async move {
389 if let Err(e) = email_service
390 .send_admin_gdpr_notification(
391 &user_email,
392 &user_name,
393 "Data Erasure",
394 &admin_email,
395 )
396 .await
397 {
398 log::error!(
399 "Failed to send admin GDPR erasure email notification: {}",
400 e
401 );
402 }
403 });
404
405 HttpResponse::Ok().json(erase_response)
406 }
407 Err(e) => {
408 let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
410 crate::infrastructure::audit::AuditEventType::GdprDataErasureFailed,
411 Some(auth.user_id),
412 auth.organization_id,
413 )
414 .with_resource("User", target_user_id)
415 .with_client_info(ip_address, user_agent)
416 .with_error(e.clone())
417 .with_metadata(serde_json::json!({
418 "admin_initiated": true,
419 "target_user_id": target_user_id.to_string()
420 }));
421
422 let audit_logger = data.audit_logger.clone();
423 tokio::spawn(async move {
424 audit_logger.log(&audit_entry).await;
425 });
426
427 if classification_erreurs::est_interdit(&e) {
428 HttpResponse::Forbidden().json(serde_json::json!({
429 "error": e
430 }))
431 } else if e.contains("already anonymized") {
432 HttpResponse::Gone().json(serde_json::json!({
433 "error": e
434 }))
435 } else if e.contains("legal holds") {
436 HttpResponse::Conflict().json(serde_json::json!({
437 "error": e,
438 "message": "Cannot erase data due to legal obligations. Please resolve pending issues before requesting erasure."
439 }))
440 } else if classification_erreurs::est_introuvable(&e) {
441 HttpResponse::NotFound().json(serde_json::json!({
442 "error": e
443 }))
444 } else {
445 HttpResponse::InternalServerError().json(serde_json::json!({
446 "error": format!("Failed to erase user data: {}", e)
447 }))
448 }
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_audit_log_query_defaults() {
459 let query = AuditLogQuery {
460 page: None,
461 per_page: None,
462 user_id: None,
463 organization_id: None,
464 event_type: None,
465 success: None,
466 start_date: None,
467 end_date: None,
468 };
469 let page = query.page.unwrap_or(1).max(1);
470 let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
471 assert_eq!(page, 1);
472 assert_eq!(per_page, 20);
473 }
474
475 #[test]
476 fn test_audit_log_query_per_page_clamped() {
477 let query = AuditLogQuery {
478 page: Some(2),
479 per_page: Some(500),
480 user_id: None,
481 organization_id: None,
482 event_type: None,
483 success: None,
484 start_date: None,
485 end_date: None,
486 };
487 let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
488 assert_eq!(per_page, 100);
489 }
490
491 #[test]
492 fn test_parse_event_type_known() {
493 assert!(parse_event_type("UserLogin").is_some());
494 assert!(parse_event_type("GdprDataExported").is_some());
495 assert!(parse_event_type("GdprDataErased").is_some());
496 }
497
498 #[test]
499 fn test_parse_event_type_unknown_returns_none() {
500 assert!(parse_event_type("UnknownEvent").is_none());
501 assert!(parse_event_type("").is_none());
502 }
503}