File size: 5,892 Bytes
1295969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! GDPR Art.7 consent Β· Art.17 erasure Β· Art.20 portability. CCPA opt-out.
//!
//! Persistence: LMDB via persist::LmdbStore.
//! Per-user auth: callers may only read/modify their own data.
use crate::AppState;
use axum::{
    extract::{Path, State},
    http::{HeaderMap, StatusCode},
    response::Json,
};
use serde::{Deserialize, Serialize};
use tracing::warn;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ConsentPurpose {
    Analytics,
    Marketing,
    ThirdPartySharing,
    DataProcessing,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentRecord {
    pub user_id: String,
    pub purpose: ConsentPurpose,
    pub granted: bool,
    pub timestamp: String,
    pub ip_hash: String,
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeletionRequest {
    pub user_id: String,
    pub requested_at: String,
    pub fulfilled_at: Option<String>,
    pub scope: Vec<String>,
}

#[derive(Deserialize)]
pub struct ConsentRequest {
    pub user_id: String,
    pub purpose: ConsentPurpose,
    pub granted: bool,
    pub ip_hash: String,
    pub version: String,
}

pub struct PrivacyStore {
    consent_db: crate::persist::LmdbStore,
    deletion_db: crate::persist::LmdbStore,
}

impl PrivacyStore {
    pub fn open(path: &str) -> anyhow::Result<Self> {
        // Two named databases inside the same LMDB directory
        let consent_dir = format!("{path}/consents");
        let deletion_dir = format!("{path}/deletions");
        Ok(Self {
            consent_db: crate::persist::LmdbStore::open(&consent_dir, "consents")?,
            deletion_db: crate::persist::LmdbStore::open(&deletion_dir, "deletions")?,
        })
    }

    /// Append a consent record; key = user_id (list of consents per user).
    pub fn record_consent(&self, r: ConsentRecord) {
        if let Err(e) = self.consent_db.append(&r.user_id.clone(), r) {
            tracing::error!(err=%e, "Consent persist error");
        }
    }

    /// Return the latest consent value for (user_id, purpose).
    pub fn has_consent(&self, user_id: &str, purpose: &ConsentPurpose) -> bool {
        self.consent_db
            .get_list::<ConsentRecord>(user_id)
            .unwrap_or_default()
            .into_iter()
            .rev()
            .find(|c| &c.purpose == purpose)
            .map(|c| c.granted)
            .unwrap_or(false)
    }

    /// Queue a GDPR deletion request.
    pub fn queue_deletion(&self, r: DeletionRequest) {
        if let Err(e) = self.deletion_db.put(&r.user_id, &r) {
            tracing::error!(err=%e, user=%r.user_id, "Deletion persist error");
        }
    }

    /// Export all consent records for a user (GDPR Art.20 portability).
    pub fn export_user_data(&self, user_id: &str) -> serde_json::Value {
        let consents = self
            .consent_db
            .get_list::<ConsentRecord>(user_id)
            .unwrap_or_default();
        serde_json::json!({ "user_id": user_id, "consents": consents })
    }
}

// ── HTTP handlers ─────────────────────────────────────────────────────────────

pub async fn record_consent(
    State(state): State<AppState>,
    headers: HeaderMap,
    Json(req): Json<ConsentRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
    // PER-USER AUTH: the caller's wallet address must match the user_id in the request
    let caller = crate::auth::extract_caller(&headers)?;
    if !caller.eq_ignore_ascii_case(&req.user_id) {
        warn!(caller=%caller, uid=%req.user_id, "Consent: caller != uid β€” forbidden");
        return Err(StatusCode::FORBIDDEN);
    }

    state.privacy_db.record_consent(ConsentRecord {
        user_id: req.user_id.clone(),
        purpose: req.purpose,
        granted: req.granted,
        timestamp: chrono::Utc::now().to_rfc3339(),
        ip_hash: req.ip_hash,
        version: req.version,
    });
    state
        .audit_log
        .record(&format!(
            "CONSENT user='{}' granted={}",
            req.user_id, req.granted
        ))
        .ok();
    Ok(Json(serde_json::json!({ "status": "recorded" })))
}

pub async fn delete_user_data(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(user_id): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
    // PER-USER AUTH: caller may only delete their own data
    let caller = crate::auth::extract_caller(&headers)?;
    if !caller.eq_ignore_ascii_case(&user_id) {
        warn!(caller=%caller, uid=%user_id, "Privacy delete: caller != uid β€” forbidden");
        return Err(StatusCode::FORBIDDEN);
    }

    state.privacy_db.queue_deletion(DeletionRequest {
        user_id: user_id.clone(),
        requested_at: chrono::Utc::now().to_rfc3339(),
        fulfilled_at: None,
        scope: vec!["uploads", "consents", "kyc", "payments"]
            .into_iter()
            .map(|s| s.into())
            .collect(),
    });
    state
        .audit_log
        .record(&format!("GDPR_DELETE_REQUEST user='{user_id}'"))
        .ok();
    warn!(user=%user_id, "GDPR deletion queued β€” 30 day deadline (Art.17)");
    Ok(Json(
        serde_json::json!({ "status": "queued", "deadline": "30 days per GDPR Art.17" }),
    ))
}

pub async fn export_user_data(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(user_id): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
    // PER-USER AUTH: caller may only export their own data
    let caller = crate::auth::extract_caller(&headers)?;
    if !caller.eq_ignore_ascii_case(&user_id) {
        warn!(caller=%caller, uid=%user_id, "Privacy export: caller != uid β€” forbidden");
        return Err(StatusCode::FORBIDDEN);
    }

    Ok(Json(state.privacy_db.export_user_data(&user_id)))
}