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

663 lines
23 KiB
Python
Raw Normal View History

2025-07-17 19:03:11 +02:00
"""
User management API routes
"""
2026-01-13 22:22:38 +01:00
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Path, Body
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-10-24 13:05:04 +02:00
from app.schemas.users import UserUpdate, BatchUserRequest, OwnerUserCreate
2026-01-15 20:45:49 +01:00
from app.services.user_service import EnhancedUserService
2025-10-06 15:27:01 +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-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-08-01 22:20:32 +02:00
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
)
from shared.security import create_audit_logger, AuditSeverity, AuditAction
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
# Initialize audit logger
2025-10-29 06:58:05 +01:00
audit_logger = create_audit_logger("auth-service", AuditLog)
2025-10-27 16:33:26 +01:00
@router.delete("/api/v1/auth/users/{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
)
# 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))
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
)
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-10-27 16:33:26 +01:00
@router.get("/api/v1/auth/users/{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.
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
This endpoint provides a dry-run preview of the deletion operation
without actually deleting any data.
"""
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
try:
uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
deletion_service = AdminUserDeleteService(db)
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
# 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"
)
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
# Get tenant associations
tenant_info = await deletion_service._get_user_tenant_info(user_id)
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
# Build preview
preview = {
"user": user_info,
"tenant_associations": tenant_info,
"estimated_deletions": {
"training_models": "All models for associated tenants",
2025-10-24 13:05:04 +02:00
"forecasts": "All forecasts for associated tenants",
2025-08-01 22:20:32 +02:00
"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"
}
2025-10-24 13:05:04 +02:00
2025-08-01 22:20:32 +02:00
return preview
2025-10-24 13:05:04 +02:00
2025-10-27 16:33:26 +01:00
@router.get("/api/v1/auth/users/{user_id}", response_model=UserResponse)
2025-10-24 13:05:04 +02:00
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,
2026-01-13 22:22:38 +01:00
tenant_id=None,
payment_customer_id=user.payment_customer_id,
default_payment_method_id=user.default_payment_method_id
2025-10-24 13:05:04 +02:00
)
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"
)
2026-01-16 15:19:34 +01:00
@router.put("/api/v1/auth/users/{user_id}", response_model=UserResponse)
async def update_user_profile(
user_id: str = Path(..., description="User ID"),
update_data: UserUpdate = Body(..., description="User profile update data"),
current_user = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Update user profile information.
This endpoint allows users to update their profile information including:
- Full name
- Phone number
- Language preference
- Timezone
**Permissions:** Users can update their own profile, admins can update any user's profile
"""
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"
)
# Check permissions - user can update their own profile, admins can update any
if current_user["user_id"] != user_id:
# Check if current user has admin privileges
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="Insufficient permissions to update this user's profile"
)
# 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"
)
# Prepare update data (only include fields that are provided)
update_fields = update_data.dict(exclude_unset=True)
if not update_fields:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No update data provided"
)
# Update user
updated_user = await user_repo.update(user_id, update_fields)
logger.info("User profile updated", user_id=user_id, updated_fields=list(update_fields.keys()))
# Log audit event for user profile update
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_event(
db_session=db,
tenant_id=tenant_id_str,
user_id=current_user["user_id"],
action=AuditAction.UPDATE.value,
resource_type="user",
resource_id=user_id,
severity=AuditSeverity.MEDIUM.value,
description=f"User {current_user.get('email', current_user['user_id'])} updated profile for user {user.email}",
changes={"updated_fields": list(update_fields.keys())},
audit_metadata={"updated_data": update_fields},
endpoint="/users/{user_id}",
method="PUT"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
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 or "es",
timezone=updated_user.timezone or "Europe/Madrid",
created_at=updated_user.created_at,
last_login=updated_user.last_login,
role=updated_user.role,
tenant_id=None,
payment_customer_id=updated_user.payment_customer_id,
default_payment_method_id=updated_user.default_payment_method_id
)
except HTTPException:
raise
except Exception as e:
logger.error("Update user profile error", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user profile"
)
2025-10-27 16:33:26 +01:00
@router.post("/api/v1/auth/users/create-by-owner", response_model=UserResponse)
2025-10-24 13:05:04 +02:00
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"
)
2025-10-27 16:33:26 +01:00
@router.post("/api/v1/auth/users/batch", response_model=Dict[str, Any])
2025-10-24 13:05:04 +02:00
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"
)
2025-10-27 16:33:26 +01:00
@router.get("/api/v1/auth/users/{user_id}/activity")
async def get_user_activity(
user_id: str = Path(..., description="User ID"),
current_user = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Get user activity information.
This endpoint returns detailed activity information for a user including:
- Last login timestamp
- Account creation date
- Active session count
- Last activity timestamp
- User status information
**Permissions:** User can view their own activity, admins can view any user's activity
"""
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"
)
# Check permissions - user can view their own activity, admins can view any
if current_user["user_id"] != user_id:
# Check if current user has admin privileges
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="Insufficient permissions to view this user's activity"
)
# Initialize enhanced user service
from app.core.config import settings
from shared.database.base import create_database_manager
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
user_service = EnhancedUserService(database_manager)
# Get user activity data
activity_data = await user_service.get_user_activity(user_id)
if "error" in activity_data:
if activity_data["error"] == "User not found":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get user activity: {activity_data['error']}"
)
logger.debug("Retrieved user activity", user_id=user_id)
return activity_data
except HTTPException:
raise
except Exception as e:
logger.error("Get user activity error", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user activity information"
)
2026-01-13 22:22:38 +01:00
@router.patch("/api/v1/auth/users/{user_id}/tenant")
async def update_user_tenant(
user_id: str = Path(..., description="User ID"),
tenant_data: Dict[str, Any] = Body(..., description="Tenant data containing tenant_id"),
db: AsyncSession = Depends(get_db)
):
"""
Update user's tenant_id after tenant registration
This endpoint is called by the tenant service after a user creates their tenant.
It links the user to their newly created tenant.
"""
try:
# Log the incoming request data for debugging
logger.debug("Received tenant update request",
user_id=user_id,
tenant_data=tenant_data)
tenant_id = tenant_data.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tenant_id is required"
)
logger.info("Updating user tenant_id",
user_id=user_id,
tenant_id=tenant_id)
2026-01-15 20:45:49 +01:00
user_service = EnhancedUserService(db)
2026-01-14 13:15:48 +01:00
user = await user_service.get_user_by_id(uuid.UUID(user_id), session=db)
2026-01-13 22:22:38 +01:00
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
2026-01-16 15:19:34 +01:00
# DEPRECATED: User-tenant relationships are now managed by tenant service
# This endpoint is kept for backward compatibility but does nothing
# The tenant service should manage user-tenant relationships internally
logger.warning("DEPRECATED: update_user_tenant endpoint called - user-tenant relationships are now managed by tenant service",
user_id=user_id,
tenant_id=tenant_id)
2026-01-13 22:22:38 +01:00
2026-01-16 15:19:34 +01:00
# Return success for backward compatibility, but don't actually update anything
2026-01-13 22:22:38 +01:00
return {
"success": True,
"user_id": str(user.id),
2026-01-16 15:19:34 +01:00
"tenant_id": tenant_id,
"message": "User-tenant relationships are now managed by tenant service. This endpoint is deprecated."
2026-01-13 22:22:38 +01:00
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update user tenant_id",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user tenant_id"
)