Files
bakery-ia/services/auth/app/models/consent.py

111 lines
4.1 KiB
Python
Raw Permalink Normal View History

2025-10-16 07:28:04 +02:00
"""
User consent tracking models for GDPR compliance
"""
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Index
from sqlalchemy.dialects.postgresql import UUID, JSON
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class UserConsent(Base):
"""
Tracks user consent for various data processing activities
GDPR Article 7 - Conditions for consent
"""
__tablename__ = "user_consents"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Consent types
terms_accepted = Column(Boolean, nullable=False, default=False)
privacy_accepted = Column(Boolean, nullable=False, default=False)
marketing_consent = Column(Boolean, nullable=False, default=False)
analytics_consent = Column(Boolean, nullable=False, default=False)
# Consent metadata
consent_version = Column(String(20), nullable=False, default="1.0")
consent_method = Column(String(50), nullable=False) # registration, settings_update, cookie_banner
ip_address = Column(String(45), nullable=True)
user_agent = Column(Text, nullable=True)
# Consent text at time of acceptance
terms_text_hash = Column(String(64), nullable=True)
privacy_text_hash = Column(String(64), nullable=True)
# Timestamps
consented_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
withdrawn_at = Column(DateTime(timezone=True), nullable=True)
# Additional metadata (renamed from 'metadata' to avoid SQLAlchemy reserved word)
extra_data = Column(JSON, nullable=True)
__table_args__ = (
Index('idx_user_consent_user_id', 'user_id'),
Index('idx_user_consent_consented_at', 'consented_at'),
)
def __repr__(self):
return f"<UserConsent(user_id={self.user_id}, version={self.consent_version})>"
def to_dict(self):
return {
"id": str(self.id),
"user_id": str(self.user_id),
"terms_accepted": self.terms_accepted,
"privacy_accepted": self.privacy_accepted,
"marketing_consent": self.marketing_consent,
"analytics_consent": self.analytics_consent,
"consent_version": self.consent_version,
"consent_method": self.consent_method,
"consented_at": self.consented_at.isoformat() if self.consented_at else None,
"withdrawn_at": self.withdrawn_at.isoformat() if self.withdrawn_at else None,
}
class ConsentHistory(Base):
"""
Historical record of all consent changes
Provides audit trail for GDPR compliance
"""
__tablename__ = "consent_history"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
consent_id = Column(UUID(as_uuid=True), ForeignKey("user_consents.id", ondelete="SET NULL"), nullable=True)
# Action type
action = Column(String(50), nullable=False) # granted, updated, withdrawn, revoked
# Consent state at time of action
consent_snapshot = Column(JSON, nullable=False)
# Context
ip_address = Column(String(45), nullable=True)
user_agent = Column(Text, nullable=True)
consent_method = Column(String(50), nullable=True)
# Timestamp
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
__table_args__ = (
Index('idx_consent_history_user_id', 'user_id'),
Index('idx_consent_history_created_at', 'created_at'),
Index('idx_consent_history_action', 'action'),
)
def __repr__(self):
return f"<ConsentHistory(user_id={self.user_id}, action={self.action})>"
def to_dict(self):
return {
"id": str(self.id),
"user_id": str(self.user_id),
"action": self.action,
"consent_snapshot": self.consent_snapshot,
"created_at": self.created_at.isoformat() if self.created_at else None,
}