1use crate::state::AppState;
10use axum::{
11 extract::{Path, State},
12 http::StatusCode,
13 response::IntoResponse,
14 Json,
15};
16use parking_lot::RwLock;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::path::PathBuf;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum Purpose {
30 Marketing,
32 Analytics,
34 ThirdPartySharing,
36 DataProcessing,
38 Personalization,
40 LocationTracking,
42 Profiling,
44 CrossDeviceTracking,
46 Advertising,
48 Research,
50 DoNotSell,
52 Custom(u32),
54}
55
56impl Purpose {
57 pub fn description(&self) -> &'static str {
59 match self {
60 Purpose::Marketing => "Marketing communications and promotional materials",
61 Purpose::Analytics => "Analytics and usage tracking",
62 Purpose::ThirdPartySharing => "Sharing data with third parties",
63 Purpose::DataProcessing => "General data processing for service operation",
64 Purpose::Personalization => "Personalization and recommendations",
65 Purpose::LocationTracking => "Location tracking and geolocation services",
66 Purpose::Profiling => "Profiling for automated decision-making",
67 Purpose::CrossDeviceTracking => "Cross-device tracking",
68 Purpose::Advertising => "Advertising and targeted ads",
69 Purpose::Research => "Research and statistical purposes",
70 Purpose::DoNotSell => "CCPA: Do Not Sell My Personal Information",
71 Purpose::Custom(_) => "Custom purpose",
72 }
73 }
74
75 pub fn from_str(s: &str) -> Option<Self> {
77 match s.to_lowercase().as_str() {
78 "marketing" => Some(Purpose::Marketing),
79 "analytics" => Some(Purpose::Analytics),
80 "third_party_sharing" | "thirdpartysharing" => Some(Purpose::ThirdPartySharing),
81 "data_processing" | "dataprocessing" => Some(Purpose::DataProcessing),
82 "personalization" => Some(Purpose::Personalization),
83 "location_tracking" | "locationtracking" => Some(Purpose::LocationTracking),
84 "profiling" => Some(Purpose::Profiling),
85 "cross_device_tracking" | "crossdevicetracking" => Some(Purpose::CrossDeviceTracking),
86 "advertising" => Some(Purpose::Advertising),
87 "research" => Some(Purpose::Research),
88 "do_not_sell" | "donotsell" => Some(Purpose::DoNotSell),
89 s if s.starts_with("custom:") => s
90 .strip_prefix("custom:")
91 .and_then(|id| id.parse().ok())
92 .map(Purpose::Custom),
93 _ => None,
94 }
95 }
96
97 pub fn to_str(&self) -> String {
99 match self {
100 Purpose::Marketing => "marketing".to_string(),
101 Purpose::Analytics => "analytics".to_string(),
102 Purpose::ThirdPartySharing => "third_party_sharing".to_string(),
103 Purpose::DataProcessing => "data_processing".to_string(),
104 Purpose::Personalization => "personalization".to_string(),
105 Purpose::LocationTracking => "location_tracking".to_string(),
106 Purpose::Profiling => "profiling".to_string(),
107 Purpose::CrossDeviceTracking => "cross_device_tracking".to_string(),
108 Purpose::Advertising => "advertising".to_string(),
109 Purpose::Research => "research".to_string(),
110 Purpose::DoNotSell => "do_not_sell".to_string(),
111 Purpose::Custom(id) => format!("custom:{}", id),
112 }
113 }
114}
115
116impl std::fmt::Display for Purpose {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", self.to_str())
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum ConsentSource {
126 WebForm,
128 MobileApp,
130 Api,
132 Import,
134 PaperForm,
136 Email,
138 Verbal,
140 SystemDefault,
142 Custom(String),
144}
145
146impl ConsentSource {
147 pub fn from_str(s: &str) -> Self {
148 match s.to_lowercase().as_str() {
149 "web_form" | "webform" | "web" => ConsentSource::WebForm,
150 "mobile_app" | "mobileapp" | "mobile" => ConsentSource::MobileApp,
151 "api" => ConsentSource::Api,
152 "import" => ConsentSource::Import,
153 "paper_form" | "paperform" | "paper" => ConsentSource::PaperForm,
154 "email" => ConsentSource::Email,
155 "verbal" => ConsentSource::Verbal,
156 "system_default" | "systemdefault" | "system" => ConsentSource::SystemDefault,
157 other => ConsentSource::Custom(other.to_string()),
158 }
159 }
160}
161
162impl std::fmt::Display for ConsentSource {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 ConsentSource::WebForm => write!(f, "web_form"),
166 ConsentSource::MobileApp => write!(f, "mobile_app"),
167 ConsentSource::Api => write!(f, "api"),
168 ConsentSource::Import => write!(f, "import"),
169 ConsentSource::PaperForm => write!(f, "paper_form"),
170 ConsentSource::Email => write!(f, "email"),
171 ConsentSource::Verbal => write!(f, "verbal"),
172 ConsentSource::SystemDefault => write!(f, "system_default"),
173 ConsentSource::Custom(s) => write!(f, "{}", s),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ConsentRecord {
181 pub id: String,
183 pub subject_id: String,
185 pub purpose: Purpose,
187 pub granted: bool,
189 pub timestamp: u64,
191 pub source: ConsentSource,
193 pub expires_at: Option<u64>,
195 pub version: String,
197 #[serde(default)]
199 pub metadata: HashMap<String, String>,
200}
201
202impl ConsentRecord {
203 pub fn new(
205 subject_id: &str,
206 purpose: Purpose,
207 granted: bool,
208 source: ConsentSource,
209 version: &str,
210 ) -> Self {
211 Self {
212 id: generate_consent_id(),
213 subject_id: subject_id.to_string(),
214 purpose,
215 granted,
216 timestamp: now_timestamp_ms(),
217 source,
218 expires_at: None,
219 version: version.to_string(),
220 metadata: HashMap::new(),
221 }
222 }
223
224 pub fn is_expired(&self) -> bool {
226 if let Some(expires_at) = self.expires_at {
227 now_timestamp_ms() > expires_at
228 } else {
229 false
230 }
231 }
232
233 pub fn is_valid(&self) -> bool {
235 self.granted && !self.is_expired()
236 }
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ConsentHistoryEntry {
242 pub record: ConsentRecord,
244 pub action: ConsentAction,
246 pub actor: String,
248 pub reason: Option<String>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "snake_case")]
255pub enum ConsentAction {
256 Grant,
258 Deny,
260 Withdraw,
262 Renew,
264 Update,
266 Expire,
268}
269
270pub struct ConsentManager {
277 records: RwLock<HashMap<String, HashMap<Purpose, ConsentRecord>>>,
279 history: RwLock<HashMap<String, Vec<ConsentHistoryEntry>>>,
281 do_not_sell_list: RwLock<std::collections::HashSet<String>>,
283 record_counter: RwLock<u64>,
285 data_dir: Option<PathBuf>,
287}
288
289#[derive(Serialize, Deserialize)]
291struct ConsentSnapshot {
292 records: HashMap<String, HashMap<Purpose, ConsentRecord>>,
293 history: HashMap<String, Vec<ConsentHistoryEntry>>,
294 do_not_sell_list: std::collections::HashSet<String>,
295 record_counter: u64,
296}
297
298impl ConsentManager {
299 pub fn new() -> Self {
301 Self::with_data_dir(None)
302 }
303
304 pub fn with_data_dir(data_dir: Option<PathBuf>) -> Self {
307 if let Some(ref dir) = data_dir {
308 let path = dir.join("consent.json");
309 if path.exists() {
310 match std::fs::read_to_string(&path) {
311 Ok(contents) => match serde_json::from_str::<ConsentSnapshot>(&contents) {
312 Ok(snapshot) => {
313 tracing::info!("Loaded consent data from {}", path.display());
314 return Self {
315 records: RwLock::new(snapshot.records),
316 history: RwLock::new(snapshot.history),
317 do_not_sell_list: RwLock::new(snapshot.do_not_sell_list),
318 record_counter: RwLock::new(snapshot.record_counter),
319 data_dir,
320 };
321 }
322 Err(e) => {
323 tracing::error!(
324 "Failed to deserialize consent data from {}: {}",
325 path.display(),
326 e
327 );
328 }
329 },
330 Err(e) => {
331 tracing::error!(
332 "Failed to read consent data from {}: {}",
333 path.display(),
334 e
335 );
336 }
337 }
338 }
339 }
340
341 Self {
342 records: RwLock::new(HashMap::new()),
343 history: RwLock::new(HashMap::new()),
344 do_not_sell_list: RwLock::new(std::collections::HashSet::new()),
345 record_counter: RwLock::new(0),
346 data_dir,
347 }
348 }
349
350 fn flush_to_disk(&self) {
352 let Some(ref dir) = self.data_dir else {
353 return;
354 };
355
356 let snapshot = ConsentSnapshot {
357 records: self.records.read().clone(),
358 history: self.history.read().clone(),
359 do_not_sell_list: self.do_not_sell_list.read().clone(),
360 record_counter: *self.record_counter.read(),
361 };
362
363 if let Err(e) = std::fs::create_dir_all(dir) {
364 tracing::error!(
365 "Failed to create consent data directory {}: {}",
366 dir.display(),
367 e
368 );
369 return;
370 }
371
372 let path = dir.join("consent.json");
373 match serde_json::to_string_pretty(&snapshot) {
374 Ok(json) => {
375 if let Err(e) = std::fs::write(&path, json) {
376 tracing::error!("Failed to write consent data to {}: {}", path.display(), e);
377 }
378 }
379 Err(e) => {
380 tracing::error!("Failed to serialize consent data: {}", e);
381 }
382 }
383 }
384
385 pub fn record_consent(
387 &self,
388 subject_id: &str,
389 purpose: Purpose,
390 granted: bool,
391 source: ConsentSource,
392 version: &str,
393 expires_at: Option<u64>,
394 metadata: Option<HashMap<String, String>>,
395 actor: &str,
396 ) -> ConsentRecord {
397 let mut record = ConsentRecord::new(subject_id, purpose, granted, source, version);
398 record.expires_at = expires_at;
399 if let Some(meta) = metadata {
400 record.metadata = meta;
401 }
402
403 let action = {
405 let records = self.records.read();
406 if let Some(subject_records) = records.get(subject_id) {
407 if let Some(existing) = subject_records.get(&purpose) {
408 if granted && !existing.granted {
409 ConsentAction::Grant
410 } else if !granted && existing.granted {
411 ConsentAction::Withdraw
412 } else if granted {
413 ConsentAction::Renew
414 } else {
415 ConsentAction::Deny
416 }
417 } else if granted {
418 ConsentAction::Grant
419 } else {
420 ConsentAction::Deny
421 }
422 } else if granted {
423 ConsentAction::Grant
424 } else {
425 ConsentAction::Deny
426 }
427 };
428
429 {
431 let mut records = self.records.write();
432 let subject_records = records.entry(subject_id.to_string()).or_default();
433 subject_records.insert(purpose, record.clone());
434 }
435
436 if purpose == Purpose::DoNotSell {
438 let mut dns_list = self.do_not_sell_list.write();
439 if granted {
440 dns_list.insert(subject_id.to_string());
442 } else {
443 dns_list.remove(subject_id);
445 }
446 }
447
448 {
450 let mut history = self.history.write();
451 let subject_history = history.entry(subject_id.to_string()).or_default();
452 subject_history.push(ConsentHistoryEntry {
453 record: record.clone(),
454 action,
455 actor: actor.to_string(),
456 reason: None,
457 });
458 if subject_history.len() > 1000 {
460 let excess = subject_history.len() - 1000;
461 subject_history.drain(..excess);
462 }
463 }
464
465 tracing::info!(
466 "Consent recorded: subject={}, purpose={}, granted={}, action={:?}",
467 subject_id,
468 purpose,
469 granted,
470 action
471 );
472
473 self.flush_to_disk();
474
475 record
476 }
477
478 pub fn get_consent(&self, subject_id: &str) -> Vec<ConsentRecord> {
480 let records = self.records.read();
481 records
482 .get(subject_id)
483 .map(|r| r.values().cloned().collect())
484 .unwrap_or_default()
485 }
486
487 pub fn get_consent_for_purpose(
489 &self,
490 subject_id: &str,
491 purpose: Purpose,
492 ) -> Option<ConsentRecord> {
493 let records = self.records.read();
494 records
495 .get(subject_id)
496 .and_then(|r| r.get(&purpose))
497 .cloned()
498 }
499
500 pub fn check_consent(&self, subject_id: &str, purpose: Purpose) -> bool {
503 let records = self.records.read();
504 if let Some(subject_records) = records.get(subject_id) {
505 if let Some(record) = subject_records.get(&purpose) {
506 return record.is_valid();
507 }
508 }
509 false
510 }
511
512 pub fn withdraw_consent(
514 &self,
515 subject_id: &str,
516 purpose: Purpose,
517 actor: &str,
518 reason: Option<&str>,
519 ) -> Result<ConsentRecord, String> {
520 let existing = {
522 let records = self.records.read();
523 records
524 .get(subject_id)
525 .and_then(|r| r.get(&purpose))
526 .cloned()
527 };
528
529 let existing = existing.ok_or_else(|| {
530 format!(
531 "No consent record found for subject {} and purpose {}",
532 subject_id, purpose
533 )
534 })?;
535
536 let mut record = ConsentRecord::new(
538 subject_id,
539 purpose,
540 false,
541 existing.source.clone(),
542 &existing.version,
543 );
544 record.metadata = existing.metadata.clone();
545 record
546 .metadata
547 .insert("withdrawn_from".to_string(), existing.id.clone());
548
549 {
551 let mut records = self.records.write();
552 if let Some(subject_records) = records.get_mut(subject_id) {
553 subject_records.insert(purpose, record.clone());
554 }
555 }
556
557 if purpose == Purpose::DoNotSell {
559 let mut dns_list = self.do_not_sell_list.write();
560 dns_list.remove(subject_id);
562 }
563
564 {
566 let mut history = self.history.write();
567 let subject_history = history.entry(subject_id.to_string()).or_default();
568 subject_history.push(ConsentHistoryEntry {
569 record: record.clone(),
570 action: ConsentAction::Withdraw,
571 actor: actor.to_string(),
572 reason: reason.map(String::from),
573 });
574 }
575
576 tracing::info!(
577 "Consent withdrawn: subject={}, purpose={}, actor={}",
578 subject_id,
579 purpose,
580 actor
581 );
582
583 self.flush_to_disk();
584
585 Ok(record)
586 }
587
588 pub fn get_history(&self, subject_id: &str) -> Vec<ConsentHistoryEntry> {
590 let history = self.history.read();
591 history.get(subject_id).cloned().unwrap_or_default()
592 }
593
594 pub fn get_history_for_purpose(
596 &self,
597 subject_id: &str,
598 purpose: Purpose,
599 ) -> Vec<ConsentHistoryEntry> {
600 let history = self.history.read();
601 history
602 .get(subject_id)
603 .map(|h| {
604 h.iter()
605 .filter(|e| e.record.purpose == purpose)
606 .cloned()
607 .collect()
608 })
609 .unwrap_or_default()
610 }
611
612 pub fn is_on_do_not_sell_list(&self, subject_id: &str) -> bool {
614 self.do_not_sell_list.read().contains(subject_id)
615 }
616
617 pub fn get_do_not_sell_list(&self) -> Vec<String> {
619 self.do_not_sell_list.read().iter().cloned().collect()
620 }
621
622 pub fn export_subject_data(&self, subject_id: &str) -> SubjectConsentExport {
624 let records = self.get_consent(subject_id);
625 let history = self.get_history(subject_id);
626 let on_dns_list = self.is_on_do_not_sell_list(subject_id);
627
628 SubjectConsentExport {
629 subject_id: subject_id.to_string(),
630 export_timestamp: now_timestamp_ms(),
631 current_consents: records,
632 consent_history: history,
633 on_do_not_sell_list: on_dns_list,
634 }
635 }
636
637 pub fn delete_subject_data(&self, subject_id: &str, actor: &str) -> bool {
639 let had_records = {
640 let mut records = self.records.write();
641 records.remove(subject_id).is_some()
642 };
643
644 let had_history = {
645 let mut history = self.history.write();
646 history.remove(subject_id).is_some()
647 };
648
649 {
650 let mut dns_list = self.do_not_sell_list.write();
651 dns_list.remove(subject_id);
652 }
653
654 if had_records || had_history {
655 tracing::info!(
656 "Subject consent data deleted: subject={}, actor={}",
657 subject_id,
658 actor
659 );
660 self.flush_to_disk();
661 }
662
663 had_records || had_history
664 }
665
666 pub fn get_stats(&self) -> ConsentStats {
668 let records = self.records.read();
669 let history = self.history.read();
670 let dns_list = self.do_not_sell_list.read();
671
672 let total_subjects = records.len();
673 let total_records: usize = records.values().map(|r| r.len()).sum();
674 let total_history_entries: usize = history.values().map(|h| h.len()).sum();
675
676 let mut consents_by_purpose: HashMap<String, (usize, usize)> = HashMap::new();
677 for subject_records in records.values() {
678 for (purpose, record) in subject_records {
679 let entry = consents_by_purpose
680 .entry(purpose.to_str())
681 .or_insert((0, 0));
682 if record.is_valid() {
683 entry.0 += 1;
684 } else {
685 entry.1 += 1;
686 }
687 }
688 }
689
690 ConsentStats {
691 total_subjects,
692 total_records,
693 total_history_entries,
694 do_not_sell_count: dns_list.len(),
695 consents_by_purpose,
696 }
697 }
698}
699
700impl Default for ConsentManager {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct SubjectConsentExport {
709 pub subject_id: String,
710 pub export_timestamp: u64,
711 pub current_consents: Vec<ConsentRecord>,
712 pub consent_history: Vec<ConsentHistoryEntry>,
713 pub on_do_not_sell_list: bool,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct ConsentStats {
719 pub total_subjects: usize,
720 pub total_records: usize,
721 pub total_history_entries: usize,
722 pub do_not_sell_count: usize,
723 pub consents_by_purpose: HashMap<String, (usize, usize)>,
725}
726
727#[derive(Debug, Clone, Deserialize)]
733pub struct RecordConsentRequest {
734 pub subject_id: String,
735 pub purpose: String,
736 pub granted: bool,
737 #[serde(default = "default_source")]
738 pub source: String,
739 #[serde(default = "default_version")]
740 pub version: String,
741 pub expires_at: Option<u64>,
742 #[serde(default)]
743 pub metadata: Option<HashMap<String, String>>,
744}
745
746fn default_source() -> String {
747 "api".to_string()
748}
749
750fn default_version() -> String {
751 "1.0".to_string()
752}
753
754#[derive(Debug, Clone, Serialize)]
756pub struct ConsentResponse {
757 pub success: bool,
758 pub record: Option<ConsentRecord>,
759 pub error: Option<String>,
760}
761
762#[derive(Debug, Clone, Serialize)]
764pub struct ConsentStatusResponse {
765 pub subject_id: String,
766 pub consents: Vec<ConsentRecord>,
767 pub on_do_not_sell_list: bool,
768}
769
770#[derive(Debug, Clone, Deserialize)]
772pub struct WithdrawConsentRequest {
773 pub reason: Option<String>,
774}
775
776pub async fn record_consent(
783 State(state): State<AppState>,
784 Json(request): Json<RecordConsentRequest>,
785) -> impl IntoResponse {
786 let purpose = match Purpose::from_str(&request.purpose) {
787 Some(p) => p,
788 None => {
789 return (
790 StatusCode::BAD_REQUEST,
791 Json(ConsentResponse {
792 success: false,
793 record: None,
794 error: Some(format!("Invalid purpose: {}", request.purpose)),
795 }),
796 );
797 }
798 };
799
800 let source = ConsentSource::from_str(&request.source);
801
802 let record = state.consent_manager.record_consent(
803 &request.subject_id,
804 purpose,
805 request.granted,
806 source,
807 &request.version,
808 request.expires_at,
809 request.metadata,
810 "api", );
812
813 state.activity.log_write(
814 &format!(
815 "Consent recorded: subject={}, purpose={}, granted={}",
816 request.subject_id, purpose, request.granted
817 ),
818 None,
819 );
820
821 (
822 StatusCode::CREATED,
823 Json(ConsentResponse {
824 success: true,
825 record: Some(record),
826 error: None,
827 }),
828 )
829}
830
831pub async fn get_consent_status(
834 State(state): State<AppState>,
835 Path(subject_id): Path<String>,
836) -> Json<ConsentStatusResponse> {
837 let consents = state.consent_manager.get_consent(&subject_id);
838 let on_dns_list = state.consent_manager.is_on_do_not_sell_list(&subject_id);
839
840 Json(ConsentStatusResponse {
841 subject_id,
842 consents,
843 on_do_not_sell_list: on_dns_list,
844 })
845}
846
847pub async fn withdraw_consent(
850 State(state): State<AppState>,
851 Path((subject_id, purpose_str)): Path<(String, String)>,
852 Json(request): Json<WithdrawConsentRequest>,
853) -> impl IntoResponse {
854 let purpose = match Purpose::from_str(&purpose_str) {
855 Some(p) => p,
856 None => {
857 return (
858 StatusCode::BAD_REQUEST,
859 Json(ConsentResponse {
860 success: false,
861 record: None,
862 error: Some(format!("Invalid purpose: {}", purpose_str)),
863 }),
864 );
865 }
866 };
867
868 match state.consent_manager.withdraw_consent(
869 &subject_id,
870 purpose,
871 "api", request.reason.as_deref(),
873 ) {
874 Ok(record) => {
875 state.activity.log_write(
876 &format!(
877 "Consent withdrawn: subject={}, purpose={}",
878 subject_id, purpose
879 ),
880 None,
881 );
882
883 (
884 StatusCode::OK,
885 Json(ConsentResponse {
886 success: true,
887 record: Some(record),
888 error: None,
889 }),
890 )
891 }
892 Err(e) => (
893 StatusCode::NOT_FOUND,
894 Json(ConsentResponse {
895 success: false,
896 record: None,
897 error: Some(e),
898 }),
899 ),
900 }
901}
902
903pub async fn get_consent_history(
906 State(state): State<AppState>,
907 Path(subject_id): Path<String>,
908 axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
909) -> Json<Vec<ConsentHistoryEntry>> {
910 let history = if let Some(purpose_str) = params.get("purpose") {
911 if let Some(purpose) = Purpose::from_str(purpose_str) {
912 state
913 .consent_manager
914 .get_history_for_purpose(&subject_id, purpose)
915 } else {
916 state.consent_manager.get_history(&subject_id)
917 }
918 } else {
919 state.consent_manager.get_history(&subject_id)
920 };
921
922 Json(history)
923}
924
925pub async fn check_consent_status(
928 State(state): State<AppState>,
929 Path((subject_id, purpose_str)): Path<(String, String)>,
930) -> impl IntoResponse {
931 let purpose = match Purpose::from_str(&purpose_str) {
932 Some(p) => p,
933 None => {
934 return (
935 StatusCode::BAD_REQUEST,
936 Json(serde_json::json!({
937 "error": format!("Invalid purpose: {}", purpose_str)
938 })),
939 );
940 }
941 };
942
943 let granted = state.consent_manager.check_consent(&subject_id, purpose);
944
945 (
946 StatusCode::OK,
947 Json(serde_json::json!({
948 "subject_id": subject_id,
949 "purpose": purpose.to_str(),
950 "granted": granted
951 })),
952 )
953}
954
955pub async fn export_consent_data(
958 State(state): State<AppState>,
959 Path(subject_id): Path<String>,
960) -> Json<SubjectConsentExport> {
961 let export = state.consent_manager.export_subject_data(&subject_id);
962 Json(export)
963}
964
965pub async fn delete_consent_data(
968 State(state): State<AppState>,
969 Path(subject_id): Path<String>,
970) -> impl IntoResponse {
971 let deleted = state
972 .consent_manager
973 .delete_subject_data(&subject_id, "api");
974
975 state.activity.log(
976 crate::activity::ActivityType::Delete,
977 &format!("Consent data deleted for subject: {}", subject_id),
978 );
979
980 if deleted {
981 (
982 StatusCode::OK,
983 Json(serde_json::json!({
984 "success": true,
985 "message": format!("Consent data deleted for subject: {}", subject_id)
986 })),
987 )
988 } else {
989 (
990 StatusCode::NOT_FOUND,
991 Json(serde_json::json!({
992 "success": false,
993 "error": format!("No consent data found for subject: {}", subject_id)
994 })),
995 )
996 }
997}
998
999pub async fn get_consent_stats(State(state): State<AppState>) -> Json<ConsentStats> {
1002 Json(state.consent_manager.get_stats())
1003}
1004
1005pub async fn get_do_not_sell_list(State(state): State<AppState>) -> Json<Vec<String>> {
1008 Json(state.consent_manager.get_do_not_sell_list())
1009}
1010
1011pub fn check_consent(state: &AppState, subject_id: &str, purpose: Purpose) -> bool {
1026 state.consent_manager.check_consent(subject_id, purpose)
1027}
1028
1029pub fn check_all_consents(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
1032 purposes
1033 .iter()
1034 .all(|p| state.consent_manager.check_consent(subject_id, *p))
1035}
1036
1037pub fn check_any_consent(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
1039 purposes
1040 .iter()
1041 .any(|p| state.consent_manager.check_consent(subject_id, *p))
1042}
1043
1044fn now_timestamp_ms() -> u64 {
1050 SystemTime::now()
1051 .duration_since(UNIX_EPOCH)
1052 .unwrap_or_default()
1053 .as_millis() as u64
1054}
1055
1056fn generate_consent_id() -> String {
1058 use std::sync::atomic::{AtomicU64, Ordering};
1059 static COUNTER: AtomicU64 = AtomicU64::new(0);
1060
1061 let count = COUNTER.fetch_add(1, Ordering::SeqCst);
1062 let timestamp = now_timestamp_ms();
1063 format!("consent-{}-{:06}", timestamp, count)
1064}
1065
1066#[cfg(test)]
1071mod tests {
1072 use super::*;
1073
1074 #[test]
1075 fn test_consent_record_creation() {
1076 let record = ConsentRecord::new(
1077 "user123",
1078 Purpose::Marketing,
1079 true,
1080 ConsentSource::WebForm,
1081 "1.0",
1082 );
1083
1084 assert_eq!(record.subject_id, "user123");
1085 assert_eq!(record.purpose, Purpose::Marketing);
1086 assert!(record.granted);
1087 assert!(record.is_valid());
1088 }
1089
1090 #[test]
1091 fn test_consent_expiration() {
1092 let mut record = ConsentRecord::new(
1093 "user123",
1094 Purpose::Analytics,
1095 true,
1096 ConsentSource::Api,
1097 "1.0",
1098 );
1099
1100 record.expires_at = Some(1000);
1102 assert!(record.is_expired());
1103 assert!(!record.is_valid());
1104 }
1105
1106 #[test]
1107 fn test_consent_manager_basic_operations() {
1108 let manager = ConsentManager::new();
1109
1110 let record = manager.record_consent(
1112 "user123",
1113 Purpose::Marketing,
1114 true,
1115 ConsentSource::WebForm,
1116 "1.0",
1117 None,
1118 None,
1119 "test",
1120 );
1121
1122 assert!(record.granted);
1123
1124 assert!(manager.check_consent("user123", Purpose::Marketing));
1126 assert!(!manager.check_consent("user123", Purpose::Analytics));
1127 assert!(!manager.check_consent("user456", Purpose::Marketing));
1128 }
1129
1130 #[test]
1131 fn test_consent_withdrawal() {
1132 let manager = ConsentManager::new();
1133
1134 manager.record_consent(
1136 "user123",
1137 Purpose::Marketing,
1138 true,
1139 ConsentSource::WebForm,
1140 "1.0",
1141 None,
1142 None,
1143 "test",
1144 );
1145
1146 assert!(manager.check_consent("user123", Purpose::Marketing));
1147
1148 let result =
1150 manager.withdraw_consent("user123", Purpose::Marketing, "test", Some("User request"));
1151 assert!(result.is_ok());
1152 assert!(!manager.check_consent("user123", Purpose::Marketing));
1153 }
1154
1155 #[test]
1156 fn test_consent_history() {
1157 let manager = ConsentManager::new();
1158
1159 manager.record_consent(
1161 "user123",
1162 Purpose::Marketing,
1163 true,
1164 ConsentSource::WebForm,
1165 "1.0",
1166 None,
1167 None,
1168 "test",
1169 );
1170
1171 manager.record_consent(
1172 "user123",
1173 Purpose::Marketing,
1174 false,
1175 ConsentSource::WebForm,
1176 "1.0",
1177 None,
1178 None,
1179 "test",
1180 );
1181
1182 let history = manager.get_history("user123");
1183 assert_eq!(history.len(), 2);
1184 assert_eq!(history[0].action, ConsentAction::Grant);
1185 assert_eq!(history[1].action, ConsentAction::Withdraw);
1186 }
1187
1188 #[test]
1189 fn test_do_not_sell_list() {
1190 let manager = ConsentManager::new();
1191
1192 manager.record_consent(
1194 "user123",
1195 Purpose::DoNotSell,
1196 true,
1197 ConsentSource::WebForm,
1198 "1.0",
1199 None,
1200 None,
1201 "test",
1202 );
1203
1204 assert!(manager.is_on_do_not_sell_list("user123"));
1205 assert!(!manager.is_on_do_not_sell_list("user456"));
1206
1207 manager.record_consent(
1209 "user123",
1210 Purpose::DoNotSell,
1211 false,
1212 ConsentSource::WebForm,
1213 "1.0",
1214 None,
1215 None,
1216 "test",
1217 );
1218
1219 assert!(!manager.is_on_do_not_sell_list("user123"));
1220 }
1221
1222 #[test]
1223 fn test_purpose_parsing() {
1224 assert_eq!(Purpose::from_str("marketing"), Some(Purpose::Marketing));
1225 assert_eq!(Purpose::from_str("ANALYTICS"), Some(Purpose::Analytics));
1226 assert_eq!(
1227 Purpose::from_str("third_party_sharing"),
1228 Some(Purpose::ThirdPartySharing)
1229 );
1230 assert_eq!(Purpose::from_str("do_not_sell"), Some(Purpose::DoNotSell));
1231 assert_eq!(Purpose::from_str("custom:42"), Some(Purpose::Custom(42)));
1232 assert_eq!(Purpose::from_str("unknown"), None);
1233 }
1234
1235 #[test]
1236 fn test_consent_stats() {
1237 let manager = ConsentManager::new();
1238
1239 manager.record_consent(
1240 "user1",
1241 Purpose::Marketing,
1242 true,
1243 ConsentSource::Api,
1244 "1.0",
1245 None,
1246 None,
1247 "test",
1248 );
1249 manager.record_consent(
1250 "user1",
1251 Purpose::Analytics,
1252 false,
1253 ConsentSource::Api,
1254 "1.0",
1255 None,
1256 None,
1257 "test",
1258 );
1259 manager.record_consent(
1260 "user2",
1261 Purpose::Marketing,
1262 true,
1263 ConsentSource::Api,
1264 "1.0",
1265 None,
1266 None,
1267 "test",
1268 );
1269
1270 let stats = manager.get_stats();
1271 assert_eq!(stats.total_subjects, 2);
1272 assert_eq!(stats.total_records, 3);
1273
1274 let marketing_stats = stats.consents_by_purpose.get("marketing").unwrap();
1275 assert_eq!(marketing_stats.0, 2); assert_eq!(marketing_stats.1, 0); }
1278
1279 #[test]
1280 fn test_subject_data_export() {
1281 let manager = ConsentManager::new();
1282
1283 manager.record_consent(
1284 "user123",
1285 Purpose::Marketing,
1286 true,
1287 ConsentSource::Api,
1288 "1.0",
1289 None,
1290 None,
1291 "test",
1292 );
1293 manager.record_consent(
1294 "user123",
1295 Purpose::Analytics,
1296 true,
1297 ConsentSource::Api,
1298 "1.0",
1299 None,
1300 None,
1301 "test",
1302 );
1303
1304 let export = manager.export_subject_data("user123");
1305 assert_eq!(export.subject_id, "user123");
1306 assert_eq!(export.current_consents.len(), 2);
1307 assert_eq!(export.consent_history.len(), 2);
1308 }
1309
1310 #[test]
1311 fn test_subject_data_deletion() {
1312 let manager = ConsentManager::new();
1313
1314 manager.record_consent(
1315 "user123",
1316 Purpose::Marketing,
1317 true,
1318 ConsentSource::Api,
1319 "1.0",
1320 None,
1321 None,
1322 "test",
1323 );
1324
1325 assert!(manager.delete_subject_data("user123", "test"));
1326 assert!(manager.get_consent("user123").is_empty());
1327 assert!(manager.get_history("user123").is_empty());
1328
1329 assert!(!manager.delete_subject_data("user123", "test"));
1331 }
1332}