koprogo_api/infrastructure/web/handlers/
admin_gdpr_handlers.rs

1use crate::application::dto::PageRequest;
2use crate::application::ports::{AuditLogFilters, AuditLogRepository};
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 from repository
150    let audit_repo =
151        crate::infrastructure::database::PostgresAuditLogRepository::new(data.pool.clone());
152    match audit_repo.find_all_paginated(&page_request, &filters).await {
153        Ok((logs, total)) => {
154            let total_pages = (total as f64 / per_page as f64).ceil() as i64;
155
156            let logs_dto: Vec<AuditLogDto> = logs
157                .iter()
158                .map(|log| AuditLogDto {
159                    id: log.id.to_string(),
160                    timestamp: log.timestamp.to_rfc3339(),
161                    event_type: format!("{:?}", log.event_type),
162                    user_id: log.user_id.map(|id| id.to_string()),
163                    organization_id: log.organization_id.map(|id| id.to_string()),
164                    resource_type: log.resource_type.clone(),
165                    resource_id: log.resource_id.map(|id| id.to_string()),
166                    success: log.success,
167                    error_message: log.error_message.clone(),
168                    metadata: log.metadata.clone(),
169                })
170                .collect();
171
172            HttpResponse::Ok().json(AuditLogsResponse {
173                logs: logs_dto,
174                total,
175                page,
176                per_page,
177                total_pages,
178            })
179        }
180        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
181            "error": format!("Failed to fetch audit logs: {}", e)
182        })),
183    }
184}
185
186/// Parse event_type string to AuditEventType enum
187fn parse_event_type(s: &str) -> Option<crate::infrastructure::audit::AuditEventType> {
188    use crate::infrastructure::audit::AuditEventType;
189    match s {
190        "UserLogin" => Some(AuditEventType::UserLogin),
191        "UserLogout" => Some(AuditEventType::UserLogout),
192        "UserRegistration" => Some(AuditEventType::UserRegistration),
193        "GdprDataExported" => Some(AuditEventType::GdprDataExported),
194        "GdprDataExportFailed" => Some(AuditEventType::GdprDataExportFailed),
195        "GdprDataErased" => Some(AuditEventType::GdprDataErased),
196        "GdprDataErasureFailed" => Some(AuditEventType::GdprDataErasureFailed),
197        "GdprErasureCheckRequested" => Some(AuditEventType::GdprErasureCheckRequested),
198        _ => None,
199    }
200}
201
202/// GET /api/v1/admin/gdpr/users/:id/export
203/// Export user data as SuperAdmin (for compliance requests)
204///
205/// # Returns
206/// * `200 OK` - JSON with complete user data export
207/// * `401 Unauthorized` - Missing or invalid authentication
208/// * `403 Forbidden` - User is not SuperAdmin
209/// * `404 Not Found` - User not found
210/// * `500 Internal Server Error` - Database error
211#[get("/admin/gdpr/users/{user_id}/export")]
212pub async fn admin_export_user_data(
213    req: HttpRequest,
214    data: web::Data<AppState>,
215    auth: AuthenticatedUser,
216    path: web::Path<Uuid>,
217) -> impl Responder {
218    // Only SuperAdmin can perform admin exports
219    if auth.role != "superadmin" {
220        return HttpResponse::Forbidden().json(serde_json::json!({
221            "error": "Access denied. SuperAdmin role required."
222        }));
223    }
224
225    let target_user_id = path.into_inner();
226
227    // Extract client information for audit logging
228    let ip_address = extract_ip_address(&req);
229    let user_agent = extract_user_agent(&req);
230
231    // SuperAdmin can export any user's data (no organization restriction)
232    match data
233        .gdpr_use_cases
234        .export_user_data(auth.user_id, target_user_id, None)
235        .await
236    {
237        Ok(export_data) => {
238            // Extract user info for email notification
239            let user_email = export_data.user.email.clone();
240            let user_name = format!(
241                "{} {}",
242                export_data.user.first_name, export_data.user.last_name
243            );
244            let admin_email = auth.email.clone();
245
246            // Audit log: admin-initiated export
247            let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
248                crate::infrastructure::audit::AuditEventType::GdprDataExported,
249                Some(auth.user_id),
250                auth.organization_id,
251            )
252            .with_resource("User", target_user_id)
253            .with_client_info(ip_address, user_agent)
254            .with_metadata(serde_json::json!({
255                "total_items": export_data.total_items,
256                "export_date": export_data.export_date,
257                "admin_initiated": true,
258                "target_user_id": target_user_id.to_string()
259            }));
260
261            let audit_logger = data.audit_logger.clone();
262            tokio::spawn(async move {
263                audit_logger.log(&audit_entry).await;
264            });
265
266            // Send admin notification email (async)
267            let email_service = data.email_service.clone();
268            tokio::spawn(async move {
269                if let Err(e) = email_service
270                    .send_admin_gdpr_notification(
271                        &user_email,
272                        &user_name,
273                        "Data Export",
274                        &admin_email,
275                    )
276                    .await
277                {
278                    log::error!("Failed to send admin GDPR export email notification: {}", e);
279                }
280            });
281
282            HttpResponse::Ok().json(export_data)
283        }
284        Err(e) => {
285            // Audit log: failed admin export
286            let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
287                crate::infrastructure::audit::AuditEventType::GdprDataExportFailed,
288                Some(auth.user_id),
289                auth.organization_id,
290            )
291            .with_resource("User", target_user_id)
292            .with_client_info(ip_address, user_agent)
293            .with_error(e.clone())
294            .with_metadata(serde_json::json!({
295                "admin_initiated": true,
296                "target_user_id": target_user_id.to_string()
297            }));
298
299            let audit_logger = data.audit_logger.clone();
300            tokio::spawn(async move {
301                audit_logger.log(&audit_entry).await;
302            });
303
304            if e.contains("not found") {
305                HttpResponse::NotFound().json(serde_json::json!({
306                    "error": e
307                }))
308            } else {
309                HttpResponse::InternalServerError().json(serde_json::json!({
310                    "error": format!("Failed to export user data: {}", e)
311                }))
312            }
313        }
314    }
315}
316
317/// DELETE /api/v1/admin/gdpr/users/:id/erase
318/// Erase user data as SuperAdmin (for compliance requests or account cleanup)
319///
320/// # Returns
321/// * `200 OK` - JSON confirmation of successful anonymization
322/// * `401 Unauthorized` - Missing or invalid authentication
323/// * `403 Forbidden` - User is not SuperAdmin
324/// * `404 Not Found` - User not found
325/// * `409 Conflict` - Legal holds prevent erasure
326/// * `410 Gone` - User already anonymized
327/// * `500 Internal Server Error` - Database error
328#[delete("/admin/gdpr/users/{user_id}/erase")]
329pub async fn admin_erase_user_data(
330    req: HttpRequest,
331    data: web::Data<AppState>,
332    auth: AuthenticatedUser,
333    path: web::Path<Uuid>,
334) -> impl Responder {
335    // Only SuperAdmin can perform admin erasures
336    if auth.role != "superadmin" {
337        return HttpResponse::Forbidden().json(serde_json::json!({
338            "error": "Access denied. SuperAdmin role required."
339        }));
340    }
341
342    let target_user_id = path.into_inner();
343
344    // Extract client information for audit logging
345    let ip_address = extract_ip_address(&req);
346    let user_agent = extract_user_agent(&req);
347
348    // SuperAdmin can erase any user's data (no organization restriction)
349    match data
350        .gdpr_use_cases
351        .erase_user_data(auth.user_id, target_user_id, None)
352        .await
353    {
354        Ok(erase_response) => {
355            // Extract user info for email notification
356            let user_email = erase_response.user_email.clone();
357            let user_name = format!(
358                "{} {}",
359                erase_response.user_first_name, erase_response.user_last_name
360            );
361            let admin_email = auth.email.clone();
362
363            // Audit log: admin-initiated erasure
364            let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
365                crate::infrastructure::audit::AuditEventType::GdprDataErased,
366                Some(auth.user_id),
367                auth.organization_id,
368            )
369            .with_resource("User", target_user_id)
370            .with_client_info(ip_address, user_agent)
371            .with_metadata(serde_json::json!({
372                "owners_anonymized": erase_response.owners_anonymized,
373                "anonymized_at": erase_response.anonymized_at,
374                "admin_initiated": true,
375                "target_user_id": target_user_id.to_string()
376            }));
377
378            let audit_logger = data.audit_logger.clone();
379            tokio::spawn(async move {
380                audit_logger.log(&audit_entry).await;
381            });
382
383            // Send admin notification email (async)
384            let email_service = data.email_service.clone();
385            tokio::spawn(async move {
386                if let Err(e) = email_service
387                    .send_admin_gdpr_notification(
388                        &user_email,
389                        &user_name,
390                        "Data Erasure",
391                        &admin_email,
392                    )
393                    .await
394                {
395                    log::error!(
396                        "Failed to send admin GDPR erasure email notification: {}",
397                        e
398                    );
399                }
400            });
401
402            HttpResponse::Ok().json(erase_response)
403        }
404        Err(e) => {
405            // Audit log: failed admin erasure
406            let audit_entry = crate::infrastructure::audit::AuditLogEntry::new(
407                crate::infrastructure::audit::AuditEventType::GdprDataErasureFailed,
408                Some(auth.user_id),
409                auth.organization_id,
410            )
411            .with_resource("User", target_user_id)
412            .with_client_info(ip_address, user_agent)
413            .with_error(e.clone())
414            .with_metadata(serde_json::json!({
415                "admin_initiated": true,
416                "target_user_id": target_user_id.to_string()
417            }));
418
419            let audit_logger = data.audit_logger.clone();
420            tokio::spawn(async move {
421                audit_logger.log(&audit_entry).await;
422            });
423
424            if e.contains("Unauthorized") {
425                HttpResponse::Forbidden().json(serde_json::json!({
426                    "error": e
427                }))
428            } else if e.contains("already anonymized") {
429                HttpResponse::Gone().json(serde_json::json!({
430                    "error": e
431                }))
432            } else if e.contains("legal holds") {
433                HttpResponse::Conflict().json(serde_json::json!({
434                    "error": e,
435                    "message": "Cannot erase data due to legal obligations. Please resolve pending issues before requesting erasure."
436                }))
437            } else if e.contains("not found") {
438                HttpResponse::NotFound().json(serde_json::json!({
439                    "error": e
440                }))
441            } else {
442                HttpResponse::InternalServerError().json(serde_json::json!({
443                    "error": format!("Failed to erase user data: {}", e)
444                }))
445            }
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    #[test]
453    fn test_handler_structure_list_audit_logs() {
454        // Structural test - actual testing in E2E
455    }
456
457    #[test]
458    fn test_handler_structure_admin_export() {
459        // Structural test - actual testing in E2E
460    }
461
462    #[test]
463    fn test_handler_structure_admin_erase() {
464        // Structural test - actual testing in E2E
465    }
466}