""" 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, get_background_db_session from app.schemas.auth import UserResponse, PasswordChange from app.schemas.users import UserUpdate, BatchUserRequest, OwnerUserCreate 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 ) from shared.routing import RouteBuilder from shared.security import create_audit_logger, AuditSeverity, AuditAction logger = structlog.get_logger() router = APIRouter(tags=["users"]) route_builder = RouteBuilder('auth') # Initialize audit logger audit_logger = create_audit_logger("auth-service") @router.get(route_builder.build_base_route("me", include_tenant_prefix=False), 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 - FIXED VERSION""" try: logger.debug(f"Getting user info for: {current_user}") # 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" ) # ✅ FIX: Fetch full user from database to get the real role from app.repositories import UserRepository user_repo = UserRepository(User, db) user = await user_repo.get_by_id(user_id) logger.debug(f"Fetched user from DB - Role: {user.role}, Email: {user.email}") # ✅ FIX: Return role from database, not from JWT headers 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 or "es", timezone=user.timezone or "Europe/Madrid", created_at=user.created_at, last_login=user.last_login, role=user.role, # ✅ CRITICAL: Use role from database, not headers tenant_id=current_user.get("tenant_id") ) else: # Direct User object (shouldn't happen in microservice architecture) logger.debug(f"Direct user object received - Role: {current_user.role}") 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 or "es", timezone=current_user.timezone or "Europe/Madrid", created_at=current_user.created_at, last_login=current_user.last_login, role=current_user.role, # ✅ Use role from database tenant_id=None ) except HTTPException: raise except Exception as e: logger.error(f"Get user info error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get user information" ) @router.put(route_builder.build_base_route("me", include_tenant_prefix=False), 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: user_id = current_user.get("user_id") if isinstance(current_user, dict) else current_user.id 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) 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, role=updated_user.role, # ✅ Include role tenant_id=current_user.get("tenant_id") if isinstance(current_user, dict) else None ) 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(route_builder.build_base_route("delete/{user_id}", include_tenant_prefix=False)) 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" ) # 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" ) # Log audit event for user deletion try: # Get tenant_id from current_user or use a placeholder for system-level operations tenant_id_str = current_user.get("tenant_id", "00000000-0000-0000-0000-000000000000") await audit_logger.log_deletion( db_session=db, tenant_id=tenant_id_str, user_id=current_user["user_id"], resource_type="user", resource_id=user_id, resource_data=user_info, description=f"Admin {current_user.get('email', current_user['user_id'])} initiated deletion of user {user_info.get('email', user_id)}", endpoint="/delete/{user_id}", method="DELETE" ) except Exception as audit_error: logger.warning("Failed to log audit event", error=str(audit_error)) # 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["user_id"] ) 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): """ Background task using shared infrastructure """ # ✅ 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 ) logger.info("Background admin user deletion completed successfully", user_id=user_id, requesting_user=requesting_user_id, result=result) @router.get(route_builder.build_base_route("delete/{user_id}/deletion-preview", include_tenant_prefix=False)) 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 @router.get(route_builder.build_base_route("users/{user_id}", include_tenant_prefix=False), response_model=UserResponse) async def get_user_by_id( user_id: str = Path(..., description="User ID"), db: AsyncSession = Depends(get_db) ): """ Get user information by user ID. This endpoint is for internal service-to-service communication. It returns user details needed by other services (e.g., tenant service for enriching member data). """ try: # Validate UUID format try: uuid.UUID(user_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid user ID format" ) # Fetch user from database from app.repositories import UserRepository user_repo = UserRepository(User, db) user = await user_repo.get_by_id(user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found" ) logger.debug("Retrieved user by ID", user_id=user_id, email=user.email) 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 or "es", timezone=user.timezone or "Europe/Madrid", created_at=user.created_at, last_login=user.last_login, role=user.role, tenant_id=None ) except HTTPException: raise except Exception as e: logger.error("Get user by ID error", user_id=user_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get user information" ) @router.post(route_builder.build_base_route("users/create-by-owner", include_tenant_prefix=False), response_model=UserResponse) async def create_user_by_owner( user_data: OwnerUserCreate, current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Create a new user account (owner/admin only - for pilot phase). This endpoint allows tenant owners to directly create user accounts with passwords during the pilot phase. In production, this will be replaced with an invitation-based flow. **Permissions:** Owner or Admin role required **Security:** Password is hashed server-side before storage """ try: # Verify caller has admin or owner privileges # In pilot phase, we allow 'admin' role from auth service user_role = current_user.get("role", "user") if user_role not in ["admin", "super_admin", "manager"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only administrators can create users directly" ) # Validate email uniqueness from app.repositories import UserRepository user_repo = UserRepository(User, db) existing_user = await user_repo.get_by_email(user_data.email) if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"User with email {user_data.email} already exists" ) # Hash password from app.core.security import SecurityManager hashed_password = SecurityManager.hash_password(user_data.password) # Create user create_data = { "email": user_data.email, "full_name": user_data.full_name, "hashed_password": hashed_password, "phone": user_data.phone, "role": user_data.role, "language": user_data.language or "es", "timezone": user_data.timezone or "Europe/Madrid", "is_active": True, "is_verified": False # Can be verified later if needed } new_user = await user_repo.create_user(create_data) logger.info( "User created by owner", created_user_id=str(new_user.id), created_user_email=new_user.email, created_by=current_user.get("user_id"), created_by_email=current_user.get("email") ) # Return user response return UserResponse( id=str(new_user.id), email=new_user.email, full_name=new_user.full_name, is_active=new_user.is_active, is_verified=new_user.is_verified, phone=new_user.phone, language=new_user.language, timezone=new_user.timezone, created_at=new_user.created_at, last_login=new_user.last_login, role=new_user.role, tenant_id=None # Will be set when added to tenant ) except HTTPException: raise except Exception as e: logger.error( "Failed to create user by owner", email=user_data.email, error=str(e), created_by=current_user.get("user_id") ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create user account" ) @router.post(route_builder.build_base_route("users/batch", include_tenant_prefix=False), response_model=Dict[str, Any]) async def get_users_batch( request: BatchUserRequest, db: AsyncSession = Depends(get_db) ): """ Get multiple users by their IDs in a single request. This endpoint is for internal service-to-service communication. It efficiently fetches multiple user records needed by other services (e.g., tenant service for enriching member lists). Returns a dict mapping user_id -> user data, with null for non-existent users. """ try: # Validate all UUIDs validated_ids = [] for user_id in request.user_ids: try: uuid.UUID(user_id) validated_ids.append(user_id) except ValueError: logger.warning(f"Invalid user ID format in batch request: {user_id}") continue if not validated_ids: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No valid user IDs provided" ) # Fetch users from database from app.repositories import UserRepository user_repo = UserRepository(User, db) # Build response map user_map = {} for user_id in validated_ids: user = await user_repo.get_by_id(user_id) if user: user_map[user_id] = { "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 or "es", "timezone": user.timezone or "Europe/Madrid", "created_at": user.created_at.isoformat() if user.created_at else None, "last_login": user.last_login.isoformat() if user.last_login else None, "role": user.role } else: user_map[user_id] = None logger.debug( "Batch user fetch completed", requested_count=len(request.user_ids), found_count=sum(1 for v in user_map.values() if v is not None) ) return { "users": user_map, "requested_count": len(request.user_ids), "found_count": sum(1 for v in user_map.values() if v is not None) } except HTTPException: raise except Exception as e: logger.error("Batch user fetch error", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to fetch users" )