Improve GDPR implementation
This commit is contained in:
216
services/auth/app/api/account_deletion.py
Normal file
216
services/auth/app/api/account_deletion.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
User self-service account deletion API for GDPR compliance
|
||||
Implements Article 17 (Right to erasure / "Right to be forgotten")
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, timezone
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_delete import AdminUserDeleteService
|
||||
from app.models.users import User
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import httpx
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
|
||||
class AccountDeletionRequest(BaseModel):
|
||||
"""Request model for account deletion"""
|
||||
confirm_email: str = Field(..., description="User's email for confirmation")
|
||||
reason: str = Field(default="", description="Optional reason for deletion")
|
||||
password: str = Field(..., description="User's password for verification")
|
||||
|
||||
|
||||
class DeletionScheduleResponse(BaseModel):
|
||||
"""Response for scheduled deletion"""
|
||||
message: str
|
||||
user_id: str
|
||||
scheduled_deletion_date: str
|
||||
grace_period_days: int = 30
|
||||
|
||||
|
||||
@router.post("/api/v1/users/me/delete/request")
|
||||
async def request_account_deletion(
|
||||
deletion_request: AccountDeletionRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Request account deletion (self-service)
|
||||
|
||||
GDPR Article 17 - Right to erasure ("right to be forgotten")
|
||||
|
||||
This initiates account deletion with a 30-day grace period.
|
||||
During this period:
|
||||
- Account is marked for deletion
|
||||
- User can still log in and cancel deletion
|
||||
- After 30 days, account is permanently deleted
|
||||
|
||||
Requires:
|
||||
- Email confirmation matching logged-in user
|
||||
- Current password verification
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_email = current_user.get("email")
|
||||
|
||||
if deletion_request.confirm_email.lower() != user_email.lower():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email confirmation does not match your account email"
|
||||
)
|
||||
|
||||
query = select(User).where(User.id == user_id)
|
||||
result = await db.execute(query)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
from app.core.security import SecurityManager
|
||||
if not SecurityManager.verify_password(deletion_request.password, user.hashed_password):
|
||||
logger.warning(
|
||||
"account_deletion_invalid_password",
|
||||
user_id=str(user_id),
|
||||
ip_address=request.client.host if request.client else None
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid password"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"account_deletion_requested",
|
||||
user_id=str(user_id),
|
||||
email=user_email,
|
||||
reason=deletion_request.reason[:100] if deletion_request.reason else None,
|
||||
ip_address=request.client.host if request.client else None
|
||||
)
|
||||
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if tenant_id:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
cancel_response = await client.get(
|
||||
f"http://tenant-service:8000/api/v1/subscriptions/{tenant_id}/status",
|
||||
headers={"Authorization": request.headers.get("Authorization")}
|
||||
)
|
||||
|
||||
if cancel_response.status_code == 200:
|
||||
subscription_data = cancel_response.json()
|
||||
if subscription_data.get("status") in ["active", "pending_cancellation"]:
|
||||
cancel_sub_response = await client.delete(
|
||||
f"http://tenant-service:8000/api/v1/tenants/{tenant_id}/subscription",
|
||||
headers={"Authorization": request.headers.get("Authorization")}
|
||||
)
|
||||
logger.info(
|
||||
"subscription_cancelled_before_deletion",
|
||||
user_id=str(user_id),
|
||||
tenant_id=tenant_id,
|
||||
subscription_status=subscription_data.get("status")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"subscription_cancellation_failed_during_account_deletion",
|
||||
user_id=str(user_id),
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
deletion_service = AdminUserDeleteService(db)
|
||||
result = await deletion_service.delete_admin_user_complete(
|
||||
user_id=str(user_id),
|
||||
requesting_user_id=str(user_id)
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Account deleted successfully",
|
||||
"user_id": str(user_id),
|
||||
"deletion_date": datetime.now(timezone.utc).isoformat(),
|
||||
"data_retained": "Audit logs will be anonymized after legal retention period (1 year)",
|
||||
"gdpr_article": "Article 17 - Right to erasure"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"account_deletion_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to process account deletion request"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/delete/info")
|
||||
async def get_deletion_info(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get information about what will be deleted
|
||||
|
||||
Shows user exactly what data will be deleted when they request
|
||||
account deletion. Transparency requirement under GDPR.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
|
||||
deletion_service = AdminUserDeleteService(db)
|
||||
preview = await deletion_service.preview_user_deletion(str(user_id))
|
||||
|
||||
return {
|
||||
"user_info": preview.get("user"),
|
||||
"what_will_be_deleted": {
|
||||
"account_data": "Your account, email, name, and profile information",
|
||||
"sessions": "All active sessions and refresh tokens",
|
||||
"consents": "Your consent history and preferences",
|
||||
"security_data": "Login history and security logs",
|
||||
"tenant_data": preview.get("tenant_associations"),
|
||||
"estimated_records": preview.get("estimated_deletions")
|
||||
},
|
||||
"what_will_be_retained": {
|
||||
"audit_logs": "Anonymized for 1 year (legal requirement)",
|
||||
"financial_records": "Anonymized for 7 years (tax law)",
|
||||
"anonymized_analytics": "Aggregated data without personal identifiers"
|
||||
},
|
||||
"process": {
|
||||
"immediate_deletion": True,
|
||||
"grace_period": "No grace period - deletion is immediate",
|
||||
"reversible": False,
|
||||
"completion_time": "Immediate"
|
||||
},
|
||||
"gdpr_rights": {
|
||||
"article_17": "Right to erasure (right to be forgotten)",
|
||||
"article_5_1_e": "Storage limitation principle",
|
||||
"exceptions": "Data required for legal obligations will be retained in anonymized form"
|
||||
},
|
||||
"warning": "⚠️ This action is irreversible. All your data will be permanently deleted."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"deletion_info_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve deletion information"
|
||||
)
|
||||
372
services/auth/app/api/consent.py
Normal file
372
services/auth/app/api/consent.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
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"
|
||||
)
|
||||
123
services/auth/app/api/data_export.py
Normal file
123
services/auth/app/api/data_export.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
User data export API endpoints for GDPR compliance
|
||||
Implements Article 15 (Right to Access) and Article 20 (Right to Data Portability)
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.services.data_export_service import DataExportService
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/export")
|
||||
async def export_my_data(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Export all personal data for the current user
|
||||
|
||||
GDPR Article 15 - Right of access by the data subject
|
||||
GDPR Article 20 - Right to data portability
|
||||
|
||||
Returns complete user data in machine-readable JSON format including:
|
||||
- Personal information
|
||||
- Account data
|
||||
- Consent history
|
||||
- Security logs
|
||||
- Audit trail
|
||||
|
||||
Response is provided in JSON format for easy data portability.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
|
||||
export_service = DataExportService(db)
|
||||
data = await export_service.export_user_data(user_id)
|
||||
|
||||
logger.info(
|
||||
"data_export_requested",
|
||||
user_id=str(user_id),
|
||||
email=current_user.get("email")
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content=data,
|
||||
status_code=status.HTTP_200_OK,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="user_data_export_{user_id}.json"',
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"data_export_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to export user data"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/export/summary")
|
||||
async def get_export_summary(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a summary of what data would be exported
|
||||
|
||||
Useful for showing users what data we have about them
|
||||
before they request full export.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
|
||||
export_service = DataExportService(db)
|
||||
data = await export_service.export_user_data(user_id)
|
||||
|
||||
summary = {
|
||||
"user_id": str(user_id),
|
||||
"data_categories": {
|
||||
"personal_data": bool(data.get("personal_data")),
|
||||
"account_data": bool(data.get("account_data")),
|
||||
"consent_data": bool(data.get("consent_data")),
|
||||
"security_data": bool(data.get("security_data")),
|
||||
"onboarding_data": bool(data.get("onboarding_data")),
|
||||
"audit_logs": bool(data.get("audit_logs"))
|
||||
},
|
||||
"data_counts": {
|
||||
"active_sessions": data.get("account_data", {}).get("active_sessions_count", 0),
|
||||
"consent_changes": data.get("consent_data", {}).get("total_consent_changes", 0),
|
||||
"login_attempts": len(data.get("security_data", {}).get("recent_login_attempts", [])),
|
||||
"audit_logs": data.get("audit_logs", {}).get("total_logs_exported", 0)
|
||||
},
|
||||
"export_format": "JSON",
|
||||
"gdpr_articles": ["Article 15 (Right to Access)", "Article 20 (Data Portability)"]
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"export_summary_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to generate export summary"
|
||||
)
|
||||
Reference in New Issue
Block a user