Skip to main content

koprogo_api/infrastructure/web/handlers/
admin_gdpr_handlers.rs

1use 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
9/// Extract client IP address from request
10fn extract_ip_address(req: &HttpRequest) -> Option<String> {
11    // Try X-Forwarded-For first (for proxy/load balancer scenarios)
12    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            // Try X-Real-IP header
19            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            // Fall back to peer address
26            req.peer_addr().map(|addr| addr.ip().to_string())
27        })
28}
29
30/// Extract user agent from request
31fn 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/// Query parameters for audit log filtering
39#[derive(Debug, Deserialize)]
40pub struct AuditLogQuery {
41    /// Page number (default: 1)
42    pub page: Option<i64>,
43    /// Items per page (default: 20, max: 100)
44    pub per_page: Option<i64>,
45    /// Filter by user_id
46    pub user_id: Option<Uuid>,
47    /// Filter by organization_id
48    pub organization_id: Option<Uuid>,
49    /// Filter by event_type (e.g., "GdprDataExported")
50    pub event_type: Option<String>,
51    /// Filter by success status
52    pub success: Option<bool>,
53    /// Filter by start date (ISO 8601)
54    pub start_date: Option<String>,
55    /// Filter by end date (ISO 8601)
56    pub end_date: Option<String>,
57}
58
59/// Response for paginated audit logs
60#[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/// DTO for audit log entry
70#[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 /api/v1/admin/gdpr/audit-logs
85/// List audit logs with pagination and filters (SuperAdmin only)
86///
87/// # Query Parameters
88/// - page: Page number (default: 1)
89/// - per_page: Items per page (default: 20, max: 100)
90/// - user_id: Filter by user UUID
91/// - organization_id: Filter by organization UUID
92/// - event_type: Filter by event type (e.g., "GdprDataExported")
93/// - success: Filter by success status (true/false)
94/// - start_date: Filter by start date (ISO 8601)
95/// - end_date: Filter by end date (ISO 8601)
96///
97/// # Returns
98/// * `200 OK` - Paginated audit logs
99/// * `401 Unauthorized` - Missing or invalid authentication
100/// * `403 Forbidden` - User is not SuperAdmin
101/// * `500 Internal Server Error` - Database error
102#[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    // Only SuperAdmin can view audit logs
109    if !auth.is_superadmin() {
110        return HttpResponse::Forbidden().json(serde_json::json!({
111            "error": "Access denied. SuperAdmin role required."
112        }));
113    }
114
115    // Build pagination
116    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    // Build filters
126    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    // Parse event_type string to enum
134    if let Some(ref event_type_str) = query.event_type {
135        filters.event_type = parse_event_type(event_type_str);
136    }
137
138    // Parse dates
139    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    // Fetch audit logs via use case
151    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
189/// Parse event_type string to AuditEventType enum
190fn 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 /api/v1/admin/gdpr/users/:id/export
206/// Export user data as SuperAdmin (for compliance requests)
207///
208/// # Returns
209/// * `200 OK` - JSON with complete user data export
210/// * `401 Unauthorized` - Missing or invalid authentication
211/// * `403 Forbidden` - User is not SuperAdmin
212/// * `404 Not Found` - User not found
213/// * `500 Internal Server Error` - Database error
214#[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    // Only SuperAdmin can perform admin exports
222    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    // Extract client information for audit logging
231    let ip_address = extract_ip_address(&req);
232    let user_agent = extract_user_agent(&req);
233
234    // SuperAdmin can export any user's data (no organization restriction)
235    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            // Extract user info for email notification
242            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            // Audit log: admin-initiated export
250            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            // Send admin notification email (async)
270            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            // Audit log: failed admin export
289            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 /api/v1/admin/gdpr/users/:id/erase
321/// Erase user data as SuperAdmin (for compliance requests or account cleanup)
322///
323/// # Returns
324/// * `200 OK` - JSON confirmation of successful anonymization
325/// * `401 Unauthorized` - Missing or invalid authentication
326/// * `403 Forbidden` - User is not SuperAdmin
327/// * `404 Not Found` - User not found
328/// * `409 Conflict` - Legal holds prevent erasure
329/// * `410 Gone` - User already anonymized
330/// * `500 Internal Server Error` - Database error
331#[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    // Only SuperAdmin can perform admin erasures
339    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    // Extract client information for audit logging
348    let ip_address = extract_ip_address(&req);
349    let user_agent = extract_user_agent(&req);
350
351    // SuperAdmin can erase any user's data (no organization restriction)
352    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            // Extract user info for email notification
359            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            // Audit log: admin-initiated erasure
367            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            // Send admin notification email (async)
387            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            // Audit log: failed admin erasure
409            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}