Files
bakery-ia/services/auth/app/api/users.py
2025-08-02 18:18:05 +02:00

286 lines
9.7 KiB
Python

"""
User management API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Path
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Dict, Any
import structlog
import uuid
from datetime import datetime, timezone
from app.core.database import get_db
from app.schemas.auth import UserResponse, PasswordChange
from app.schemas.users import UserUpdate
from app.services.user_service import UserService
from app.models.users import User
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.admin_delete import AdminUserDeleteService
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
require_admin_role_dep
)
logger = structlog.get_logger()
router = APIRouter(tags=["users"])
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get current user information"""
try:
# Handle both User object (direct auth) and dict (from gateway headers)
if isinstance(current_user, dict):
# Coming from gateway headers - need to fetch user from DB
user_id = current_user.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid user context"
)
# Fetch full user from database
from sqlalchemy import select
from app.models.users import User
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
phone=user.phone,
language=user.language,
timezone=user.timezone,
created_at=user.created_at,
last_login=user.last_login
)
else:
# Direct User object (when called directly)
return UserResponse(
id=str(current_user.id),
email=current_user.email,
full_name=current_user.full_name,
is_active=current_user.is_active,
is_verified=current_user.is_verified,
phone=current_user.phone,
language=current_user.language,
timezone=current_user.timezone,
created_at=current_user.created_at,
last_login=current_user.last_login
)
except Exception as e:
logger.error(f"Get current user error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user information"
)
@router.put("/me", response_model=UserResponse)
async def update_current_user(
user_update: UserUpdate,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update current user information"""
try:
updated_user = await UserService.update_user(current_user.id, user_update, db)
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
phone=updated_user.phone,
language=updated_user.language,
timezone=updated_user.timezone,
created_at=updated_user.created_at,
last_login=updated_user.last_login
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Update user error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
@router.delete("/delete/{user_id}")
async def delete_admin_user(
background_tasks: BackgroundTasks,
user_id: str = Path(..., description="User ID"),
current_user = Depends(require_admin_role_dep),
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"
)
# Quick validation that user exists before starting background task
deletion_service = AdminUserDeleteService(db)
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"
)
# Start deletion as background task for better performance
background_tasks.add_task(
execute_admin_user_deletion,
user_id=user_id,
requesting_user_id=current_user.id,
db_url=str(db.bind.url) # Pass DB connection string for background task
)
return {
"success": True,
"message": f"Admin user deletion for {user_id} has been initiated",
"status": "processing",
"user_info": user_info,
"initiated_at": datetime.utcnow().isoformat(),
"note": "Deletion is processing in the background. Check logs for completion status."
}
# Add this background task function to services/auth/app/api/users.py:
async def execute_admin_user_deletion(
user_id: str,
requesting_user_id: str,
db_url: str
):
"""
Background task to execute complete admin user deletion
"""
# Create new database session for background task
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(db_url)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
try:
# Initialize deletion service with new session
deletion_service = AdminUserDeleteService(session)
# Perform the deletion
result = await deletion_service.delete_admin_user_complete(
user_id=user_id,
requesting_user_id=requesting_user_id
)
logger.info("Background admin user deletion completed successfully",
user_id=user_id,
requesting_user=requesting_user_id,
result=result)
except Exception as e:
logger.error("Background admin user deletion failed",
user_id=user_id,
requesting_user=requesting_user_id,
error=str(e))
# Attempt to publish failure event
try:
deletion_service = AdminUserDeleteService(session)
await deletion_service._publish_user_deletion_failed_event(user_id, str(e))
except:
pass
finally:
await engine.dispose()
@router.get("/delete/{user_id}/deletion-preview")
async def preview_user_deletion(
user_id: str = Path(..., description="User ID"),
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