"""User management API routes (admin only).""" from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession import bcrypt from ..auth.dependencies import require_admin from ..database.engine import get_session from ..database.models import User router = APIRouter(prefix="/api/users", tags=["users"]) class UserCreate(BaseModel): username: str password: str role: str = "user" class UserUpdate(BaseModel): username: str | None = None password: str | None = None role: str | None = None @router.get("") async def list_users( admin: User = Depends(require_admin), session: AsyncSession = Depends(get_session), ): """List all users (admin only).""" result = await session.exec(select(User)) return [ {"id": u.id, "username": u.username, "role": u.role, "created_at": u.created_at.isoformat()} for u in result.all() ] @router.post("", status_code=status.HTTP_201_CREATED) async def create_user( body: UserCreate, admin: User = Depends(require_admin), session: AsyncSession = Depends(get_session), ): """Create a new user (admin only).""" # Check for duplicate username result = await session.exec(select(User).where(User.username == body.username)) if result.first(): raise HTTPException(status_code=409, detail="Username already exists") user = User( username=body.username, hashed_password=bcrypt.hashpw(body.password.encode(), bcrypt.gensalt()).decode(), role=body.role if body.role in ("admin", "user") else "user", ) session.add(user) await session.commit() await session.refresh(user) return {"id": user.id, "username": user.username, "role": user.role} class ResetPasswordRequest(BaseModel): new_password: str @router.put("/{user_id}/password") async def reset_user_password( user_id: int, body: ResetPasswordRequest, admin: User = Depends(require_admin), session: AsyncSession = Depends(get_session), ): """Reset a user's password (admin only).""" user = await session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if len(body.new_password) < 6: raise HTTPException(status_code=400, detail="Password must be at least 6 characters") user.hashed_password = bcrypt.hashpw(body.new_password.encode(), bcrypt.gensalt()).decode() session.add(user) await session.commit() return {"success": True} @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: int, admin: User = Depends(require_admin), session: AsyncSession = Depends(get_session), ): """Delete a user (admin only, cannot delete self).""" if user_id == admin.id: raise HTTPException(status_code=400, detail="Cannot delete yourself") user = await session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") await session.delete(user) await session.commit()