pub mod manager;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum ConsentError {
#[error("Consent record not found: {0}")]
ConsentNotFound(String),
#[error("Invalid consent data: {0}")]
InvalidData(String),
#[error("Consent already exists: {0}")]
ConsentExists(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ConsentType {
DataProcessing,
Marketing,
Analytics,
DataSharing,
AutomatedDecisionMaking,
SessionStorage,
AuditLogging,
Custom(String),
}
impl std::fmt::Display for ConsentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConsentType::DataProcessing => write!(f, "Data Processing"),
ConsentType::Marketing => write!(f, "Marketing Communications"),
ConsentType::Analytics => write!(f, "Analytics & Performance"),
ConsentType::DataSharing => write!(f, "Third-party Data Sharing"),
ConsentType::AutomatedDecisionMaking => write!(f, "Automated Decision Making"),
ConsentType::SessionStorage => write!(f, "Session Storage"),
ConsentType::AuditLogging => write!(f, "Audit Logging"),
ConsentType::Custom(desc) => write!(f, "Custom: {desc}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LegalBasis {
Consent,
Contract,
LegalObligation,
VitalInterests,
PublicTask,
LegitimateInterests,
}
impl std::fmt::Display for LegalBasis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LegalBasis::Consent => write!(f, "Consent (GDPR 6.1.a)"),
LegalBasis::Contract => write!(f, "Contract (GDPR 6.1.b)"),
LegalBasis::LegalObligation => write!(f, "Legal Obligation (GDPR 6.1.c)"),
LegalBasis::VitalInterests => write!(f, "Vital Interests (GDPR 6.1.d)"),
LegalBasis::PublicTask => write!(f, "Public Task (GDPR 6.1.e)"),
LegalBasis::LegitimateInterests => write!(f, "Legitimate Interests (GDPR 6.1.f)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConsentStatus {
Granted,
Withdrawn,
Pending,
Expired,
Denied,
}
impl std::fmt::Display for ConsentStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConsentStatus::Granted => write!(f, "Granted"),
ConsentStatus::Withdrawn => write!(f, "Withdrawn"),
ConsentStatus::Pending => write!(f, "Pending"),
ConsentStatus::Expired => write!(f, "Expired"),
ConsentStatus::Denied => write!(f, "Denied"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentRecord {
pub id: String,
pub subject_id: String,
pub consent_type: ConsentType,
pub status: ConsentStatus,
pub legal_basis: LegalBasis,
pub purpose: String,
pub data_categories: Vec<String>,
pub granted_at: Option<DateTime<Utc>>,
pub withdrawn_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
pub consent_source: String,
pub source_ip: Option<String>,
pub metadata: HashMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ConsentRecord {
pub fn new(
subject_id: String,
consent_type: ConsentType,
legal_basis: LegalBasis,
purpose: String,
consent_source: String,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
subject_id,
consent_type,
status: ConsentStatus::Pending,
legal_basis,
purpose,
data_categories: Vec::new(),
granted_at: None,
withdrawn_at: None,
expires_at: None,
consent_source,
source_ip: None,
metadata: HashMap::new(),
created_at: now,
updated_at: now,
}
}
pub fn grant(&mut self, source_ip: Option<String>) {
self.status = ConsentStatus::Granted;
self.granted_at = Some(Utc::now());
self.withdrawn_at = None;
self.source_ip = source_ip;
self.updated_at = Utc::now();
}
pub fn withdraw(&mut self, source_ip: Option<String>) {
self.status = ConsentStatus::Withdrawn;
self.withdrawn_at = Some(Utc::now());
self.source_ip = source_ip;
self.updated_at = Utc::now();
}
pub fn deny(&mut self, source_ip: Option<String>) {
self.status = ConsentStatus::Denied;
self.source_ip = source_ip;
self.updated_at = Utc::now();
}
pub fn is_valid(&self) -> bool {
match self.status {
ConsentStatus::Granted => {
if let Some(expires_at) = self.expires_at {
Utc::now() < expires_at
} else {
true
}
}
_ => false,
}
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Utc::now() >= expires_at
} else {
false
}
}
pub fn set_expiration(&mut self, expires_at: DateTime<Utc>) {
self.expires_at = Some(expires_at);
self.updated_at = Utc::now();
}
pub fn add_data_category(&mut self, category: String) {
if !self.data_categories.contains(&category) {
self.data_categories.push(category);
self.updated_at = Utc::now();
}
}
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentAuditEntry {
pub id: String,
pub consent_id: String,
pub subject_id: String,
pub action: String,
pub previous_status: Option<ConsentStatus>,
pub new_status: ConsentStatus,
pub action_source: String,
pub source_ip: Option<String>,
pub details: HashMap<String, String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsentSummary {
pub subject_id: String,
pub consents: HashMap<ConsentType, ConsentStatus>,
pub is_valid: bool,
pub last_updated: DateTime<Utc>,
pub pending_requests: usize,
pub expired_consents: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consent_record_creation() {
let record = ConsentRecord::new(
"user123".to_string(),
ConsentType::DataProcessing,
LegalBasis::Consent,
"Process user authentication data".to_string(),
"web_form".to_string(),
);
assert_eq!(record.subject_id, "user123");
assert_eq!(record.consent_type, ConsentType::DataProcessing);
assert_eq!(record.status, ConsentStatus::Pending);
assert_eq!(record.legal_basis, LegalBasis::Consent);
assert!(record.granted_at.is_none());
assert!(!record.is_valid());
}
#[test]
fn test_consent_grant_and_withdraw() {
let mut record = ConsentRecord::new(
"user123".to_string(),
ConsentType::Analytics,
LegalBasis::Consent,
"Analytics tracking".to_string(),
"api".to_string(),
);
record.grant(Some("192.168.1.100".to_string()));
assert_eq!(record.status, ConsentStatus::Granted);
assert!(record.granted_at.is_some());
assert!(record.is_valid());
record.withdraw(Some("192.168.1.100".to_string()));
assert_eq!(record.status, ConsentStatus::Withdrawn);
assert!(record.withdrawn_at.is_some());
assert!(!record.is_valid());
}
#[test]
fn test_consent_expiration() {
let mut record = ConsentRecord::new(
"user123".to_string(),
ConsentType::Marketing,
LegalBasis::Consent,
"Marketing emails".to_string(),
"web_form".to_string(),
);
record.grant(None);
assert!(record.is_valid());
record.set_expiration(Utc::now() - chrono::Duration::hours(1));
assert!(!record.is_valid());
assert!(record.is_expired());
}
#[test]
fn test_consent_type_display() {
assert_eq!(ConsentType::DataProcessing.to_string(), "Data Processing");
assert_eq!(
ConsentType::Custom("Special Processing".to_string()).to_string(),
"Custom: Special Processing"
);
}
#[test]
fn test_legal_basis_display() {
assert_eq!(LegalBasis::Consent.to_string(), "Consent (GDPR 6.1.a)");
assert_eq!(
LegalBasis::LegitimateInterests.to_string(),
"Legitimate Interests (GDPR 6.1.f)"
);
}
#[test]
fn test_data_categories() {
let mut record = ConsentRecord::new(
"user123".to_string(),
ConsentType::DataProcessing,
LegalBasis::Consent,
"User data processing".to_string(),
"api".to_string(),
);
record.add_data_category("personal_identifiers".to_string());
record.add_data_category("authentication_data".to_string());
record.add_data_category("personal_identifiers".to_string());
assert_eq!(record.data_categories.len(), 2);
assert!(
record
.data_categories
.contains(&"personal_identifiers".to_string())
);
assert!(
record
.data_categories
.contains(&"authentication_data".to_string())
);
}
}