koprogo_api/infrastructure/web/handlers/
gdpr_handlers.rs

1use crate::application::dto::{
2    GdprActionResponse, GdprMarketingPreferenceRequest, GdprRectifyRequest,
3    GdprRestrictProcessingRequest,
4};
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, put, web, HttpRequest, HttpResponse, Responder};
8use chrono::Utc;
9use tokio::spawn;
10
11/// Extract client IP address from request
12fn extract_ip_address(req: &HttpRequest) -> Option<String> {
13    // Try X-Forwarded-For first (for proxy/load balancer scenarios)
14    req.headers()
15        .get("X-Forwarded-For")
16        .and_then(|h| h.to_str().ok())
17        .map(|s| s.split(',').next().unwrap_or("").trim().to_string())
18        .filter(|s| !s.is_empty())
19        .or_else(|| {
20            // Try X-Real-IP header
21            req.headers()
22                .get("X-Real-IP")
23                .and_then(|h| h.to_str().ok())
24                .map(|s| s.to_string())
25        })
26        .or_else(|| {
27            // Fall back to peer address
28            req.peer_addr().map(|addr| addr.ip().to_string())
29        })
30}
31
32/// Extract user agent from request
33fn extract_user_agent(req: &HttpRequest) -> Option<String> {
34    req.headers()
35        .get("User-Agent")
36        .and_then(|h| h.to_str().ok())
37        .map(|s| s.to_string())
38}
39
40/// GET /api/v1/gdpr/export
41/// Export all personal data for the authenticated user (GDPR Article 15 - Right to Access)
42///
43/// # Returns
44/// * `200 OK` - JSON with complete user data export
45/// * `401 Unauthorized` - Missing or invalid authentication
46/// * `403 Forbidden` - User attempting to export another user's data
47/// * `404 Not Found` - User not found
48/// * `500 Internal Server Error` - Database or processing error
49#[get("/gdpr/export")]
50pub async fn export_user_data(
51    req: HttpRequest,
52    data: web::Data<AppState>,
53    auth: AuthenticatedUser,
54) -> impl Responder {
55    // Extract user_id from authenticated user
56    let user_id = auth.user_id;
57
58    // Extract client information for audit logging
59    let ip_address = extract_ip_address(&req);
60    let user_agent = extract_user_agent(&req);
61
62    // Determine organization scope based on role
63    // SuperAdmin can export across all organizations (organization_id = None)
64    // Regular users are scoped to their organization
65    let organization_id = if auth.role == "superadmin" {
66        None
67    } else {
68        auth.organization_id
69    };
70
71    // Call use case to export data
72    match data
73        .gdpr_use_cases
74        .export_user_data(user_id, user_id, organization_id)
75        .await
76    {
77        Ok(export_data) => {
78            // Extract user info for email notification
79            let user_email = export_data.user.email.clone();
80            let user_name = format!(
81                "{} {}",
82                export_data.user.first_name, export_data.user.last_name
83            );
84
85            // Audit log: successful GDPR data export (async with database persistence)
86            let audit_entry = AuditLogEntry::new(
87                AuditEventType::GdprDataExported,
88                Some(user_id),
89                organization_id,
90            )
91            .with_resource("User", user_id)
92            .with_client_info(ip_address, user_agent)
93            .with_metadata(serde_json::json!({
94                "total_items": export_data.total_items,
95                "export_date": export_data.export_date
96            }));
97
98            let audit_logger = data.audit_logger.clone();
99            spawn(async move {
100                audit_logger.log(&audit_entry).await;
101            });
102
103            // Send email notification (async)
104            let email_service = data.email_service.clone();
105            spawn(async move {
106                if let Err(e) = email_service
107                    .send_gdpr_export_notification(&user_email, &user_name, user_id)
108                    .await
109                {
110                    log::error!("Failed to send GDPR export email notification: {}", e);
111                }
112            });
113
114            HttpResponse::Ok().json(export_data)
115        }
116        Err(e) => {
117            // Audit log: failed GDPR data export (async with database persistence)
118            let audit_entry = AuditLogEntry::new(
119                AuditEventType::GdprDataExportFailed,
120                Some(user_id),
121                organization_id,
122            )
123            .with_resource("User", user_id)
124            .with_client_info(ip_address, user_agent)
125            .with_error(e.clone());
126
127            let audit_logger = data.audit_logger.clone();
128            spawn(async move {
129                audit_logger.log(&audit_entry).await;
130            });
131
132            if e.contains("not found") {
133                HttpResponse::NotFound().json(serde_json::json!({
134                    "error": e
135                }))
136            } else if e.contains("Unauthorized") {
137                HttpResponse::Forbidden().json(serde_json::json!({
138                    "error": e
139                }))
140            } else if e.contains("anonymized") {
141                HttpResponse::Gone().json(serde_json::json!({
142                    "error": e
143                }))
144            } else {
145                HttpResponse::InternalServerError().json(serde_json::json!({
146                    "error": format!("Failed to export user data: {}", e)
147                }))
148            }
149        }
150    }
151}
152
153/// DELETE /api/v1/gdpr/erase
154/// Erase user personal data by anonymization (GDPR Article 17 - Right to Erasure)
155///
156/// This endpoint anonymizes the user's account and all linked owner profiles.
157/// Data is not deleted entirely to preserve referential integrity and comply with
158/// legal retention requirements (e.g., financial records must be kept for 7 years).
159///
160/// # Returns
161/// * `200 OK` - JSON confirmation of successful anonymization
162/// * `401 Unauthorized` - Missing or invalid authentication
163/// * `403 Forbidden` - User attempting to erase another user's data
164/// * `409 Conflict` - Legal holds prevent erasure (e.g., unpaid expenses)
165/// * `410 Gone` - User already anonymized
166/// * `500 Internal Server Error` - Database or processing error
167#[delete("/gdpr/erase")]
168pub async fn erase_user_data(
169    req: HttpRequest,
170    data: web::Data<AppState>,
171    auth: AuthenticatedUser,
172) -> impl Responder {
173    // Extract user_id from authenticated user
174    let user_id = auth.user_id;
175
176    // Extract client information for audit logging
177    let ip_address = extract_ip_address(&req);
178    let user_agent = extract_user_agent(&req);
179
180    // Determine organization scope based on role
181    let organization_id = if auth.role == "superadmin" {
182        None
183    } else {
184        auth.organization_id
185    };
186
187    // Call use case to erase data
188    match data
189        .gdpr_use_cases
190        .erase_user_data(user_id, user_id, organization_id)
191        .await
192    {
193        Ok(erase_response) => {
194            // Extract user info for email notification
195            let user_email = erase_response.user_email.clone();
196            let user_name = format!(
197                "{} {}",
198                erase_response.user_first_name, erase_response.user_last_name
199            );
200            let owners_count = erase_response.owners_anonymized;
201
202            // Audit log: successful GDPR data erasure (async with database persistence)
203            let audit_entry = AuditLogEntry::new(
204                AuditEventType::GdprDataErased,
205                Some(user_id),
206                organization_id,
207            )
208            .with_resource("User", user_id)
209            .with_client_info(ip_address, user_agent)
210            .with_metadata(serde_json::json!({
211                "owners_anonymized": erase_response.owners_anonymized,
212                "anonymized_at": erase_response.anonymized_at
213            }));
214
215            let audit_logger = data.audit_logger.clone();
216            spawn(async move {
217                audit_logger.log(&audit_entry).await;
218            });
219
220            // Send email notification (async)
221            let email_service = data.email_service.clone();
222            spawn(async move {
223                if let Err(e) = email_service
224                    .send_gdpr_erasure_notification(&user_email, &user_name, owners_count)
225                    .await
226                {
227                    log::error!("Failed to send GDPR erasure email notification: {}", e);
228                }
229            });
230
231            HttpResponse::Ok().json(erase_response)
232        }
233        Err(e) => {
234            // Audit log: failed GDPR data erasure (async with database persistence)
235            let audit_entry = AuditLogEntry::new(
236                AuditEventType::GdprDataErasureFailed,
237                Some(user_id),
238                organization_id,
239            )
240            .with_resource("User", user_id)
241            .with_client_info(ip_address, user_agent)
242            .with_error(e.clone());
243
244            let audit_logger = data.audit_logger.clone();
245            spawn(async move {
246                audit_logger.log(&audit_entry).await;
247            });
248
249            if e.contains("Unauthorized") {
250                HttpResponse::Forbidden().json(serde_json::json!({
251                    "error": e
252                }))
253            } else if e.contains("already anonymized") {
254                HttpResponse::Gone().json(serde_json::json!({
255                    "error": e
256                }))
257            } else if e.contains("legal holds") {
258                HttpResponse::Conflict().json(serde_json::json!({
259                    "error": e,
260                    "message": "Cannot erase data due to legal obligations. Please resolve pending issues before requesting erasure."
261                }))
262            } else if e.contains("not found") {
263                HttpResponse::NotFound().json(serde_json::json!({
264                    "error": e
265                }))
266            } else {
267                HttpResponse::InternalServerError().json(serde_json::json!({
268                    "error": format!("Failed to erase user data: {}", e)
269                }))
270            }
271        }
272    }
273}
274
275/// GET /api/v1/gdpr/can-erase
276/// Check if user data can be erased (no legal holds)
277///
278/// # Returns
279/// * `200 OK` - JSON with erasure eligibility status
280/// * `401 Unauthorized` - Missing or invalid authentication
281/// * `500 Internal Server Error` - Database or processing error
282#[get("/gdpr/can-erase")]
283pub async fn can_erase_user(
284    req: HttpRequest,
285    data: web::Data<AppState>,
286    auth: AuthenticatedUser,
287) -> impl Responder {
288    let user_id = auth.user_id;
289
290    // Extract client information for audit logging
291    let ip_address = extract_ip_address(&req);
292    let user_agent = extract_user_agent(&req);
293
294    match data.gdpr_use_cases.can_erase_user(user_id).await {
295        Ok(can_erase) => {
296            // Audit log: erasure check requested (async with database persistence)
297            let audit_entry = AuditLogEntry::new(
298                AuditEventType::GdprErasureCheckRequested,
299                Some(user_id),
300                auth.organization_id,
301            )
302            .with_resource("User", user_id)
303            .with_client_info(ip_address, user_agent)
304            .with_metadata(serde_json::json!({
305                "can_erase": can_erase
306            }));
307
308            let audit_logger = data.audit_logger.clone();
309            spawn(async move {
310                audit_logger.log(&audit_entry).await;
311            });
312
313            HttpResponse::Ok().json(serde_json::json!({
314                "can_erase": can_erase,
315                "user_id": user_id.to_string()
316            }))
317        }
318        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
319            "error": format!("Failed to check erasure eligibility: {}", e)
320        })),
321    }
322}
323
324/// PUT /api/v1/gdpr/rectify
325/// Rectify user personal data (GDPR Article 16 - Right to Rectification)
326///
327/// Allows users to correct inaccurate or incomplete personal data.
328///
329/// # Request Body
330/// ```json
331/// {
332///   "email": "new@example.com",        // Optional
333///   "first_name": "Jane",              // Optional
334///   "last_name": "Doe"                 // Optional
335/// }
336/// ```
337///
338/// # Returns
339/// * `200 OK` - Data successfully rectified
340/// * `400 Bad Request` - Validation error (e.g., invalid email)
341/// * `401 Unauthorized` - Missing or invalid authentication
342/// * `403 Forbidden` - User attempting to rectify another user's data
343/// * `404 Not Found` - User not found
344/// * `500 Internal Server Error` - Database or processing error
345#[put("/gdpr/rectify")]
346pub async fn rectify_user_data(
347    req: HttpRequest,
348    data: web::Data<AppState>,
349    auth: AuthenticatedUser,
350    request: web::Json<GdprRectifyRequest>,
351) -> impl Responder {
352    let user_id = auth.user_id;
353
354    // Extract client information for audit logging
355    let ip_address = extract_ip_address(&req);
356    let user_agent = extract_user_agent(&req);
357
358    // Call use case to rectify data
359    match data
360        .gdpr_use_cases
361        .rectify_user_data(
362            user_id,
363            user_id, // Users can only rectify their own data
364            request.email.clone(),
365            request.first_name.clone(),
366            request.last_name.clone(),
367        )
368        .await
369    {
370        Ok(_) => {
371            // Audit log: successful data rectification (async with database persistence)
372            let audit_entry = AuditLogEntry::new(
373                AuditEventType::GdprDataRectified,
374                Some(user_id),
375                auth.organization_id,
376            )
377            .with_resource("User", user_id)
378            .with_client_info(ip_address, user_agent)
379            .with_metadata(serde_json::json!({
380                "fields_updated": {
381                    "email": request.email.is_some(),
382                    "first_name": request.first_name.is_some(),
383                    "last_name": request.last_name.is_some()
384                }
385            }));
386
387            let audit_logger = data.audit_logger.clone();
388            spawn(async move {
389                audit_logger.log(&audit_entry).await;
390            });
391
392            let response = GdprActionResponse {
393                success: true,
394                message: "Personal data successfully rectified".to_string(),
395                updated_at: Utc::now().to_rfc3339(),
396            };
397
398            HttpResponse::Ok().json(response)
399        }
400        Err(e) => {
401            // Audit log: failed data rectification
402            let audit_entry = AuditLogEntry::new(
403                AuditEventType::GdprDataRectificationFailed,
404                Some(user_id),
405                auth.organization_id,
406            )
407            .with_resource("User", user_id)
408            .with_client_info(ip_address, user_agent)
409            .with_error(e.clone());
410
411            let audit_logger = data.audit_logger.clone();
412            spawn(async move {
413                audit_logger.log(&audit_entry).await;
414            });
415
416            if e.contains("Unauthorized") {
417                HttpResponse::Forbidden().json(serde_json::json!({
418                    "error": e
419                }))
420            } else if e.contains("not found") {
421                HttpResponse::NotFound().json(serde_json::json!({
422                    "error": e
423                }))
424            } else if e.contains("Validation error") {
425                HttpResponse::BadRequest().json(serde_json::json!({
426                    "error": e
427                }))
428            } else {
429                HttpResponse::InternalServerError().json(serde_json::json!({
430                    "error": format!("Failed to rectify user data: {}", e)
431                }))
432            }
433        }
434    }
435}
436
437/// PUT /api/v1/gdpr/restrict-processing
438/// Restrict data processing (GDPR Article 18 - Right to Restriction of Processing)
439///
440/// Allows users to request temporary limitation of data processing.
441/// When processing is restricted:
442/// - Data is stored but not processed for certain operations
443/// - Marketing communications are blocked
444/// - Profiling/analytics are disabled
445///
446/// # Returns
447/// * `200 OK` - Processing restriction applied
448/// * `400 Bad Request` - Processing already restricted
449/// * `401 Unauthorized` - Missing or invalid authentication
450/// * `403 Forbidden` - User attempting to restrict another user's processing
451/// * `404 Not Found` - User not found
452/// * `500 Internal Server Error` - Database or processing error
453#[put("/gdpr/restrict-processing")]
454pub async fn restrict_user_processing(
455    req: HttpRequest,
456    data: web::Data<AppState>,
457    auth: AuthenticatedUser,
458    _request: web::Json<GdprRestrictProcessingRequest>,
459) -> impl Responder {
460    let user_id = auth.user_id;
461
462    // Extract client information for audit logging
463    let ip_address = extract_ip_address(&req);
464    let user_agent = extract_user_agent(&req);
465
466    // Call use case to restrict processing
467    match data
468        .gdpr_use_cases
469        .restrict_user_processing(user_id, user_id)
470        .await
471    {
472        Ok(_) => {
473            // Audit log: successful processing restriction (async with database persistence)
474            let audit_entry = AuditLogEntry::new(
475                AuditEventType::GdprProcessingRestricted,
476                Some(user_id),
477                auth.organization_id,
478            )
479            .with_resource("User", user_id)
480            .with_client_info(ip_address, user_agent);
481
482            let audit_logger = data.audit_logger.clone();
483            spawn(async move {
484                audit_logger.log(&audit_entry).await;
485            });
486
487            let response = GdprActionResponse {
488                success: true,
489                message: "Data processing successfully restricted. Your data will be stored but not processed for certain operations.".to_string(),
490                updated_at: Utc::now().to_rfc3339(),
491            };
492
493            HttpResponse::Ok().json(response)
494        }
495        Err(e) => {
496            // Audit log: failed processing restriction
497            let audit_entry = AuditLogEntry::new(
498                AuditEventType::GdprProcessingRestrictionFailed,
499                Some(user_id),
500                auth.organization_id,
501            )
502            .with_resource("User", user_id)
503            .with_client_info(ip_address, user_agent)
504            .with_error(e.clone());
505
506            let audit_logger = data.audit_logger.clone();
507            spawn(async move {
508                audit_logger.log(&audit_entry).await;
509            });
510
511            if e.contains("Unauthorized") {
512                HttpResponse::Forbidden().json(serde_json::json!({
513                    "error": e
514                }))
515            } else if e.contains("not found") {
516                HttpResponse::NotFound().json(serde_json::json!({
517                    "error": e
518                }))
519            } else if e.contains("already restricted") {
520                HttpResponse::BadRequest().json(serde_json::json!({
521                    "error": e
522                }))
523            } else {
524                HttpResponse::InternalServerError().json(serde_json::json!({
525                    "error": format!("Failed to restrict processing: {}", e)
526                }))
527            }
528        }
529    }
530}
531
532/// PUT /api/v1/gdpr/marketing-preference
533/// Set marketing opt-out preference (GDPR Article 21 - Right to Object)
534///
535/// Allows users to object to marketing communications and profiling.
536///
537/// # Request Body
538/// ```json
539/// {
540///   "opt_out": true  // true to opt out, false to opt back in
541/// }
542/// ```
543///
544/// # Returns
545/// * `200 OK` - Marketing preference updated
546/// * `401 Unauthorized` - Missing or invalid authentication
547/// * `403 Forbidden` - User attempting to change another user's preferences
548/// * `404 Not Found` - User not found
549/// * `500 Internal Server Error` - Database or processing error
550#[put("/gdpr/marketing-preference")]
551pub async fn set_marketing_preference(
552    req: HttpRequest,
553    data: web::Data<AppState>,
554    auth: AuthenticatedUser,
555    request: web::Json<GdprMarketingPreferenceRequest>,
556) -> impl Responder {
557    let user_id = auth.user_id;
558
559    // Extract client information for audit logging
560    let ip_address = extract_ip_address(&req);
561    let user_agent = extract_user_agent(&req);
562
563    let opt_out = request.opt_out;
564
565    // Call use case to set marketing preference
566    match data
567        .gdpr_use_cases
568        .set_marketing_preference(user_id, user_id, opt_out)
569        .await
570    {
571        Ok(_) => {
572            // Audit log: marketing preference change (async with database persistence)
573            let event_type = if opt_out {
574                AuditEventType::GdprMarketingOptOut
575            } else {
576                AuditEventType::GdprMarketingOptIn
577            };
578
579            let audit_entry = AuditLogEntry::new(event_type, Some(user_id), auth.organization_id)
580                .with_resource("User", user_id)
581                .with_client_info(ip_address, user_agent)
582                .with_metadata(serde_json::json!({
583                    "opt_out": opt_out
584                }));
585
586            let audit_logger = data.audit_logger.clone();
587            spawn(async move {
588                audit_logger.log(&audit_entry).await;
589            });
590
591            let message = if opt_out {
592                "You have successfully opted out of marketing communications. You will no longer receive promotional emails or offers."
593            } else {
594                "You have successfully opted back in to marketing communications. You will receive promotional emails and offers."
595            };
596
597            let response = GdprActionResponse {
598                success: true,
599                message: message.to_string(),
600                updated_at: Utc::now().to_rfc3339(),
601            };
602
603            HttpResponse::Ok().json(response)
604        }
605        Err(e) => {
606            // Audit log: failed marketing preference change
607            let audit_entry = AuditLogEntry::new(
608                AuditEventType::GdprMarketingPreferenceChangeFailed,
609                Some(user_id),
610                auth.organization_id,
611            )
612            .with_resource("User", user_id)
613            .with_client_info(ip_address, user_agent)
614            .with_error(e.clone());
615
616            let audit_logger = data.audit_logger.clone();
617            spawn(async move {
618                audit_logger.log(&audit_entry).await;
619            });
620
621            if e.contains("Unauthorized") {
622                HttpResponse::Forbidden().json(serde_json::json!({
623                    "error": e
624                }))
625            } else if e.contains("not found") {
626                HttpResponse::NotFound().json(serde_json::json!({
627                    "error": e
628                }))
629            } else {
630                HttpResponse::InternalServerError().json(serde_json::json!({
631                    "error": format!("Failed to set marketing preference: {}", e)
632                }))
633            }
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    // Note: Full integration tests with actual AppState would require proper initialization
641    // of all use cases. These handler tests are covered by E2E tests in tests/e2e/
642
643    #[test]
644    fn test_handler_structure_export() {
645        // This test just verifies the handler function signature compiles
646        // Real testing happens in E2E tests with testcontainers
647    }
648
649    #[test]
650    fn test_handler_structure_erase() {
651        // This test just verifies the handler function signature compiles
652    }
653
654    #[test]
655    fn test_handler_structure_can_erase() {
656        // This test just verifies the handler function signature compiles
657    }
658}