koprogo_api/infrastructure/web/handlers/
consent_handlers.rs

1use crate::infrastructure::web::{AppState, AuthenticatedUser};
2use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
3use serde::{Deserialize, Serialize};
4
5/// Extract client IP address from request
6fn extract_ip_address(req: &HttpRequest) -> Option<String> {
7    // Try X-Forwarded-For first (for proxy/load balancer scenarios)
8    req.headers()
9        .get("X-Forwarded-For")
10        .and_then(|h| h.to_str().ok())
11        .map(|s| s.split(',').next().unwrap_or("").trim().to_string())
12        .filter(|s| !s.is_empty())
13        .or_else(|| {
14            // Try X-Real-IP header
15            req.headers()
16                .get("X-Real-IP")
17                .and_then(|h| h.to_str().ok())
18                .map(|s| s.to_string())
19        })
20        .or_else(|| {
21            // Fall back to peer address
22            req.peer_addr().map(|addr| addr.ip().to_string())
23        })
24}
25
26/// Extract user agent from request
27fn extract_user_agent(req: &HttpRequest) -> Option<String> {
28    req.headers()
29        .get("User-Agent")
30        .and_then(|h| h.to_str().ok())
31        .map(|s| s.to_string())
32}
33
34/// Request body for recording user consent
35#[derive(Debug, Deserialize, utoipa::ToSchema)]
36pub struct RecordConsentRequest {
37    /// Type of consent: 'privacy_policy' or 'terms'
38    pub consent_type: String,
39    /// Optional policy version (e.g., "1.0", "1.1")
40    #[serde(default)]
41    pub policy_version: Option<String>,
42}
43
44/// Response for consent status check
45#[derive(Debug, Serialize, utoipa::ToSchema)]
46pub struct ConsentStatusResponse {
47    /// Whether the user has given consent to privacy policy
48    pub privacy_policy_accepted: bool,
49    /// Whether the user has given consent to terms
50    pub terms_accepted: bool,
51    /// Timestamp of latest privacy policy consent (if given)
52    pub privacy_policy_accepted_at: Option<String>,
53    /// Timestamp of latest terms consent (if given)
54    pub terms_accepted_at: Option<String>,
55    /// User ID
56    pub user_id: String,
57}
58
59/// Response for successful consent recording
60#[derive(Debug, Serialize, utoipa::ToSchema)]
61pub struct ConsentRecordedResponse {
62    /// Success message
63    pub message: String,
64    /// Consent type that was recorded
65    pub consent_type: String,
66    /// Timestamp when consent was recorded
67    pub accepted_at: String,
68}
69
70/// POST /api/v1/consent
71/// Record user consent to privacy policy or terms of service
72///
73/// Requires JWT authentication. Records the consent in the database with
74/// audit trail (IP address, user agent, timestamp) for GDPR Art. 7 / Art. 13-14 compliance.
75///
76/// # Parameters
77/// * `consent_type` - Type of consent: "privacy_policy" or "terms"
78/// * `policy_version` - Optional version of the policy (e.g., "1.0")
79///
80/// # Returns
81/// * `200 OK` - Consent recorded successfully
82/// * `400 Bad Request` - Invalid consent_type or request body
83/// * `401 Unauthorized` - Missing or invalid authentication
84/// * `500 Internal Server Error` - Database error
85#[utoipa::path(
86    post,
87    path = "/consent",
88    tag = "GDPR",
89    summary = "Record user consent to privacy policy or terms",
90    request_body = RecordConsentRequest,
91    responses(
92        (status = 200, description = "Consent recorded", body = ConsentRecordedResponse),
93        (status = 400, description = "Bad request"),
94        (status = 401, description = "Unauthorized"),
95        (status = 500, description = "Internal server error"),
96    ),
97    security(("bearer_auth" = []))
98)]
99#[post("/consent")]
100pub async fn record_consent(
101    req: HttpRequest,
102    data: web::Data<AppState>,
103    auth: AuthenticatedUser,
104    body: web::Json<RecordConsentRequest>,
105) -> impl Responder {
106    // Extract client information for audit trail
107    let ip_address = extract_ip_address(&req);
108    let user_agent = extract_user_agent(&req);
109
110    // Get organization_id (required for consent records)
111    let organization_id = match auth.require_organization() {
112        Ok(org_id) => org_id,
113        Err(_) => {
114            return HttpResponse::BadRequest().json(serde_json::json!({
115                "error": "Organization context required to record consent"
116            }));
117        }
118    };
119
120    // Call use case (validates consent_type, persists, creates audit trail)
121    match data
122        .consent_use_cases
123        .record_consent(
124            auth.user_id,
125            organization_id,
126            &body.consent_type,
127            ip_address,
128            user_agent,
129            body.policy_version.clone(),
130        )
131        .await
132    {
133        Ok(response) => HttpResponse::Ok().json(ConsentRecordedResponse {
134            message: response.message,
135            consent_type: response.consent_type,
136            accepted_at: response.accepted_at.to_rfc3339(),
137        }),
138        Err(e) if e.contains("Invalid consent type") => {
139            HttpResponse::BadRequest().json(serde_json::json!({ "error": e }))
140        }
141        Err(e) => {
142            log::error!("Failed to record consent: {}", e);
143            HttpResponse::InternalServerError().json(serde_json::json!({ "error": e }))
144        }
145    }
146}
147
148/// GET /api/v1/consent/status
149/// Check current consent status for the authenticated user
150///
151/// Returns which types of consent the user has given (privacy policy and/or terms).
152/// Useful for conditional display of consent modals in the frontend.
153///
154/// # Returns
155/// * `200 OK` - Consent status returned
156/// * `401 Unauthorized` - Missing or invalid authentication
157/// * `500 Internal Server Error` - Database error
158#[utoipa::path(
159    get,
160    path = "/consent/status",
161    tag = "GDPR",
162    summary = "Check user consent status",
163    responses(
164        (status = 200, description = "Consent status", body = ConsentStatusResponse),
165        (status = 401, description = "Unauthorized"),
166        (status = 500, description = "Internal server error"),
167    ),
168    security(("bearer_auth" = []))
169)]
170#[get("/consent/status")]
171pub async fn get_consent_status(
172    _req: HttpRequest,
173    data: web::Data<AppState>,
174    auth: AuthenticatedUser,
175) -> impl Responder {
176    match data
177        .consent_use_cases
178        .get_consent_status(auth.user_id)
179        .await
180    {
181        Ok(status) => HttpResponse::Ok().json(ConsentStatusResponse {
182            privacy_policy_accepted: status.privacy_policy_accepted,
183            terms_accepted: status.terms_accepted,
184            privacy_policy_accepted_at: status.privacy_policy_accepted_at.map(|t| t.to_rfc3339()),
185            terms_accepted_at: status.terms_accepted_at.map(|t| t.to_rfc3339()),
186            user_id: auth.user_id.to_string(),
187        }),
188        Err(e) => {
189            log::error!("Failed to get consent status: {}", e);
190            HttpResponse::InternalServerError().json(serde_json::json!({ "error": e }))
191        }
192    }
193}