Files
bambuddy/backend/app/api/routes/users.py
T
2026-01-21 14:41:24 +01:00

206 lines
6.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import RequireAdmin, get_password_hash
from backend.app.core.database import get_db
from backend.app.models.user import User
from backend.app.schemas.auth import UserCreate, UserResponse, UserUpdate
router = APIRouter(prefix="/users", tags=["users"])
@router.get("", response_model=list[UserResponse])
@router.get("/", response_model=list[UserResponse])
async def list_users(
current_user: User = RequireAdmin(),
db: AsyncSession = Depends(get_db),
):
"""List all users (admin only)."""
result = await db.execute(select(User).order_by(User.created_at))
users = result.scalars().all()
return [
UserResponse(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
)
for user in users
]
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
current_user: User = RequireAdmin(),
db: AsyncSession = Depends(get_db),
):
"""Create a new user (admin only)."""
# Check if username already exists
existing_user = await db.execute(select(User).where(User.username == user_data.username))
if existing_user.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists",
)
# Validate role
if user_data.role not in ["admin", "user"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Role must be 'admin' or 'user'",
)
new_user = User(
username=user_data.username,
password_hash=get_password_hash(user_data.password),
role=user_data.role,
is_active=True,
)
db.add(new_user)
await db.commit()
await db.refresh(new_user)
return UserResponse(
id=new_user.id,
username=new_user.username,
role=new_user.role,
is_active=new_user.is_active,
created_at=new_user.created_at.isoformat(),
)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
current_user: User = RequireAdmin(),
db: AsyncSession = Depends(get_db),
):
"""Get a user by ID (admin only)."""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return UserResponse(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
)
@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
current_user: User = RequireAdmin(),
db: AsyncSession = Depends(get_db),
):
"""Update a user (admin only)."""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Prevent deactivating the last admin
if user_data.is_active is False and user.role == "admin":
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.is_active.is_(True)))
admin_count = len(admin_count_result.scalars().all())
if admin_count <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot deactivate the last admin user",
)
# Prevent changing role of last admin
if user_data.role and user_data.role != "admin" and user.role == "admin":
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.is_active.is_(True)))
admin_count = len(admin_count_result.scalars().all())
if admin_count <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot change role of the last admin user",
)
if user_data.username is not None:
# Check if new username already exists
existing_user = await db.execute(select(User).where(User.username == user_data.username, User.id != user_id))
if existing_user.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists",
)
user.username = user_data.username
if user_data.password is not None:
user.password_hash = get_password_hash(user_data.password)
if user_data.role is not None:
if user_data.role not in ["admin", "user"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Role must be 'admin' or 'user'",
)
user.role = user_data.role
if user_data.is_active is not None:
user.is_active = user_data.is_active
await db.commit()
await db.refresh(user)
return UserResponse(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: int,
current_user: User = RequireAdmin(),
db: AsyncSession = Depends(get_db),
):
"""Delete a user (admin only)."""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Prevent deleting the last admin
if user.role == "admin":
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.id != user_id))
admin_count = len(admin_count_result.scalars().all())
if admin_count == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete the last admin user",
)
# Prevent deleting yourself
if user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account",
)
await db.delete(user)
await db.commit()