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"
|
||||
)
|
||||
Reference in New Issue
Block a user