""" 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 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() 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.delete("/api/v1/auth/me/account") 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["user_id"]) 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("user_id"), error=str(e) ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to process account deletion request" ) @router.get("/api/v1/auth/me/account/deletion-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["user_id"]) 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("user_id"), error=str(e) ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve deletion information" )