Files
bakery-ia/services/auth/app/api/account_deletion.py
2025-10-27 16:33:26 +01:00

215 lines
8.0 KiB
Python

"""
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"
)