from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import List, Optional from datetime import datetime import uuid from app.core.database import get_db from app.core.security import security from app.models.user import User, UserRole from app.services.audit_service import AuditService from app.api import deps from app.api.schemas.user import UserCreate, UserUpdate, UserResponse router = APIRouter() # =================================== # ENDPOINTS # =================================== @router.get("/me", response_model=UserResponse) async def read_current_user( current_user: User = Depends(deps.get_current_user), ): """Obtener el perfil del usuario actual.""" return current_user @router.get("/", response_model=List[UserResponse]) async def read_users( skip: int = 0, limit: int = 100, role: Optional[UserRole] = None, is_active: Optional[bool] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Listar usuarios del tenant del usuario actual. ✅ Implementa multi-tenancy: solo muestra usuarios del tenant del usuario. Filtros opcionales: - role: filtrar por rol - is_active: filtrar por estado activo """ # ADMIN global ve todos los tenants; el resto solo ve su propio tenant from app.models.user import UserRole as _UserRole if current_user.role != _UserRole.ADMIN: query = select(User).where(User.tenant_id == current_user.tenant_id) else: query = select(User) # Aplicar filtros opcionales if role: query = query.where(User.role == role) if is_active is not None: query = query.where(User.is_active == is_active) query = query.offset(skip).limit(limit).order_by(User.created_at.desc()) result = await db.execute(query) return result.scalars().all() @router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED) async def create_user( user: UserCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Crear nuevo usuario en el tenant del usuario actual. ✅ Implementa multi-tenancy: asigna automáticamente tenant_id del usuario. Restricciones: - Solo ADMIN, SUPPORT_MANAGER y CLIENT_ADMIN pueden crear usuarios - El email debe ser único dentro del tenant """ # Verificar permisos if not current_user.can_manage_users: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to create users" ) # Verificar si el email ya existe en el tenant query = select(User).where( User.email == user.email, User.tenant_id == current_user.tenant_id ) result = await db.execute(query) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered in this tenant" ) # Preparar datos del usuario user_data = user.model_dump(exclude={"password"}) password_hash = security.hash_password(user.password) # ✅ CORREGIDO: Asignar tenant_id del usuario actual db_user = User( **user_data, password_hash=password_hash, tenant_id=current_user.tenant_id # ✅ Multi-tenancy automático ) db.add(db_user) await db.commit() await db.refresh(db_user) # Registrar creación en auditoría try: await AuditService.log( db=db, tenant_id=current_user.tenant_id, user_id=current_user.id, action="user.create", resource_type="user", resource_id=db_user.id, new_values=AuditService.sanitize_values({ "email": db_user.email, "first_name": db_user.first_name, "last_name": db_user.last_name, "role": db_user.role.value }) ) await db.commit() except Exception as e: # No fallar si falla el audit log pass return db_user @router.get("/{user_id}", response_model=UserResponse) async def read_user( user_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Obtener un usuario específico del tenant. ✅ Implementa multi-tenancy: solo permite acceso a usuarios del propio tenant. """ if current_user.role.value == 'ADMIN': query = select(User).where(User.id == user_id) else: query = select(User).where( User.id == user_id, User.tenant_id == current_user.tenant_id ) result = await db.execute(query) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) return user @router.put("/{user_id}", response_model=UserResponse) async def update_user( user_id: uuid.UUID, user_update: UserUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Actualizar usuario del tenant. ✅ Implementa multi-tenancy: solo permite actualizar usuarios del propio tenant. Restricciones: - Solo ADMIN, SUPPORT_MANAGER y CLIENT_ADMIN pueden actualizar usuarios - No se puede cambiar el tenant_id """ # Verificar permisos if not current_user.can_manage_users: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to update users" ) # Buscar usuario - ADMIN global puede editar cualquier tenant if current_user.role.value == "ADMIN": query = select(User).where(User.id == user_id) else: query = select(User).where( User.id == user_id, User.tenant_id == current_user.tenant_id ) result = await db.execute(query) db_user = result.scalar_one_or_none() if not db_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Guardar valores anteriores para audit old_values = { "email": db_user.email, "first_name": db_user.first_name, "last_name": db_user.last_name, "role": db_user.role.value, "is_active": db_user.is_active } # Verificar email único si se está cambiando update_data = user_update.model_dump(exclude_unset=True) if "email" in update_data and update_data["email"] != db_user.email: email_query = select(User).where( User.email == update_data["email"], User.tenant_id == current_user.tenant_id, User.id != user_id ) email_result = await db.execute(email_query) if email_result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already in use by another user" ) # Actualizar campos for field, value in update_data.items(): if field == "password": # Hash the new password db_user.password_hash = security.hash_password(value) else: setattr(db_user, field, value) await db.commit() await db.refresh(db_user) # Registrar actualización en auditoría try: new_values = { "email": db_user.email, "first_name": db_user.first_name, "last_name": db_user.last_name, "role": db_user.role.value, "is_active": db_user.is_active } await AuditService.log( db=db, tenant_id=current_user.tenant_id, user_id=current_user.id, action="user.update", resource_type="user", resource_id=db_user.id, old_values=AuditService.sanitize_values(old_values), new_values=AuditService.sanitize_values(new_values) ) await db.commit() except Exception as e: # No fallar si falla el audit log pass return db_user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Desactivar usuario del tenant (soft delete). ✅ Implementa multi-tenancy: solo permite desactivar usuarios del propio tenant. Restricciones: - Solo ADMIN puede eliminar usuarios - No se puede eliminar a sí mismo - No se puede eliminar el último ADMIN del tenant """ # Verificar permisos - ADMIN global o CLIENT_ADMIN pueden eliminar allowed = [UserRole.ADMIN, UserRole.CLIENT_ADMIN] if current_user.role not in allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only admins can delete users" ) # No se puede eliminar a sí mismo if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="You cannot delete yourself" ) # Buscar usuario - ADMIN global puede editar cualquier tenant if current_user.role.value == "ADMIN": query = select(User).where(User.id == user_id) else: query = select(User).where( User.id == user_id, User.tenant_id == current_user.tenant_id ) result = await db.execute(query) db_user = result.scalar_one_or_none() if not db_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Verificar que no sea el último admin del tenant if db_user.role == UserRole.ADMIN: admin_query = select(User).where( User.tenant_id == current_user.tenant_id, User.role == UserRole.ADMIN, User.is_active == True, User.id != user_id ) admin_result = await db.execute(admin_query) active_admins = admin_result.scalars().all() if len(active_admins) == 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete the last active admin of the tenant" ) # Hard delete await db.delete(db_user) await db.commit() # Registrar eliminación en auditoría try: await AuditService.log( db=db, tenant_id=current_user.tenant_id, user_id=current_user.id, action="user.delete", resource_type="user", resource_id=db_user.id, old_values={ "email": db_user.email, "role": db_user.role.value, "was_active": True }, metadata={"action_type": "soft_delete"} ) await db.commit() except Exception as e: # No fallar si falla el audit log pass return None @router.patch("/{user_id}/activate", response_model=UserResponse) async def activate_user( user_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user) ): """ Reactivar usuario desactivado. ✅ Implementa multi-tenancy: solo permite reactivar usuarios del propio tenant. """ # Verificar permisos if not current_user.can_manage_users: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to activate users" ) # Buscar usuario - ADMIN global puede editar cualquier tenant if current_user.role.value == "ADMIN": query = select(User).where(User.id == user_id) else: query = select(User).where( User.id == user_id, User.tenant_id == current_user.tenant_id ) result = await db.execute(query) db_user = result.scalar_one_or_none() if not db_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) db_user.is_active = True await db.commit() await db.refresh(db_user) return db_user