Add the initial user admin delete code
This commit is contained in:
@@ -2,19 +2,22 @@
|
|||||||
User management API routes
|
User management API routes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import structlog
|
import structlog
|
||||||
|
import uuid
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.schemas.auth import UserResponse, PasswordChange
|
from app.schemas.auth import UserResponse, PasswordChange
|
||||||
from app.schemas.users import UserUpdate
|
from app.schemas.users import UserUpdate
|
||||||
from app.services.user_service import UserService
|
from app.services.user_service import UserService
|
||||||
from app.models.users import User
|
from app.models.users import User
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.services.admin_delete import AdminUserDeleteService
|
||||||
|
|
||||||
# Import unified authentication from shared library
|
# Import unified authentication from shared library
|
||||||
from shared.auth.decorators import (
|
from shared.auth.decorators import (
|
||||||
get_current_user_dep,
|
get_current_user_dep,
|
||||||
@@ -116,4 +119,122 @@ async def update_current_user(
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update user"
|
detail="Failed to update user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.delete("/delete/users/{user_id}")
|
||||||
|
async def delete_admin_user(
|
||||||
|
user_id: str,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
current_user = Depends(get_current_user_dep),
|
||||||
|
#_admin_check = Depends(require_admin_role),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete an admin user and all associated data across all services.
|
||||||
|
|
||||||
|
This operation will:
|
||||||
|
1. Cancel any active training jobs for user's tenants
|
||||||
|
2. Delete all trained models and artifacts
|
||||||
|
3. Delete all forecasts and predictions
|
||||||
|
4. Delete notification preferences and logs
|
||||||
|
5. Handle tenant ownership (transfer or delete)
|
||||||
|
6. Delete user account and authentication data
|
||||||
|
|
||||||
|
**Warning: This operation is irreversible!**
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Validate user_id format
|
||||||
|
try:
|
||||||
|
uuid.UUID(user_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid user ID format"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prevent self-deletion
|
||||||
|
if user_id == current_user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot delete your own account"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize deletion service
|
||||||
|
deletion_service = AdminUserDeleteService(db)
|
||||||
|
|
||||||
|
# Perform the deletion
|
||||||
|
try:
|
||||||
|
result = await deletion_service.delete_admin_user_complete(
|
||||||
|
user_id=user_id,
|
||||||
|
requesting_user_id=current_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Admin user {user_id} has been successfully deleted",
|
||||||
|
"deletion_details": result
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Unexpected error during user deletion",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="An unexpected error occurred during user deletion"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/delete/users/{user_id}/deletion-preview")
|
||||||
|
async def preview_user_deletion(
|
||||||
|
user_id: str,
|
||||||
|
current_user = Depends(get_current_user_dep),
|
||||||
|
#_admin_check = Depends(require_admin_role),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Preview what data would be deleted for an admin user.
|
||||||
|
|
||||||
|
This endpoint provides a dry-run preview of the deletion operation
|
||||||
|
without actually deleting any data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
uuid.UUID(user_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid user ID format"
|
||||||
|
)
|
||||||
|
|
||||||
|
deletion_service = AdminUserDeleteService(db)
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user_info = await deletion_service._validate_admin_user(user_id)
|
||||||
|
if not user_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Admin user {user_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get tenant associations
|
||||||
|
tenant_info = await deletion_service._get_user_tenant_info(user_id)
|
||||||
|
|
||||||
|
# Build preview
|
||||||
|
preview = {
|
||||||
|
"user": user_info,
|
||||||
|
"tenant_associations": tenant_info,
|
||||||
|
"estimated_deletions": {
|
||||||
|
"training_models": "All models for associated tenants",
|
||||||
|
"forecasts": "All forecasts for associated tenants",
|
||||||
|
"notifications": "All user notification data",
|
||||||
|
"tenant_memberships": tenant_info['total_tenants'],
|
||||||
|
"owned_tenants": f"{tenant_info['owned_tenants']} (will be transferred or deleted)"
|
||||||
|
},
|
||||||
|
"warning": "This operation is irreversible and will permanently delete all associated data"
|
||||||
|
}
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|||||||
620
services/auth/app/services/admin_delete.py
Normal file
620
services/auth/app/services/admin_delete.py
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
# ================================================================
|
||||||
|
# Admin User Delete API - Complete Implementation
|
||||||
|
# ================================================================
|
||||||
|
"""
|
||||||
|
Complete admin user deletion API that handles all associated data
|
||||||
|
across all microservices in the bakery forecasting platform.
|
||||||
|
|
||||||
|
This implementation ensures proper cascade deletion of:
|
||||||
|
1. User account and authentication data
|
||||||
|
2. Tenant ownership and memberships
|
||||||
|
3. All training models and artifacts
|
||||||
|
4. Forecasts and predictions
|
||||||
|
5. Notification preferences and logs
|
||||||
|
6. Refresh tokens and sessions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, delete, text
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import structlog
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from shared.auth.decorators import get_current_user_dep
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.services.messaging import auth_publisher
|
||||||
|
from app.services.auth_service_clients import AuthServiceClientFactory
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class AdminUserDeleteService:
|
||||||
|
"""Service to handle complete admin user deletion across all microservices"""
|
||||||
|
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.clients = AuthServiceClientFactory(settings)
|
||||||
|
|
||||||
|
async def delete_admin_user_complete(self, user_id: str, requesting_user_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Complete admin user deletion with all associated data using inter-service clients
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID of the admin user to delete
|
||||||
|
requesting_user_id: ID of the user performing the deletion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with deletion results from all services
|
||||||
|
"""
|
||||||
|
|
||||||
|
deletion_results = {
|
||||||
|
'user_id': user_id,
|
||||||
|
'requested_by': requesting_user_id,
|
||||||
|
'started_at': datetime.utcnow().isoformat(),
|
||||||
|
'services_processed': {},
|
||||||
|
'errors': [],
|
||||||
|
'summary': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Validate user exists and is admin
|
||||||
|
user_info = await self._validate_admin_user(user_id)
|
||||||
|
if not user_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Admin user {user_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
deletion_results['user_info'] = user_info
|
||||||
|
|
||||||
|
# Step 2: Get all tenant associations using tenant client
|
||||||
|
tenant_info = await self._get_user_tenant_info(user_id)
|
||||||
|
deletion_results['tenant_associations'] = tenant_info
|
||||||
|
|
||||||
|
# Step 3: Delete in proper order to respect dependencies
|
||||||
|
|
||||||
|
# 3.1 Stop all active training jobs and delete models
|
||||||
|
training_result = await self._delete_training_data(tenant_info['tenant_ids'])
|
||||||
|
deletion_results['services_processed']['training'] = training_result
|
||||||
|
|
||||||
|
# 3.2 Delete all forecasts and predictions
|
||||||
|
forecasting_result = await self._delete_forecasting_data(tenant_info['tenant_ids'])
|
||||||
|
deletion_results['services_processed']['forecasting'] = forecasting_result
|
||||||
|
|
||||||
|
# 3.3 Delete notification preferences and logs
|
||||||
|
notification_result = await self._delete_notification_data(user_id)
|
||||||
|
deletion_results['services_processed']['notification'] = notification_result
|
||||||
|
|
||||||
|
# 3.4 Delete tenant memberships and handle owned tenants
|
||||||
|
tenant_result = await self._delete_tenant_data(user_id, tenant_info)
|
||||||
|
deletion_results['services_processed']['tenant'] = tenant_result
|
||||||
|
|
||||||
|
# 3.5 Finally delete user account and auth data
|
||||||
|
auth_result = await self._delete_auth_data(user_id)
|
||||||
|
deletion_results['services_processed']['auth'] = auth_result
|
||||||
|
|
||||||
|
# Step 4: Generate summary
|
||||||
|
deletion_results['summary'] = await self._generate_deletion_summary(deletion_results)
|
||||||
|
deletion_results['completed_at'] = datetime.utcnow().isoformat()
|
||||||
|
deletion_results['status'] = 'success'
|
||||||
|
|
||||||
|
# Step 5: Publish deletion event
|
||||||
|
await self._publish_user_deleted_event(user_id, deletion_results)
|
||||||
|
|
||||||
|
# Step 6: Send notification to admins
|
||||||
|
await self._notify_admins_of_deletion(user_info, deletion_results)
|
||||||
|
|
||||||
|
logger.info("Admin user deletion completed successfully",
|
||||||
|
user_id=user_id,
|
||||||
|
tenants_affected=len(tenant_info['tenant_ids']))
|
||||||
|
|
||||||
|
return deletion_results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
deletion_results['status'] = 'failed'
|
||||||
|
deletion_results['error'] = str(e)
|
||||||
|
deletion_results['completed_at'] = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
logger.error("Admin user deletion failed",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
|
||||||
|
# Attempt to publish failure event
|
||||||
|
try:
|
||||||
|
await self._publish_user_deletion_failed_event(user_id, str(e))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"User deletion failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _validate_admin_user(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Validate user exists and get basic info from local database"""
|
||||||
|
try:
|
||||||
|
from app.models.users import User
|
||||||
|
|
||||||
|
# Query user from local auth database
|
||||||
|
query = select(User).where(User.id == uuid.UUID(user_id))
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': str(user.id),
|
||||||
|
'email': user.email,
|
||||||
|
'full_name': user.full_name,
|
||||||
|
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||||
|
'is_active': user.is_active,
|
||||||
|
'is_verified': user.is_verified
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to validate admin user", user_id=user_id, error=str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _get_user_tenant_info(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""Get all tenant associations for the user using tenant client"""
|
||||||
|
try:
|
||||||
|
# Use tenant service client to get memberships
|
||||||
|
memberships = await self.clients.tenant_client.get_user_tenants(user_id)
|
||||||
|
|
||||||
|
if not memberships:
|
||||||
|
return {
|
||||||
|
'tenant_ids': [],
|
||||||
|
'total_tenants': 0,
|
||||||
|
'owned_tenants': 0,
|
||||||
|
'memberships': []
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant_ids = [m['tenant_id'] for m in memberships]
|
||||||
|
owned_tenants = [m for m in memberships if m.get('role') == 'owner']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tenant_ids': tenant_ids,
|
||||||
|
'total_tenants': len(tenant_ids),
|
||||||
|
'owned_tenants': len(owned_tenants),
|
||||||
|
'memberships': memberships
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant info", user_id=user_id, error=str(e))
|
||||||
|
return {'tenant_ids': [], 'total_tenants': 0, 'owned_tenants': 0, 'memberships': []}
|
||||||
|
|
||||||
|
async def _delete_training_data(self, tenant_ids: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Delete all training models, jobs, and artifacts for user's tenants"""
|
||||||
|
result = {
|
||||||
|
'models_deleted': 0,
|
||||||
|
'jobs_cancelled': 0,
|
||||||
|
'artifacts_deleted': 0,
|
||||||
|
'total_tenants_processed': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
for tenant_id in tenant_ids:
|
||||||
|
try:
|
||||||
|
# Cancel active training jobs using training client
|
||||||
|
cancel_result = await self.clients.training_client.cancel_tenant_training_jobs(tenant_id)
|
||||||
|
if cancel_result:
|
||||||
|
result['jobs_cancelled'] += cancel_result.get('jobs_cancelled', 0)
|
||||||
|
if cancel_result.get('errors'):
|
||||||
|
result['errors'].extend(cancel_result['errors'])
|
||||||
|
|
||||||
|
# Delete all models and artifacts using training client
|
||||||
|
delete_result = await self.clients.training_client.delete_tenant_models(tenant_id)
|
||||||
|
if delete_result:
|
||||||
|
result['models_deleted'] += delete_result.get('models_deleted', 0)
|
||||||
|
result['artifacts_deleted'] += delete_result.get('artifacts_deleted', 0)
|
||||||
|
if delete_result.get('errors'):
|
||||||
|
result['errors'].extend(delete_result['errors'])
|
||||||
|
|
||||||
|
result['total_tenants_processed'] += 1
|
||||||
|
|
||||||
|
logger.debug("Training data deleted for tenant",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
models=delete_result.get('models_deleted', 0) if delete_result else 0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error deleting training data for tenant {tenant_id}: {str(e)}"
|
||||||
|
result['errors'].append(error_msg)
|
||||||
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result['errors'].append(f"Training service communication error: {str(e)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _delete_forecasting_data(self, tenant_ids: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Delete all forecasts, predictions, and caches for user's tenants"""
|
||||||
|
result = {
|
||||||
|
'forecasts_deleted': 0,
|
||||||
|
'predictions_deleted': 0,
|
||||||
|
'cache_cleared': 0,
|
||||||
|
'batches_cancelled': 0,
|
||||||
|
'total_tenants_processed': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
for tenant_id in tenant_ids:
|
||||||
|
try:
|
||||||
|
# Cancel any active prediction batches
|
||||||
|
batch_result = await self.clients.forecasting_client.cancel_tenant_prediction_batches(tenant_id)
|
||||||
|
if batch_result:
|
||||||
|
result['batches_cancelled'] += batch_result.get('batches_cancelled', 0)
|
||||||
|
if batch_result.get('errors'):
|
||||||
|
result['errors'].extend(batch_result['errors'])
|
||||||
|
|
||||||
|
# Clear prediction cache
|
||||||
|
cache_result = await self.clients.forecasting_client.clear_tenant_prediction_cache(tenant_id)
|
||||||
|
if cache_result:
|
||||||
|
result['cache_cleared'] += cache_result.get('cache_cleared', 0)
|
||||||
|
if cache_result.get('errors'):
|
||||||
|
result['errors'].extend(cache_result['errors'])
|
||||||
|
|
||||||
|
# Delete all forecasts for tenant
|
||||||
|
delete_result = await self.clients.forecasting_client.delete_tenant_forecasts(tenant_id)
|
||||||
|
if delete_result:
|
||||||
|
result['forecasts_deleted'] += delete_result.get('forecasts_deleted', 0)
|
||||||
|
result['predictions_deleted'] += delete_result.get('predictions_deleted', 0)
|
||||||
|
if delete_result.get('errors'):
|
||||||
|
result['errors'].extend(delete_result['errors'])
|
||||||
|
|
||||||
|
result['total_tenants_processed'] += 1
|
||||||
|
|
||||||
|
logger.debug("Forecasting data deleted for tenant",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
forecasts=delete_result.get('forecasts_deleted', 0) if delete_result else 0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error deleting forecasting data for tenant {tenant_id}: {str(e)}"
|
||||||
|
result['errors'].append(error_msg)
|
||||||
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result['errors'].append(f"Forecasting service communication error: {str(e)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _delete_notification_data(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""Delete notification preferences, logs, and pending notifications"""
|
||||||
|
result = {
|
||||||
|
'preferences_deleted': 0,
|
||||||
|
'notifications_deleted': 0,
|
||||||
|
'notifications_cancelled': 0,
|
||||||
|
'logs_deleted': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Cancel pending notifications first
|
||||||
|
cancel_result = await self.clients.notification_client.cancel_pending_user_notifications(user_id)
|
||||||
|
if cancel_result:
|
||||||
|
result['notifications_cancelled'] = cancel_result.get('notifications_cancelled', 0)
|
||||||
|
if cancel_result.get('errors'):
|
||||||
|
result['errors'].extend(cancel_result['errors'])
|
||||||
|
|
||||||
|
# Delete all notification data for user
|
||||||
|
delete_result = await self.clients.notification_client.delete_user_notification_data(user_id)
|
||||||
|
if delete_result:
|
||||||
|
result['preferences_deleted'] = delete_result.get('preferences_deleted', 0)
|
||||||
|
result['notifications_deleted'] = delete_result.get('notifications_deleted', 0)
|
||||||
|
result['logs_deleted'] = delete_result.get('logs_deleted', 0)
|
||||||
|
if delete_result.get('errors'):
|
||||||
|
result['errors'].extend(delete_result['errors'])
|
||||||
|
|
||||||
|
logger.debug("Notification data deleted for user",
|
||||||
|
user_id=user_id,
|
||||||
|
notifications=result['notifications_deleted'])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result['errors'].append(f"Notification service communication error: {str(e)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _delete_tenant_data(self, user_id: str, tenant_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Delete tenant memberships and handle owned tenants using tenant client"""
|
||||||
|
result = {
|
||||||
|
'memberships_deleted': 0,
|
||||||
|
'tenants_deleted': 0,
|
||||||
|
'tenants_transferred': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Handle owned tenants - either delete or transfer ownership
|
||||||
|
for membership in tenant_info['memberships']:
|
||||||
|
if membership.get('role') == 'owner':
|
||||||
|
tenant_id = membership['tenant_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if tenant has other admin members who can take ownership
|
||||||
|
has_other_admins = await self.clients.tenant_client.check_tenant_has_other_admins(
|
||||||
|
tenant_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_other_admins:
|
||||||
|
# Get tenant members to find first admin
|
||||||
|
members = await self.clients.tenant_client.get_tenant_members(tenant_id)
|
||||||
|
admin_members = [
|
||||||
|
m for m in members
|
||||||
|
if m.get('role') == 'admin' and m.get('user_id') != user_id
|
||||||
|
]
|
||||||
|
|
||||||
|
if admin_members:
|
||||||
|
# Transfer ownership to first admin
|
||||||
|
transfer_result = await self.clients.tenant_client.transfer_tenant_ownership(
|
||||||
|
tenant_id, user_id, admin_members[0]['user_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
if transfer_result:
|
||||||
|
result['tenants_transferred'] += 1
|
||||||
|
logger.info("Transferred tenant ownership",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
new_owner=admin_members[0]['user_id'])
|
||||||
|
else:
|
||||||
|
result['errors'].append(f"Failed to transfer ownership of tenant {tenant_id}")
|
||||||
|
else:
|
||||||
|
result['errors'].append(f"No admin members found for tenant {tenant_id}")
|
||||||
|
else:
|
||||||
|
# No other admins, delete the tenant completely
|
||||||
|
delete_result = await self.clients.tenant_client.delete_tenant(tenant_id)
|
||||||
|
|
||||||
|
if delete_result:
|
||||||
|
result['tenants_deleted'] += 1
|
||||||
|
logger.info("Deleted tenant", tenant_id=tenant_id)
|
||||||
|
else:
|
||||||
|
result['errors'].append(f"Failed to delete tenant {tenant_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error handling owned tenant {tenant_id}: {str(e)}"
|
||||||
|
result['errors'].append(error_msg)
|
||||||
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
# Delete user's memberships
|
||||||
|
delete_result = await self.clients.tenant_client.delete_user_memberships(user_id)
|
||||||
|
if delete_result:
|
||||||
|
result['memberships_deleted'] = delete_result.get('memberships_deleted', 0)
|
||||||
|
if delete_result.get('errors'):
|
||||||
|
result['errors'].extend(delete_result['errors'])
|
||||||
|
else:
|
||||||
|
result['errors'].append("Failed to delete user memberships")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result['errors'].append(f"Tenant service communication error: {str(e)}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _delete_auth_data(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""Delete user account, refresh tokens, and auth data from local database"""
|
||||||
|
result = {
|
||||||
|
'user_deleted': False,
|
||||||
|
'refresh_tokens_deleted': 0,
|
||||||
|
'sessions_invalidated': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.models.users import User, RefreshToken
|
||||||
|
|
||||||
|
# Delete refresh tokens
|
||||||
|
token_delete_query = delete(RefreshToken).where(RefreshToken.user_id == uuid.UUID(user_id))
|
||||||
|
token_result = await self.db.execute(token_delete_query)
|
||||||
|
result['refresh_tokens_deleted'] = token_result.rowcount
|
||||||
|
|
||||||
|
# Delete user account
|
||||||
|
user_delete_query = delete(User).where(User.id == uuid.UUID(user_id))
|
||||||
|
user_result = await self.db.execute(user_delete_query)
|
||||||
|
|
||||||
|
if user_result.rowcount > 0:
|
||||||
|
result['user_deleted'] = True
|
||||||
|
await self.db.commit()
|
||||||
|
logger.info("User and tokens deleted from auth database",
|
||||||
|
user_id=user_id,
|
||||||
|
tokens_deleted=result['refresh_tokens_deleted'])
|
||||||
|
else:
|
||||||
|
result['errors'].append("User not found in auth database")
|
||||||
|
await self.db.rollback()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await self.db.rollback()
|
||||||
|
error_msg = f"Auth database error: {str(e)}"
|
||||||
|
result['errors'].append(error_msg)
|
||||||
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _generate_deletion_summary(self, deletion_results: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate summary of deletion operation"""
|
||||||
|
summary = {
|
||||||
|
'total_tenants_affected': deletion_results['tenant_associations']['total_tenants'],
|
||||||
|
'total_models_deleted': deletion_results['services_processed']['training']['models_deleted'],
|
||||||
|
'total_forecasts_deleted': deletion_results['services_processed']['forecasting']['forecasts_deleted'],
|
||||||
|
'total_notifications_deleted': deletion_results['services_processed']['notification']['notifications_deleted'],
|
||||||
|
'tenants_transferred': deletion_results['services_processed']['tenant']['tenants_transferred'],
|
||||||
|
'tenants_deleted': deletion_results['services_processed']['tenant']['tenants_deleted'],
|
||||||
|
'user_deleted': deletion_results['services_processed']['auth']['user_deleted'],
|
||||||
|
'total_errors': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Count total errors across all services
|
||||||
|
for service_result in deletion_results['services_processed'].values():
|
||||||
|
if isinstance(service_result, dict) and 'errors' in service_result:
|
||||||
|
summary['total_errors'] += len(service_result['errors'])
|
||||||
|
|
||||||
|
# Add success indicator
|
||||||
|
summary['deletion_successful'] = (
|
||||||
|
summary['user_deleted'] and
|
||||||
|
summary['total_errors'] == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
async def _publish_user_deleted_event(self, user_id: str, deletion_results: Dict[str, Any]):
|
||||||
|
"""Publish user deletion event to message queue"""
|
||||||
|
try:
|
||||||
|
await auth_publisher.publish_event(
|
||||||
|
exchange="user_events",
|
||||||
|
routing_key="user.admin.deleted",
|
||||||
|
message={
|
||||||
|
"event_type": "admin_user_deleted",
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"deletion_summary": deletion_results['summary'],
|
||||||
|
"services_affected": list(deletion_results['services_processed'].keys())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info("Published user deletion event", user_id=user_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to publish user deletion event", error=str(e))
|
||||||
|
|
||||||
|
async def _publish_user_deletion_failed_event(self, user_id: str, error: str):
|
||||||
|
"""Publish user deletion failure event"""
|
||||||
|
try:
|
||||||
|
await auth_publisher.publish_event(
|
||||||
|
exchange="user_events",
|
||||||
|
routing_key="user.deletion.failed",
|
||||||
|
message={
|
||||||
|
"event_type": "admin_user_deletion_failed",
|
||||||
|
"user_id": user_id,
|
||||||
|
"error": error,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info("Published user deletion failure event", user_id=user_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to publish deletion failure event", error=str(e))
|
||||||
|
|
||||||
|
async def _notify_admins_of_deletion(self, user_info: Dict[str, Any], deletion_results: Dict[str, Any]):
|
||||||
|
"""Send notification to other admins about the user deletion"""
|
||||||
|
try:
|
||||||
|
# Get requesting user info for notification
|
||||||
|
requesting_user_id = deletion_results['requested_by']
|
||||||
|
requesting_user = await self._validate_admin_user(requesting_user_id)
|
||||||
|
|
||||||
|
if requesting_user:
|
||||||
|
await self.clients.notification_client.send_user_deletion_notification(
|
||||||
|
admin_email=requesting_user['email'],
|
||||||
|
deleted_user_email=user_info['email'],
|
||||||
|
deletion_summary=deletion_results['summary']
|
||||||
|
)
|
||||||
|
logger.info("Sent user deletion notification",
|
||||||
|
deleted_user=user_info['email'],
|
||||||
|
notified_admin=requesting_user['email'])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to send admin notification", error=str(e))
|
||||||
|
|
||||||
|
async def preview_user_deletion(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Preview what data would be deleted for an admin user without actually deleting
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get user info
|
||||||
|
user_info = await self._validate_admin_user(user_id)
|
||||||
|
if not user_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Admin user {user_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get tenant associations
|
||||||
|
tenant_info = await self._get_user_tenant_info(user_id)
|
||||||
|
|
||||||
|
# Get counts from each service
|
||||||
|
training_models_count = 0
|
||||||
|
forecasts_count = 0
|
||||||
|
notifications_count = 0
|
||||||
|
|
||||||
|
for tenant_id in tenant_info['tenant_ids']:
|
||||||
|
try:
|
||||||
|
# Get training models count
|
||||||
|
models_count = await self.clients.training_client.get_tenant_models_count(tenant_id)
|
||||||
|
training_models_count += models_count
|
||||||
|
|
||||||
|
# Get forecasts count
|
||||||
|
tenant_forecasts = await self.clients.forecasting_client.get_tenant_forecasts_count(tenant_id)
|
||||||
|
forecasts_count += tenant_forecasts
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Could not get counts for tenant", tenant_id=tenant_id, error=str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get user notifications count
|
||||||
|
notifications_count = await self.clients.notification_client.get_user_notification_count(user_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Could not get notification count", user_id=user_id, error=str(e))
|
||||||
|
|
||||||
|
# Build preview
|
||||||
|
preview = {
|
||||||
|
"user": user_info,
|
||||||
|
"tenant_associations": tenant_info,
|
||||||
|
"estimated_deletions": {
|
||||||
|
"training_models": training_models_count,
|
||||||
|
"forecasts": forecasts_count,
|
||||||
|
"notifications": notifications_count,
|
||||||
|
"tenant_memberships": tenant_info['total_tenants'],
|
||||||
|
"owned_tenants": tenant_info['owned_tenants']
|
||||||
|
},
|
||||||
|
"tenant_handling": await self._preview_tenant_handling(user_id, tenant_info),
|
||||||
|
"warning": "This operation is irreversible and will permanently delete all associated data"
|
||||||
|
}
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error generating deletion preview", user_id=user_id, error=str(e))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to generate deletion preview"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _preview_tenant_handling(self, user_id: str, tenant_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Preview how each owned tenant would be handled"""
|
||||||
|
tenant_handling = []
|
||||||
|
|
||||||
|
for membership in tenant_info['memberships']:
|
||||||
|
if membership.get('role') == 'owner':
|
||||||
|
tenant_id = membership['tenant_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
has_other_admins = await self.clients.tenant_client.check_tenant_has_other_admins(
|
||||||
|
tenant_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_other_admins:
|
||||||
|
members = await self.clients.tenant_client.get_tenant_members(tenant_id)
|
||||||
|
admin_members = [
|
||||||
|
m for m in members
|
||||||
|
if m.get('role') == 'admin' and m.get('user_id') != user_id
|
||||||
|
]
|
||||||
|
|
||||||
|
tenant_handling.append({
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"action": "transfer_ownership",
|
||||||
|
"details": f"Ownership will be transferred to admin: {admin_members[0]['user_id'] if admin_members else 'Unknown'}"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
tenant_handling.append({
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"action": "delete_tenant",
|
||||||
|
"details": "Tenant will be deleted completely (no other admins found)"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
tenant_handling.append({
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"action": "error",
|
||||||
|
"details": f"Could not determine action: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
return tenant_handling
|
||||||
403
services/auth/app/services/auth_service_clients.py
Normal file
403
services/auth/app/services/auth_service_clients.py
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
# ================================================================
|
||||||
|
# Auth Service Inter-Service Communication Clients
|
||||||
|
# ================================================================
|
||||||
|
"""
|
||||||
|
Inter-service communication clients for the Auth Service to communicate
|
||||||
|
with other microservices in the bakery forecasting platform.
|
||||||
|
|
||||||
|
These clients handle authenticated API calls to:
|
||||||
|
- Tenant Service
|
||||||
|
- Training Service
|
||||||
|
- Forecasting Service
|
||||||
|
- Notification Service
|
||||||
|
"""
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from typing import Dict, Any, Optional, List, Union
|
||||||
|
from shared.clients.base_service_client import BaseServiceClient
|
||||||
|
from shared.config.base import BaseServiceSettings
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# TENANT SERVICE CLIENT
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
class AuthTenantServiceClient(BaseServiceClient):
|
||||||
|
"""Client for Auth Service to communicate with Tenant Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
super().__init__("auth", config)
|
||||||
|
self.service_url = config.TENANT_SERVICE_URL
|
||||||
|
|
||||||
|
def get_service_base_path(self) -> str:
|
||||||
|
return "/api/v1"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# USER TENANT OPERATIONS
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
async def get_user_tenants(self, user_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
"""Get all tenant memberships for a user"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"users/{user_id}/tenants")
|
||||||
|
return result.get("memberships", []) if result else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get user tenants", user_id=user_id, error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_user_owned_tenants(self, user_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
"""Get tenants owned by a user"""
|
||||||
|
try:
|
||||||
|
memberships = await self.get_user_tenants(user_id)
|
||||||
|
if memberships:
|
||||||
|
return [m for m in memberships if m.get('role') == 'owner']
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get owned tenants", user_id=user_id, error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def transfer_tenant_ownership(
|
||||||
|
self,
|
||||||
|
tenant_id: str,
|
||||||
|
current_owner_id: str,
|
||||||
|
new_owner_id: str
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Transfer tenant ownership from one user to another"""
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"current_owner_id": current_owner_id,
|
||||||
|
"new_owner_id": new_owner_id
|
||||||
|
}
|
||||||
|
return await self.post(f"tenants/{tenant_id}/transfer-ownership", data=data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to transfer tenant ownership",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def delete_tenant(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Delete a tenant completely"""
|
||||||
|
try:
|
||||||
|
return await self.delete(f"tenants/{tenant_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete tenant", tenant_id=tenant_id, error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def delete_user_memberships(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Delete all tenant memberships for a user"""
|
||||||
|
try:
|
||||||
|
return await self.delete(f"users/{user_id}/memberships")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete user memberships", user_id=user_id, error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_tenant_members(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
"""Get all members of a tenant"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"tenants/{tenant_id}/members")
|
||||||
|
return result.get("members", []) if result else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant members", tenant_id=tenant_id, error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def check_tenant_has_other_admins(self, tenant_id: str, excluding_user_id: str) -> bool:
|
||||||
|
"""Check if tenant has other admin users besides the excluded one"""
|
||||||
|
try:
|
||||||
|
members = await self.get_tenant_members(tenant_id)
|
||||||
|
if members:
|
||||||
|
admin_members = [
|
||||||
|
m for m in members
|
||||||
|
if m.get('role') in ['admin', 'owner'] and m.get('user_id') != excluding_user_id
|
||||||
|
]
|
||||||
|
return len(admin_members) > 0
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to check tenant admins",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# TRAINING SERVICE CLIENT
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
class AuthTrainingServiceClient(BaseServiceClient):
|
||||||
|
"""Client for Auth Service to communicate with Training Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
super().__init__("auth", config)
|
||||||
|
self.service_url = config.TRAINING_SERVICE_URL
|
||||||
|
|
||||||
|
def get_service_base_path(self) -> str:
|
||||||
|
return "/api/v1"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# TRAINING JOB OPERATIONS
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
async def cancel_tenant_training_jobs(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Cancel all active training jobs for a tenant"""
|
||||||
|
try:
|
||||||
|
data = {"tenant_id": tenant_id}
|
||||||
|
return await self.post("jobs/cancel-tenant", data=data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to cancel tenant training jobs",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return {"jobs_cancelled": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
|
async def get_tenant_active_jobs(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
"""Get all active training jobs for a tenant"""
|
||||||
|
try:
|
||||||
|
params = {"status": "running,queued,pending", "tenant_id": tenant_id}
|
||||||
|
result = await self.get("jobs", params=params)
|
||||||
|
return result.get("jobs", []) if result else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant active jobs",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# MODEL OPERATIONS
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
async def delete_tenant_models(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Delete all trained models and artifacts for a tenant"""
|
||||||
|
try:
|
||||||
|
return await self.delete(f"models/tenant/{tenant_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete tenant models",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return {"models_deleted": 0, "artifacts_deleted": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
|
async def get_tenant_models_count(self, tenant_id: str) -> int:
|
||||||
|
"""Get count of trained models for a tenant"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"models/tenant/{tenant_id}/count")
|
||||||
|
return result.get("count", 0) if result else 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant models count",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def get_tenant_model_artifacts(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
"""Get all model artifacts for a tenant"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"models/tenant/{tenant_id}/artifacts")
|
||||||
|
return result.get("artifacts", []) if result else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant model artifacts",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# FORECASTING SERVICE CLIENT
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
class AuthForecastingServiceClient(BaseServiceClient):
|
||||||
|
"""Client for Auth Service to communicate with Forecasting Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
super().__init__("auth", config)
|
||||||
|
self.service_url = config.FORECASTING_SERVICE_URL
|
||||||
|
|
||||||
|
def get_service_base_path(self) -> str:
|
||||||
|
return "/api/v1"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# FORECAST OPERATIONS
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
async def delete_tenant_forecasts(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Delete all forecasts and predictions for a tenant"""
|
||||||
|
try:
|
||||||
|
return await self.delete(f"forecasts/tenant/{tenant_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete tenant forecasts",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return {
|
||||||
|
"forecasts_deleted": 0,
|
||||||
|
"predictions_deleted": 0,
|
||||||
|
"cache_cleared": 0,
|
||||||
|
"errors": [str(e)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_tenant_forecasts_count(self, tenant_id: str) -> int:
|
||||||
|
"""Get count of forecasts for a tenant"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"forecasts/tenant/{tenant_id}/count")
|
||||||
|
return result.get("count", 0) if result else 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get tenant forecasts count",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def clear_tenant_prediction_cache(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Clear prediction cache for a tenant"""
|
||||||
|
try:
|
||||||
|
return await self.post(f"predictions/cache/clear/{tenant_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to clear tenant prediction cache",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return {"cache_cleared": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
|
async def cancel_tenant_prediction_batches(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Cancel any active prediction batches for a tenant"""
|
||||||
|
try:
|
||||||
|
data = {"tenant_id": tenant_id}
|
||||||
|
return await self.post("predictions/batches/cancel", data=data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to cancel tenant prediction batches",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
error=str(e))
|
||||||
|
return {"batches_cancelled": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# NOTIFICATION SERVICE CLIENT
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
class AuthNotificationServiceClient(BaseServiceClient):
|
||||||
|
"""Client for Auth Service to communicate with Notification Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
super().__init__("auth", config)
|
||||||
|
self.service_url = config.NOTIFICATION_SERVICE_URL
|
||||||
|
|
||||||
|
def get_service_base_path(self) -> str:
|
||||||
|
return "/api/v1"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# USER NOTIFICATION OPERATIONS
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
async def delete_user_notification_data(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Delete all notification data for a user"""
|
||||||
|
try:
|
||||||
|
return await self.delete(f"users/{user_id}/data")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete user notification data",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
return {
|
||||||
|
"preferences_deleted": 0,
|
||||||
|
"notifications_deleted": 0,
|
||||||
|
"logs_deleted": 0,
|
||||||
|
"errors": [str(e)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def cancel_pending_user_notifications(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Cancel all pending notifications for a user"""
|
||||||
|
try:
|
||||||
|
return await self.post(f"users/{user_id}/notifications/cancel")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to cancel user notifications",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
return {"notifications_cancelled": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
|
async def get_user_notification_count(self, user_id: str) -> int:
|
||||||
|
"""Get count of notifications for a user"""
|
||||||
|
try:
|
||||||
|
result = await self.get(f"users/{user_id}/notifications/count")
|
||||||
|
return result.get("count", 0) if result else 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get user notification count",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def send_user_deletion_notification(
|
||||||
|
self,
|
||||||
|
admin_email: str,
|
||||||
|
deleted_user_email: str,
|
||||||
|
deletion_summary: Dict[str, Any]
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Send notification about user deletion to administrators"""
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"type": "email",
|
||||||
|
"recipient_email": admin_email,
|
||||||
|
"template_key": "user_deletion_notification",
|
||||||
|
"template_data": {
|
||||||
|
"deleted_user_email": deleted_user_email,
|
||||||
|
"deletion_summary": deletion_summary
|
||||||
|
},
|
||||||
|
"priority": "high"
|
||||||
|
}
|
||||||
|
return await self.post("notifications/send", data=data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to send user deletion notification",
|
||||||
|
admin_email=admin_email,
|
||||||
|
error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# CLIENT FACTORY
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
class AuthServiceClientFactory:
|
||||||
|
"""Factory for creating inter-service clients for Auth Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
self.config = config
|
||||||
|
self._tenant_client = None
|
||||||
|
self._training_client = None
|
||||||
|
self._forecasting_client = None
|
||||||
|
self._notification_client = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tenant_client(self) -> AuthTenantServiceClient:
|
||||||
|
"""Get or create tenant service client"""
|
||||||
|
if self._tenant_client is None:
|
||||||
|
self._tenant_client = AuthTenantServiceClient(self.config)
|
||||||
|
return self._tenant_client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def training_client(self) -> AuthTrainingServiceClient:
|
||||||
|
"""Get or create training service client"""
|
||||||
|
if self._training_client is None:
|
||||||
|
self._training_client = AuthTrainingServiceClient(self.config)
|
||||||
|
return self._training_client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forecasting_client(self) -> AuthForecastingServiceClient:
|
||||||
|
"""Get or create forecasting service client"""
|
||||||
|
if self._forecasting_client is None:
|
||||||
|
self._forecasting_client = AuthForecastingServiceClient(self.config)
|
||||||
|
return self._forecasting_client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notification_client(self) -> AuthNotificationServiceClient:
|
||||||
|
"""Get or create notification service client"""
|
||||||
|
if self._notification_client is None:
|
||||||
|
self._notification_client = AuthNotificationServiceClient(self.config)
|
||||||
|
return self._notification_client
|
||||||
|
|
||||||
|
async def health_check_all_services(self) -> Dict[str, bool]:
|
||||||
|
"""Check health of all services"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
clients = [
|
||||||
|
("tenant", self.tenant_client),
|
||||||
|
("training", self.training_client),
|
||||||
|
("forecasting", self.forecasting_client),
|
||||||
|
("notification", self.notification_client)
|
||||||
|
]
|
||||||
|
|
||||||
|
for service_name, client in clients:
|
||||||
|
try:
|
||||||
|
health = await client.get("health")
|
||||||
|
results[service_name] = health is not None and health.get("status") == "healthy"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health check failed for {service_name} service", error=str(e))
|
||||||
|
results[service_name] = False
|
||||||
|
|
||||||
|
return results
|
||||||
Reference in New Issue
Block a user