Improve user delete flow

This commit is contained in:
Urtzi Alfaro
2025-08-02 17:09:53 +02:00
parent 277e8bec73
commit 3681429e11
10 changed files with 1334 additions and 210 deletions

View File

@@ -5,10 +5,14 @@
Complete notification API routes with full CRUD operations
"""
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, BackgroundTasks
from typing import List, Optional, Dict, Any
import structlog
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
import uuid
from sqlalchemy import select, delete, func
from app.schemas.notifications import (
NotificationCreate,
@@ -39,6 +43,8 @@ from shared.auth.decorators import (
require_role
)
from app.core.database import get_db
router = APIRouter()
logger = structlog.get_logger()
@@ -515,4 +521,355 @@ async def test_send_whatsapp(
except Exception as e:
logger.error("Failed to send test WhatsApp", error=str(e))
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.post("/users/{user_id}/notifications/cancel-pending")
async def cancel_pending_user_notifications(
user_id: str,
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_role(["admin"])),
db: AsyncSession = Depends(get_db)
):
"""Cancel all pending notifications for a user (admin only)"""
try:
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
try:
from app.models.notifications import NotificationQueue, NotificationLog
# Find pending notifications
pending_notifications_query = select(NotificationQueue).where(
NotificationQueue.user_id == user_uuid,
NotificationQueue.status.in_(["pending", "queued", "scheduled"])
)
pending_notifications_result = await db.execute(pending_notifications_query)
pending_notifications = pending_notifications_result.scalars().all()
notifications_cancelled = 0
cancelled_notification_ids = []
errors = []
for notification in pending_notifications:
try:
notification.status = "cancelled"
notification.updated_at = datetime.utcnow()
notification.cancelled_by = current_user.get("user_id")
notifications_cancelled += 1
cancelled_notification_ids.append(str(notification.id))
logger.info("Cancelled pending notification",
notification_id=str(notification.id),
user_id=user_id)
except Exception as e:
error_msg = f"Failed to cancel notification {notification.id}: {str(e)}"
errors.append(error_msg)
logger.error(error_msg)
if notifications_cancelled > 0:
await db.commit()
return {
"success": True,
"user_id": user_id,
"notifications_cancelled": notifications_cancelled,
"cancelled_notification_ids": cancelled_notification_ids,
"errors": errors,
"cancelled_at": datetime.utcnow().isoformat()
}
except Exception as e:
await db.rollback()
logger.error("Failed to cancel pending user notifications",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to cancel pending notifications"
)
@router.delete("/users/{user_id}/notification-data")
async def delete_user_notification_data(
user_id: str,
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_role(["admin"])),
db: AsyncSession = Depends(get_db)
):
"""Delete all notification data for a user (admin only)"""
try:
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
try:
from app.models.notifications import (
NotificationPreference,
NotificationQueue,
NotificationLog,
DeliveryAttempt
)
deletion_stats = {
"user_id": user_id,
"deleted_at": datetime.utcnow().isoformat(),
"preferences_deleted": 0,
"notifications_deleted": 0,
"logs_deleted": 0,
"delivery_attempts_deleted": 0,
"errors": []
}
# Delete delivery attempts first (they reference notifications)
try:
delivery_attempts_query = select(DeliveryAttempt).join(
NotificationQueue, DeliveryAttempt.notification_id == NotificationQueue.id
).where(NotificationQueue.user_id == user_uuid)
delivery_attempts_result = await db.execute(delivery_attempts_query)
delivery_attempts = delivery_attempts_result.scalars().all()
for attempt in delivery_attempts:
await db.delete(attempt)
deletion_stats["delivery_attempts_deleted"] = len(delivery_attempts)
except Exception as e:
error_msg = f"Error deleting delivery attempts: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
# Delete notification queue entries
try:
notifications_delete_query = delete(NotificationQueue).where(
NotificationQueue.user_id == user_uuid
)
notifications_delete_result = await db.execute(notifications_delete_query)
deletion_stats["notifications_deleted"] = notifications_delete_result.rowcount
except Exception as e:
error_msg = f"Error deleting notifications: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
# Delete notification logs
try:
logs_delete_query = delete(NotificationLog).where(
NotificationLog.user_id == user_uuid
)
logs_delete_result = await db.execute(logs_delete_query)
deletion_stats["logs_deleted"] = logs_delete_result.rowcount
except Exception as e:
error_msg = f"Error deleting notification logs: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
# Delete notification preferences
try:
preferences_delete_query = delete(NotificationPreference).where(
NotificationPreference.user_id == user_uuid
)
preferences_delete_result = await db.execute(preferences_delete_query)
deletion_stats["preferences_deleted"] = preferences_delete_result.rowcount
except Exception as e:
error_msg = f"Error deleting notification preferences: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
await db.commit()
logger.info("Deleted user notification data",
user_id=user_id,
preferences=deletion_stats["preferences_deleted"],
notifications=deletion_stats["notifications_deleted"],
logs=deletion_stats["logs_deleted"])
deletion_stats["success"] = len(deletion_stats["errors"]) == 0
return deletion_stats
except Exception as e:
await db.rollback()
logger.error("Failed to delete user notification data",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete user notification data"
)
@router.post("/notifications/user-deletion")
async def send_user_deletion_notification(
notification_data: dict, # {"admin_email": str, "deleted_user_email": str, "deletion_summary": dict}
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_role(["admin"])),
db: AsyncSession = Depends(get_db)
):
"""Send notification about user deletion to admins (admin only)"""
try:
admin_email = notification_data.get("admin_email")
deleted_user_email = notification_data.get("deleted_user_email")
deletion_summary = notification_data.get("deletion_summary", {})
if not admin_email or not deleted_user_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="admin_email and deleted_user_email are required"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid request data: {str(e)}"
)
try:
from app.models.notifications import NotificationQueue
from app.services.notification_service import NotificationService
# Create notification for the admin about the user deletion
notification_content = {
"subject": f"Admin User Deletion Completed - {deleted_user_email}",
"message": f"""
Admin User Deletion Summary
Deleted User: {deleted_user_email}
Deletion Performed By: {admin_email}
Deletion Date: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
Summary:
- Tenants Affected: {deletion_summary.get('total_tenants_affected', 0)}
- Models Deleted: {deletion_summary.get('total_models_deleted', 0)}
- Forecasts Deleted: {deletion_summary.get('total_forecasts_deleted', 0)}
- Notifications Deleted: {deletion_summary.get('total_notifications_deleted', 0)}
- Tenants Transferred: {deletion_summary.get('tenants_transferred', 0)}
- Tenants Deleted: {deletion_summary.get('tenants_deleted', 0)}
Status: {'Success' if deletion_summary.get('deletion_successful', False) else 'Completed with errors'}
Total Errors: {deletion_summary.get('total_errors', 0)}
This action was performed through the admin user deletion system and all associated data has been permanently removed.
""".strip(),
"notification_type": "user_deletion_admin",
"priority": "high"
}
# Create notification queue entry
notification = NotificationQueue(
user_email=admin_email,
notification_type="user_deletion_admin",
subject=notification_content["subject"],
message=notification_content["message"],
priority="high",
status="pending",
created_at=datetime.utcnow(),
metadata={
"deleted_user_email": deleted_user_email,
"deletion_summary": deletion_summary,
"performed_by": current_user.get("user_id")
}
)
db.add(notification)
await db.commit()
# Trigger immediate sending (assuming NotificationService exists)
try:
notification_service = NotificationService(db)
await notification_service.process_pending_notification(notification.id)
except Exception as e:
logger.warning("Failed to immediately send notification, will be processed by background worker",
error=str(e))
logger.info("Created user deletion notification",
admin_email=admin_email,
deleted_user=deleted_user_email,
notification_id=str(notification.id))
return {
"success": True,
"message": "User deletion notification created successfully",
"notification_id": str(notification.id),
"recipient": admin_email,
"created_at": datetime.utcnow().isoformat()
}
except Exception as e:
await db.rollback()
logger.error("Failed to send user deletion notification",
admin_email=admin_email,
deleted_user=deleted_user_email,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send user deletion notification"
)
@router.get("/users/{user_id}/notification-data/count")
async def get_user_notification_data_count(
user_id: str,
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_role(["admin"])),
db: AsyncSession = Depends(get_db)
):
"""Get count of notification data for a user (admin only)"""
try:
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
try:
from app.models.notifications import (
NotificationPreference,
NotificationQueue,
NotificationLog
)
# Count preferences
preferences_count_query = select(func.count(NotificationPreference.id)).where(
NotificationPreference.user_id == user_uuid
)
preferences_count_result = await db.execute(preferences_count_query)
preferences_count = preferences_count_result.scalar()
# Count notifications
notifications_count_query = select(func.count(NotificationQueue.id)).where(
NotificationQueue.user_id == user_uuid
)
notifications_count_result = await db.execute(notifications_count_query)
notifications_count = notifications_count_result.scalar()
# Count logs
logs_count_query = select(func.count(NotificationLog.id)).where(
NotificationLog.user_id == user_uuid
)
logs_count_result = await db.execute(logs_count_query)
logs_count = logs_count_result.scalar()
return {
"user_id": user_id,
"preferences_count": preferences_count,
"notifications_count": notifications_count,
"logs_count": logs_count,
"total_notification_data": preferences_count + notifications_count + logs_count
}
except Exception as e:
logger.error("Failed to get user notification data count",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get notification data count"
)