koprogo_api/infrastructure/web/handlers/
admin_gdpr_handlers.rs1use 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
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 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
186fn 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("/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 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 let ip_address = extract_ip_address(&req);
229 let user_agent = extract_user_agent(&req);
230
231 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 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 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 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 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("/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 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 let ip_address = extract_ip_address(&req);
346 let user_agent = extract_user_agent(&req);
347
348 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 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 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 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 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 }
456
457 #[test]
458 fn test_handler_structure_admin_export() {
459 }
461
462 #[test]
463 fn test_handler_structure_admin_erase() {
464 }
466}