Files
bakery-ia/services/auth/app/api/users.py

272 lines
9.7 KiB
Python
Raw Normal View History

2025-07-17 19:03:11 +02:00
"""
User management API routes
"""
2025-08-02 17:53:28 +02:00
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Path
2025-07-17 19:03:11 +02:00
from sqlalchemy.ext.asyncio import AsyncSession
2025-07-21 14:41:33 +02:00
from typing import Dict, Any
2025-07-18 14:41:39 +02:00
import structlog
2025-08-01 22:20:32 +02:00
import uuid
2025-08-02 17:09:53 +02:00
from datetime import datetime, timezone
2025-07-17 19:03:11 +02:00
2025-08-03 14:42:33 +02:00
from app.core.database import get_db, get_background_db_session
2025-07-20 08:33:23 +02:00
from app.schemas.auth import UserResponse, PasswordChange
2025-07-19 21:16:25 +02:00
from app.schemas.users import UserUpdate
2025-07-17 19:03:11 +02:00
from app.services.user_service import UserService
2025-08-01 22:20:32 +02:00
from app.models.users import User
2025-07-17 19:03:11 +02:00
2025-07-26 19:15:18 +02:00
from sqlalchemy.ext.asyncio import AsyncSession
2025-08-01 22:20:32 +02:00
from app.services.admin_delete import AdminUserDeleteService
2025-07-21 14:41:33 +02:00
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
2025-08-02 18:18:05 +02:00
require_admin_role_dep
2025-07-21 14:41:33 +02:00
)
2025-07-18 14:41:39 +02:00
logger = structlog.get_logger()
2025-07-26 18:46:52 +02:00
router = APIRouter(tags=["users"])
2025-07-17 19:03:11 +02:00
2025-08-03 00:16:31 +02:00
2025-07-17 19:03:11 +02:00
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
2025-07-21 14:41:33 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-07-17 19:03:11 +02:00
db: AsyncSession = Depends(get_db)
):
2025-08-03 00:16:31 +02:00
"""Get current user information - FIXED VERSION"""
2025-07-17 19:03:11 +02:00
try:
2025-08-03 00:16:31 +02:00
logger.debug(f"Getting user info for: {current_user}")
2025-07-26 19:15:18 +02:00
# 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"
)
2025-08-03 00:16:31 +02:00
# ✅ FIX: Fetch full user from database to get the real role
2025-08-08 19:21:23 +02:00
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
user = await user_repo.get_by_id(user_id)
2025-07-26 19:15:18 +02:00
2025-08-03 00:16:31 +02:00
logger.debug(f"Fetched user from DB - Role: {user.role}, Email: {user.email}")
2025-07-26 19:15:18 +02:00
2025-08-03 00:16:31 +02:00
# ✅ FIX: Return role from database, not from JWT headers
2025-07-26 19:15:18 +02:00
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,
2025-08-03 00:16:31 +02:00
language=user.language or "es",
timezone=user.timezone or "Europe/Madrid",
2025-07-26 19:15:18 +02:00
created_at=user.created_at,
2025-08-03 00:16:31 +02:00
last_login=user.last_login,
role=user.role, # ✅ CRITICAL: Use role from database, not headers
tenant_id=current_user.get("tenant_id")
2025-07-26 19:15:18 +02:00
)
else:
2025-08-03 00:16:31 +02:00
# Direct User object (shouldn't happen in microservice architecture)
logger.debug(f"Direct user object received - Role: {current_user.role}")
2025-07-26 19:15:18 +02:00
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,
2025-08-03 00:16:31 +02:00
language=current_user.language or "es",
timezone=current_user.timezone or "Europe/Madrid",
2025-07-26 19:15:18 +02:00
created_at=current_user.created_at,
2025-08-03 00:16:31 +02:00
last_login=current_user.last_login,
role=current_user.role, # ✅ Use role from database
tenant_id=None
2025-07-26 19:15:18 +02:00
)
2025-08-03 00:16:31 +02:00
except HTTPException:
raise
2025-07-17 19:03:11 +02:00
except Exception as e:
2025-08-03 00:16:31 +02:00
logger.error(f"Get user info error: {e}")
2025-07-17 19:03:11 +02:00
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(
2025-07-19 21:16:25 +02:00
user_update: UserUpdate,
2025-07-21 20:43:17 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-07-17 19:03:11 +02:00
db: AsyncSession = Depends(get_db)
):
"""Update current user information"""
try:
2025-08-03 00:16:31 +02:00
user_id = current_user.get("user_id") if isinstance(current_user, dict) else current_user.id
2025-08-08 19:21:23 +02:00
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
# Prepare update data
update_data = {}
if user_update.full_name is not None:
update_data["full_name"] = user_update.full_name
if user_update.phone is not None:
update_data["phone"] = user_update.phone
if user_update.language is not None:
update_data["language"] = user_update.language
if user_update.timezone is not None:
update_data["timezone"] = user_update.timezone
updated_user = await user_repo.update(user_id, update_data)
2025-07-17 19:03:11 +02:00
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,
2025-08-03 00:16:31 +02:00
last_login=updated_user.last_login,
role=updated_user.role, # ✅ Include role
tenant_id=current_user.get("tenant_id") if isinstance(current_user, dict) else None
2025-07-17 19:03:11 +02:00
)
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"
2025-08-01 22:20:32 +02:00
)
2025-08-02 17:09:53 +02:00
@router.delete("/delete/{user_id}")
2025-08-01 22:20:32 +02:00
async def delete_admin_user(
background_tasks: BackgroundTasks,
2025-08-02 17:53:28 +02:00
user_id: str = Path(..., description="User ID"),
2025-08-02 18:18:05 +02:00
current_user = Depends(require_admin_role_dep),
2025-08-01 22:20:32 +02:00
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"
)
2025-08-02 17:09:53 +02:00
# Quick validation that user exists before starting background task
2025-08-01 22:20:32 +02:00
deletion_service = AdminUserDeleteService(db)
2025-08-02 17:09:53 +02:00
user_info = await deletion_service._validate_admin_user(user_id)
if not user_info:
2025-08-01 22:20:32 +02:00
raise HTTPException(
2025-08-02 17:09:53 +02:00
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Admin user {user_id} not found"
2025-08-01 22:20:32 +02:00
)
2025-08-02 17:09:53 +02:00
# Start deletion as background task for better performance
background_tasks.add_task(
execute_admin_user_deletion,
user_id=user_id,
2025-08-03 14:42:33 +02:00
requesting_user_id=current_user["user_id"]
2025-08-02 17:09:53 +02:00
)
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:
2025-08-03 14:42:33 +02:00
async def execute_admin_user_deletion(user_id: str, requesting_user_id: str):
2025-08-02 17:09:53 +02:00
"""
2025-08-03 14:42:33 +02:00
Background task using shared infrastructure
2025-08-02 17:09:53 +02:00
"""
2025-08-03 14:42:33 +02:00
# ✅ Use the shared background session
async with get_background_db_session() as session:
deletion_service = AdminUserDeleteService(session)
result = await deletion_service.delete_admin_user_complete(
user_id=user_id,
requesting_user_id=requesting_user_id
)
2025-08-02 17:09:53 +02:00
2025-08-03 14:42:33 +02:00
logger.info("Background admin user deletion completed successfully",
user_id=user_id,
requesting_user=requesting_user_id,
result=result)
2025-08-01 22:20:32 +02:00
2025-08-02 17:09:53 +02:00
@router.get("/delete/{user_id}/deletion-preview")
2025-08-01 22:20:32 +02:00
async def preview_user_deletion(
2025-08-02 17:53:28 +02:00
user_id: str = Path(..., description="User ID"),
2025-08-01 22:20:32 +02:00
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