Add user delete process

This commit is contained in:
Urtzi Alfaro
2025-10-31 11:54:19 +01:00
parent 63f5c6d512
commit 269d3b5032
74 changed files with 16783 additions and 213 deletions

View File

@@ -8,6 +8,7 @@ import json
import structlog
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional, Dict, Any
from uuid import UUID
from sse_starlette.sse import EventSourceResponse
@@ -18,8 +19,9 @@ from app.schemas.notifications import (
from app.services.notification_service import EnhancedNotificationService
from app.models.notifications import NotificationType as ModelNotificationType
from app.models import AuditLog
from app.core.database import get_db
from shared.auth.decorators import get_current_user_dep, get_current_user
from shared.auth.access_control import require_user_role, admin_role_required
from shared.auth.access_control import require_user_role, admin_role_required, service_only_access
from shared.routing.route_builder import RouteBuilder
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
@@ -764,3 +766,207 @@ async def get_sse_status(
except Exception as e:
logger.error("Failed to get SSE status", tenant_id=tenant_id, error=str(e))
raise HTTPException(500, "Failed to get SSE status")
# ============================================================================
# Tenant Data Deletion Operations (Internal Service Only)
# ============================================================================
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID to delete data for"),
current_user: dict = Depends(get_current_user_dep)
):
"""
Delete all notification data for a tenant (Internal service only)
This endpoint is called by the orchestrator during tenant deletion.
It permanently deletes all notification-related data including:
- Notifications (all types and statuses)
- Notification logs
- User notification preferences
- Tenant-specific notification templates
- Audit logs
**NOTE**: System templates (is_system=True) are preserved
**WARNING**: This operation is irreversible!
Returns:
Deletion summary with counts of deleted records
"""
from app.services.tenant_deletion_service import NotificationTenantDeletionService
from app.core.config import settings
try:
logger.info("notification.tenant_deletion.api_called", tenant_id=tenant_id)
db_manager = create_database_manager(settings.DATABASE_URL, "notification")
async with db_manager.get_session() as session:
deletion_service = NotificationTenantDeletionService(session)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant data deletion failed: {', '.join(result.errors)}"
)
return {
"message": "Tenant data deletion completed successfully",
"note": "System templates have been preserved",
"summary": result.to_dict()
}
except HTTPException:
raise
except Exception as e:
logger.error("notification.tenant_deletion.api_error",
tenant_id=tenant_id,
error=str(e),
exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to delete tenant data: {str(e)}"
)
@router.get(
route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID to preview deletion for"),
current_user: dict = Depends(get_current_user_dep)
):
"""
Preview what data would be deleted for a tenant (dry-run)
This endpoint shows counts of all data that would be deleted
without actually deleting anything. Useful for:
- Confirming deletion scope before execution
- Auditing and compliance
- Troubleshooting
Returns:
Dictionary with entity names and their counts
"""
from app.services.tenant_deletion_service import NotificationTenantDeletionService
from app.core.config import settings
try:
logger.info("notification.tenant_deletion.preview_called", tenant_id=tenant_id)
db_manager = create_database_manager(settings.DATABASE_URL, "notification")
async with db_manager.get_session() as session:
deletion_service = NotificationTenantDeletionService(session)
preview = await deletion_service.get_tenant_data_preview(tenant_id)
total_records = sum(preview.values())
return {
"tenant_id": tenant_id,
"service": "notification",
"preview": preview,
"total_records": total_records,
"note": "System templates are not counted and will be preserved",
"warning": "These records will be permanently deleted and cannot be recovered"
}
except Exception as e:
logger.error("notification.tenant_deletion.preview_error",
tenant_id=tenant_id,
error=str(e),
exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to preview tenant data deletion: {str(e)}"
)
# ============================================================================
# Tenant Data Deletion Operations (Internal Service Only)
# ============================================================================
from shared.auth.access_control import service_only_access
from app.services.tenant_deletion_service import NotificationTenantDeletionService
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID to delete data for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all notification data for a tenant (Internal service only)
"""
try:
logger.info("notification.tenant_deletion.api_called", tenant_id=tenant_id)
deletion_service = NotificationTenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant data deletion failed: {', '.join(result.errors)}"
)
return {
"message": "Tenant data deletion completed successfully",
"summary": result.to_dict()
}
except HTTPException:
raise
except Exception as e:
logger.error("notification.tenant_deletion.api_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to delete tenant data: {str(e)}")
@router.get(
route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID to preview deletion for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
"""
try:
logger.info("notification.tenant_deletion.preview_called", tenant_id=tenant_id)
deletion_service = NotificationTenantDeletionService(db)
result = await deletion_service.preview_deletion(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant deletion preview failed: {', '.join(result.errors)}"
)
return {
"tenant_id": tenant_id,
"service": "notification-service",
"data_counts": result.deleted_counts,
"total_items": sum(result.deleted_counts.values())
}
except HTTPException:
raise
except Exception as e:
logger.error("notification.tenant_deletion.preview_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")

View File

@@ -0,0 +1,245 @@
# services/notification/app/services/tenant_deletion_service.py
"""
Tenant Data Deletion Service for Notification Service
Handles deletion of all notification-related data for a tenant
"""
from typing import Dict
from sqlalchemy import select, func, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.dialects.postgresql import UUID
import structlog
from shared.services.tenant_deletion import (
BaseTenantDataDeletionService,
TenantDataDeletionResult
)
from app.models import (
Notification,
NotificationTemplate,
NotificationPreference,
NotificationLog,
AuditLog
)
logger = structlog.get_logger(__name__)
class NotificationTenantDeletionService(BaseTenantDataDeletionService):
"""Service for deleting all notification-related data for a tenant"""
def __init__(self, db: AsyncSession):
self.db = db
self.service_name = "notification"
async def get_tenant_data_preview(self, tenant_id: str) -> Dict[str, int]:
"""
Get counts of what would be deleted for a tenant (dry-run)
Args:
tenant_id: The tenant ID to preview deletion for
Returns:
Dictionary with entity names and their counts
"""
logger.info("notification.tenant_deletion.preview", tenant_id=tenant_id)
preview = {}
try:
# Count notifications
notification_count = await self.db.scalar(
select(func.count(Notification.id)).where(
Notification.tenant_id == UUID(tenant_id)
)
)
preview["notifications"] = notification_count or 0
# Count tenant-specific notification templates
template_count = await self.db.scalar(
select(func.count(NotificationTemplate.id)).where(
NotificationTemplate.tenant_id == UUID(tenant_id),
NotificationTemplate.is_system == False # Don't delete system templates
)
)
preview["notification_templates"] = template_count or 0
# Count notification preferences
preference_count = await self.db.scalar(
select(func.count(NotificationPreference.id)).where(
NotificationPreference.tenant_id == UUID(tenant_id)
)
)
preview["notification_preferences"] = preference_count or 0
# Count notification logs
log_count = await self.db.scalar(
select(func.count(NotificationLog.id)).where(
NotificationLog.tenant_id == UUID(tenant_id)
)
)
preview["notification_logs"] = log_count or 0
# Count audit logs
audit_count = await self.db.scalar(
select(func.count(AuditLog.id)).where(
AuditLog.tenant_id == UUID(tenant_id)
)
)
preview["audit_logs"] = audit_count or 0
logger.info(
"notification.tenant_deletion.preview_complete",
tenant_id=tenant_id,
preview=preview
)
except Exception as e:
logger.error(
"notification.tenant_deletion.preview_error",
tenant_id=tenant_id,
error=str(e),
exc_info=True
)
raise
return preview
async def delete_tenant_data(self, tenant_id: str) -> TenantDataDeletionResult:
"""
Permanently delete all notification data for a tenant
Deletion order:
1. NotificationLog (independent)
2. NotificationPreference (independent)
3. Notification (main records)
4. NotificationTemplate (only tenant-specific, preserve system templates)
5. AuditLog (independent)
Note: System templates (is_system=True) are NOT deleted
Args:
tenant_id: The tenant ID to delete data for
Returns:
TenantDataDeletionResult with deletion counts and any errors
"""
logger.info("notification.tenant_deletion.started", tenant_id=tenant_id)
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=self.service_name)
try:
# Step 1: Delete notification logs
logger.info("notification.tenant_deletion.deleting_logs", tenant_id=tenant_id)
logs_result = await self.db.execute(
delete(NotificationLog).where(
NotificationLog.tenant_id == UUID(tenant_id)
)
)
result.deleted_counts["notification_logs"] = logs_result.rowcount
logger.info(
"notification.tenant_deletion.logs_deleted",
tenant_id=tenant_id,
count=logs_result.rowcount
)
# Step 2: Delete notification preferences
logger.info("notification.tenant_deletion.deleting_preferences", tenant_id=tenant_id)
preferences_result = await self.db.execute(
delete(NotificationPreference).where(
NotificationPreference.tenant_id == UUID(tenant_id)
)
)
result.deleted_counts["notification_preferences"] = preferences_result.rowcount
logger.info(
"notification.tenant_deletion.preferences_deleted",
tenant_id=tenant_id,
count=preferences_result.rowcount
)
# Step 3: Delete notifications
logger.info("notification.tenant_deletion.deleting_notifications", tenant_id=tenant_id)
notifications_result = await self.db.execute(
delete(Notification).where(
Notification.tenant_id == UUID(tenant_id)
)
)
result.deleted_counts["notifications"] = notifications_result.rowcount
logger.info(
"notification.tenant_deletion.notifications_deleted",
tenant_id=tenant_id,
count=notifications_result.rowcount
)
# Step 4: Delete tenant-specific templates (preserve system templates)
logger.info("notification.tenant_deletion.deleting_templates", tenant_id=tenant_id)
templates_result = await self.db.execute(
delete(NotificationTemplate).where(
NotificationTemplate.tenant_id == UUID(tenant_id),
NotificationTemplate.is_system == False
)
)
result.deleted_counts["notification_templates"] = templates_result.rowcount
logger.info(
"notification.tenant_deletion.templates_deleted",
tenant_id=tenant_id,
count=templates_result.rowcount,
note="System templates preserved"
)
# Step 5: Delete audit logs
logger.info("notification.tenant_deletion.deleting_audit_logs", tenant_id=tenant_id)
audit_result = await self.db.execute(
delete(AuditLog).where(
AuditLog.tenant_id == UUID(tenant_id)
)
)
result.deleted_counts["audit_logs"] = audit_result.rowcount
logger.info(
"notification.tenant_deletion.audit_logs_deleted",
tenant_id=tenant_id,
count=audit_result.rowcount
)
# Commit the transaction
await self.db.commit()
# Calculate total deleted
total_deleted = sum(result.deleted_counts.values())
logger.info(
"notification.tenant_deletion.completed",
tenant_id=tenant_id,
total_deleted=total_deleted,
breakdown=result.deleted_counts,
note="System templates preserved"
)
result.success = True
except Exception as e:
await self.db.rollback()
error_msg = f"Failed to delete notification data for tenant {tenant_id}: {str(e)}"
logger.error(
"notification.tenant_deletion.failed",
tenant_id=tenant_id,
error=str(e),
exc_info=True
)
result.errors.append(error_msg)
result.success = False
return result
def get_notification_tenant_deletion_service(
db: AsyncSession
) -> NotificationTenantDeletionService:
"""
Factory function to create NotificationTenantDeletionService instance
Args:
db: AsyncSession database session
Returns:
NotificationTenantDeletionService instance
"""
return NotificationTenantDeletionService(db)