""" User consent management API endpoints for GDPR compliance """ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status, Request from pydantic import BaseModel, Field from datetime import datetime, timezone import structlog import hashlib from shared.auth.decorators import get_current_user_dep from app.core.database import get_db from app.models.consent import UserConsent, ConsentHistory from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_ logger = structlog.get_logger() router = APIRouter() class ConsentRequest(BaseModel): """Request model for granting/updating consent""" terms_accepted: bool = Field(..., description="Accept terms of service") privacy_accepted: bool = Field(..., description="Accept privacy policy") marketing_consent: bool = Field(default=False, description="Consent to marketing communications") analytics_consent: bool = Field(default=False, description="Consent to analytics cookies") consent_method: str = Field(..., description="How consent was given (registration, settings, cookie_banner)") consent_version: str = Field(default="1.0", description="Version of terms/privacy policy") class ConsentResponse(BaseModel): """Response model for consent data""" id: str user_id: str terms_accepted: bool privacy_accepted: bool marketing_consent: bool analytics_consent: bool consent_version: str consent_method: str consented_at: str withdrawn_at: Optional[str] class ConsentHistoryResponse(BaseModel): """Response model for consent history""" id: str user_id: str action: str consent_snapshot: dict created_at: str def hash_text(text: str) -> str: """Create hash of consent text for verification""" return hashlib.sha256(text.encode()).hexdigest() @router.post("/consent", response_model=ConsentResponse, status_code=status.HTTP_201_CREATED) async def record_consent( consent_data: ConsentRequest, request: Request, current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Record user consent for data processing GDPR Article 7 - Conditions for consent """ try: user_id = UUID(current_user["sub"]) ip_address = request.client.host if request.client else None user_agent = request.headers.get("user-agent") consent = UserConsent( user_id=user_id, terms_accepted=consent_data.terms_accepted, privacy_accepted=consent_data.privacy_accepted, marketing_consent=consent_data.marketing_consent, analytics_consent=consent_data.analytics_consent, consent_version=consent_data.consent_version, consent_method=consent_data.consent_method, ip_address=ip_address, user_agent=user_agent, consented_at=datetime.now(timezone.utc) ) db.add(consent) await db.flush() history = ConsentHistory( user_id=user_id, consent_id=consent.id, action="granted", consent_snapshot=consent_data.dict(), ip_address=ip_address, user_agent=user_agent, consent_method=consent_data.consent_method, created_at=datetime.now(timezone.utc) ) db.add(history) await db.commit() await db.refresh(consent) logger.info( "consent_recorded", user_id=str(user_id), consent_version=consent_data.consent_version, method=consent_data.consent_method ) return ConsentResponse( id=str(consent.id), user_id=str(consent.user_id), terms_accepted=consent.terms_accepted, privacy_accepted=consent.privacy_accepted, marketing_consent=consent.marketing_consent, analytics_consent=consent.analytics_consent, consent_version=consent.consent_version, consent_method=consent.consent_method, consented_at=consent.consented_at.isoformat(), withdrawn_at=consent.withdrawn_at.isoformat() if consent.withdrawn_at else None ) except Exception as e: await db.rollback() logger.error("error_recording_consent", error=str(e), user_id=current_user.get("sub")) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to record consent" ) @router.get("/consent/current", response_model=Optional[ConsentResponse]) async def get_current_consent( current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Get current active consent for user """ try: user_id = UUID(current_user["sub"]) query = select(UserConsent).where( and_( UserConsent.user_id == user_id, UserConsent.withdrawn_at.is_(None) ) ).order_by(UserConsent.consented_at.desc()) result = await db.execute(query) consent = result.scalar_one_or_none() if not consent: return None return ConsentResponse( id=str(consent.id), user_id=str(consent.user_id), terms_accepted=consent.terms_accepted, privacy_accepted=consent.privacy_accepted, marketing_consent=consent.marketing_consent, analytics_consent=consent.analytics_consent, consent_version=consent.consent_version, consent_method=consent.consent_method, consented_at=consent.consented_at.isoformat(), withdrawn_at=consent.withdrawn_at.isoformat() if consent.withdrawn_at else None ) except Exception as e: logger.error("error_getting_consent", error=str(e), user_id=current_user.get("sub")) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve consent" ) @router.get("/consent/history", response_model=List[ConsentHistoryResponse]) async def get_consent_history( current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Get complete consent history for user GDPR Article 7(1) - Demonstrating consent """ try: user_id = UUID(current_user["sub"]) query = select(ConsentHistory).where( ConsentHistory.user_id == user_id ).order_by(ConsentHistory.created_at.desc()) result = await db.execute(query) history = result.scalars().all() return [ ConsentHistoryResponse( id=str(h.id), user_id=str(h.user_id), action=h.action, consent_snapshot=h.consent_snapshot, created_at=h.created_at.isoformat() ) for h in history ] except Exception as e: logger.error("error_getting_consent_history", error=str(e), user_id=current_user.get("sub")) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve consent history" ) @router.put("/consent", response_model=ConsentResponse) async def update_consent( consent_data: ConsentRequest, request: Request, current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Update user consent preferences GDPR Article 7(3) - Withdrawal of consent """ try: user_id = UUID(current_user["sub"]) query = select(UserConsent).where( and_( UserConsent.user_id == user_id, UserConsent.withdrawn_at.is_(None) ) ).order_by(UserConsent.consented_at.desc()) result = await db.execute(query) current_consent = result.scalar_one_or_none() if current_consent: current_consent.withdrawn_at = datetime.now(timezone.utc) history = ConsentHistory( user_id=user_id, consent_id=current_consent.id, action="updated", consent_snapshot=current_consent.to_dict(), ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), consent_method=consent_data.consent_method, created_at=datetime.now(timezone.utc) ) db.add(history) new_consent = UserConsent( user_id=user_id, terms_accepted=consent_data.terms_accepted, privacy_accepted=consent_data.privacy_accepted, marketing_consent=consent_data.marketing_consent, analytics_consent=consent_data.analytics_consent, consent_version=consent_data.consent_version, consent_method=consent_data.consent_method, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), consented_at=datetime.now(timezone.utc) ) db.add(new_consent) await db.flush() history = ConsentHistory( user_id=user_id, consent_id=new_consent.id, action="granted" if not current_consent else "updated", consent_snapshot=consent_data.dict(), ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), consent_method=consent_data.consent_method, created_at=datetime.now(timezone.utc) ) db.add(history) await db.commit() await db.refresh(new_consent) logger.info( "consent_updated", user_id=str(user_id), consent_version=consent_data.consent_version ) return ConsentResponse( id=str(new_consent.id), user_id=str(new_consent.user_id), terms_accepted=new_consent.terms_accepted, privacy_accepted=new_consent.privacy_accepted, marketing_consent=new_consent.marketing_consent, analytics_consent=new_consent.analytics_consent, consent_version=new_consent.consent_version, consent_method=new_consent.consent_method, consented_at=new_consent.consented_at.isoformat(), withdrawn_at=None ) except Exception as e: await db.rollback() logger.error("error_updating_consent", error=str(e), user_id=current_user.get("sub")) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update consent" ) @router.post("/consent/withdraw", status_code=status.HTTP_200_OK) async def withdraw_consent( request: Request, current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Withdraw all consent GDPR Article 7(3) - Right to withdraw consent """ try: user_id = UUID(current_user["sub"]) query = select(UserConsent).where( and_( UserConsent.user_id == user_id, UserConsent.withdrawn_at.is_(None) ) ) result = await db.execute(query) consents = result.scalars().all() for consent in consents: consent.withdrawn_at = datetime.now(timezone.utc) history = ConsentHistory( user_id=user_id, consent_id=consent.id, action="withdrawn", consent_snapshot=consent.to_dict(), ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), consent_method="user_withdrawal", created_at=datetime.now(timezone.utc) ) db.add(history) await db.commit() logger.info("consent_withdrawn", user_id=str(user_id), count=len(consents)) return { "message": "Consent withdrawn successfully", "withdrawn_count": len(consents) } except Exception as e: await db.rollback() logger.error("error_withdrawing_consent", error=str(e), user_id=current_user.get("sub")) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to withdraw consent" )