from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel, ConfigDict, EmailStr 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.api import deps router = APIRouter() # =================================== # PYDANTIC SCHEMAS # =================================== class UserCreate(BaseModel): """Schema para crear usuario - NO incluye tenant_id (se asigna automáticamente)""" email: EmailStr first_name: str last_name: str role: UserRole password: str language: str = "es" timezone: str = "UTC" notifications_email: bool = True class UserUpdate(BaseModel): """Schema para actualizar usuario""" email: Optional[EmailStr] = None first_name: Optional[str] = None last_name: Optional[str] = None role: Optional[UserRole] = None is_active: Optional[bool] = None password: Optional[str] = None language: Optional[str] = None timezone: Optional[str] = None notifications_email: Optional[bool] = None class UserResponse(BaseModel): """Schema de respuesta - incluye todos los campos públicos""" id: uuid.UUID tenant_id: uuid.UUID email: EmailStr first_name: str last_name: str avatar_url: Optional[str] = None role: UserRole is_active: bool email_verified: bool last_login: Optional[datetime] = None language: str timezone: str notifications_email: bool totp_enabled: bool created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) # =================================== # ENDPOINTS # =================================== @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 """ # ✅ CORREGIDO: Filtrar por tenant_id query = select(User).where(User.tenant_id == current_user.tenant_id) # 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) 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. """ query = select(User).where( User.id == user_id, User.tenant_id == current_user.tenant_id # ✅ Seguridad multi-tenant ) 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 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 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) 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 - solo ADMIN puede eliminar if current_user.role != UserRole.ADMIN: 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 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" ) # Soft delete db_user.is_active = False await db.commit() 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 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