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