Improve GDPR implementation

This commit is contained in:
Urtzi Alfaro
2025-10-16 07:28:04 +02:00
parent dbb48d8e2c
commit b6cb800758
37 changed files with 4876 additions and 307 deletions

View File

@@ -0,0 +1,187 @@
"""
User data export service for GDPR compliance
Implements Article 15 (Right to Access) and Article 20 (Right to Data Portability)
"""
from typing import Dict, Any, List
from uuid import UUID
from datetime import datetime, timezone
import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.users import User
from app.models.tokens import RefreshToken, LoginAttempt
from app.models.consent import UserConsent, ConsentHistory
from app.models.onboarding import UserOnboardingProgress
from app.models import AuditLog
logger = structlog.get_logger()
class DataExportService:
"""Service to export all user data in machine-readable format"""
def __init__(self, db: AsyncSession):
self.db = db
async def export_user_data(self, user_id: UUID) -> Dict[str, Any]:
"""
Export all user data from auth service
Returns data in structured JSON format
"""
try:
export_data = {
"export_metadata": {
"user_id": str(user_id),
"export_date": datetime.now(timezone.utc).isoformat(),
"data_controller": "Panadería IA",
"format_version": "1.0",
"gdpr_article": "Article 15 (Right to Access) & Article 20 (Data Portability)"
},
"personal_data": await self._export_personal_data(user_id),
"account_data": await self._export_account_data(user_id),
"consent_data": await self._export_consent_data(user_id),
"security_data": await self._export_security_data(user_id),
"onboarding_data": await self._export_onboarding_data(user_id),
"audit_logs": await self._export_audit_logs(user_id)
}
logger.info("data_export_completed", user_id=str(user_id))
return export_data
except Exception as e:
logger.error("data_export_failed", user_id=str(user_id), error=str(e))
raise
async def _export_personal_data(self, user_id: UUID) -> Dict[str, Any]:
"""Export personal identifiable information"""
query = select(User).where(User.id == user_id)
result = await self.db.execute(query)
user = result.scalar_one_or_none()
if not user:
return {}
return {
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"language": user.language,
"timezone": user.timezone,
"is_active": user.is_active,
"is_verified": user.is_verified,
"role": user.role,
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None
}
async def _export_account_data(self, user_id: UUID) -> Dict[str, Any]:
"""Export account-related data"""
query = select(RefreshToken).where(RefreshToken.user_id == user_id)
result = await self.db.execute(query)
tokens = result.scalars().all()
active_sessions = []
for token in tokens:
if token.expires_at > datetime.now(timezone.utc) and not token.revoked:
active_sessions.append({
"token_id": str(token.id),
"created_at": token.created_at.isoformat() if token.created_at else None,
"expires_at": token.expires_at.isoformat() if token.expires_at else None,
"device_info": token.device_info
})
return {
"active_sessions_count": len(active_sessions),
"active_sessions": active_sessions,
"total_tokens_issued": len(tokens)
}
async def _export_consent_data(self, user_id: UUID) -> Dict[str, Any]:
"""Export consent history"""
consent_query = select(UserConsent).where(UserConsent.user_id == user_id)
consent_result = await self.db.execute(consent_query)
consents = consent_result.scalars().all()
history_query = select(ConsentHistory).where(ConsentHistory.user_id == user_id)
history_result = await self.db.execute(history_query)
history = history_result.scalars().all()
return {
"current_consent": consents[0].to_dict() if consents else None,
"consent_history": [h.to_dict() for h in history],
"total_consent_changes": len(history)
}
async def _export_security_data(self, user_id: UUID) -> Dict[str, Any]:
"""Export security-related data"""
query = select(LoginAttempt).where(
LoginAttempt.user_id == user_id
).order_by(LoginAttempt.attempted_at.desc()).limit(50)
result = await self.db.execute(query)
attempts = result.scalars().all()
login_attempts = []
for attempt in attempts:
login_attempts.append({
"attempted_at": attempt.attempted_at.isoformat() if attempt.attempted_at else None,
"success": attempt.success,
"ip_address": attempt.ip_address,
"user_agent": attempt.user_agent,
"failure_reason": attempt.failure_reason
})
return {
"recent_login_attempts": login_attempts,
"total_attempts_exported": len(login_attempts),
"note": "Only last 50 login attempts included for data minimization"
}
async def _export_onboarding_data(self, user_id: UUID) -> Dict[str, Any]:
"""Export onboarding progress"""
query = select(UserOnboardingProgress).where(UserOnboardingProgress.user_id == user_id)
result = await self.db.execute(query)
progress = result.scalars().all()
return {
"onboarding_steps": [
{
"step_id": str(p.id),
"step_name": p.step_name,
"completed": p.completed,
"completed_at": p.completed_at.isoformat() if p.completed_at else None
}
for p in progress
]
}
async def _export_audit_logs(self, user_id: UUID) -> Dict[str, Any]:
"""Export audit logs related to user"""
query = select(AuditLog).where(
AuditLog.user_id == user_id
).order_by(AuditLog.created_at.desc()).limit(100)
result = await self.db.execute(query)
logs = result.scalars().all()
return {
"audit_trail": [
{
"log_id": str(log.id),
"action": log.action,
"resource_type": log.resource_type,
"resource_id": log.resource_id,
"severity": log.severity,
"description": log.description,
"ip_address": log.ip_address,
"created_at": log.created_at.isoformat() if log.created_at else None
}
for log in logs
],
"total_logs_exported": len(logs),
"note": "Only last 100 audit logs included for data minimization"
}