from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from app.api.deps import get_current_user, get_user_repository, require_superuser from app.core.database.user_repository import ( EmailTakenError, UserInUseError, UserRepository, ) from app.core.dto.user import TrainerDTO, UserDTO, UserRole from app.core.security import hash_password from app.schemas.users import UserCreate, UserUpdate router = APIRouter() EMAIL_TAKEN = HTTPException(status_code=409, detail="Email already registered") @router.post("/", response_model=UserDTO, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_superuser)]) async def create_user( payload: UserCreate, repository: UserRepository = Depends(get_user_repository), ): try: return await repository.create( email=payload.email, password_hash=hash_password(payload.password), first_name=payload.first_name, last_name=payload.last_name, role=payload.role, ) except EmailTakenError: raise EMAIL_TAKEN @router.get("/", response_model=List[UserDTO], dependencies=[Depends(require_superuser)]) async def list_users( role: Optional[UserRole] = None, repository: UserRepository = Depends(get_user_repository), ): return await repository.list(role) @router.get("/trainers/", response_model=List[TrainerDTO]) async def list_trainers( _: UserDTO = Depends(get_current_user), repository: UserRepository = Depends(get_user_repository), ): """Trainers to pick from in a training form: any signed-in user may ask for them.""" return [ TrainerDTO( id=user.id, name=f"{user.first_name} {user.last_name}".strip() or user.email, ) for user in await repository.list(UserRole.trainer) if user.is_active ] @router.get("/{user_id}/", response_model=UserDTO, dependencies=[Depends(require_superuser)]) async def get_user( user_id: int, repository: UserRepository = Depends(get_user_repository), ): user = await repository.get(user_id) if user is None: raise HTTPException(status_code=404, detail="User not found") return user @router.patch("/{user_id}/", response_model=UserDTO, dependencies=[Depends(require_superuser)]) async def update_user( user_id: int, payload: UserUpdate, repository: UserRepository = Depends(get_user_repository), ): values = payload.model_dump(exclude_unset=True) if "password" in values: values["password_hash"] = hash_password(values.pop("password")) # null makes no sense for these columns if any(values.get(name, "") is None for name in ("email", "role", "is_active", "password_hash")): raise HTTPException(status_code=422, detail="Field must not be null") try: user = await repository.update(user_id, values) except EmailTakenError: raise EMAIL_TAKEN if user is None: raise HTTPException(status_code=404, detail="User not found") return user @router.delete("/{user_id}/", dependencies=[Depends(require_superuser)]) async def delete_user( user_id: int, current: UserDTO = Depends(require_superuser), repository: UserRepository = Depends(get_user_repository), ): if user_id == current.id: raise HTTPException(status_code=400, detail="You can't delete yourself") try: deleted = await repository.delete(user_id) except UserInUseError: raise HTTPException(status_code=409, detail="User has trainings") if not deleted: raise HTTPException(status_code=404, detail="User not found") return {"status": "ok"}