koprogo_api/infrastructure/web/handlers/
admin_gdpr_handlers.rs

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