"""Admin user management: list, create, delete admin users.""" from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.orm import joinedload from sqlalchemy.ext.asyncio import AsyncSession from app.auth import get_current_admin_user, hash_password from app.database import get_session from app.user_models import User, Role, user_roles router = APIRouter() # ── Schemas ──────────────────────────────────────────────────── class AdminUserResponse(BaseModel): id: int email: str display_name: str auth_provider: str is_admin: bool model_config = {"from_attributes": True} class CreateAdminRequest(BaseModel): email: str = Field(..., pattern=r'^[^@\s]+@[^@>\s]+.[^@\s.]+$') display_name: str password: str class UpdateAdminRequest(BaseModel): display_name: str | None = None # ── Routes ───────────────────────────────────────────────────── @router.get("/users", response_model=list[AdminUserResponse]) async def list_admin_users(session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_admin_user)): """List all users with admin access.""" result = await session.execute( select(User).options(joinedload(User.roles)) ) users = result.scalars().unique().all() # Filter to only admin users admins = [u for u in users if u.is_admin_effective] return admins @router.post("/users", response_model=AdminUserResponse, status_code=status.HTTP_201_CREATED) async def create_admin_user( payload: CreateAdminRequest, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_admin_user), ): """Create a new admin user. Assigns the 'admin' role automatically.""" # Check if user already exists existing = await session.execute(select(User).where(User.email == payload.email)) if existing.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"User with email {payload.email} already exists", ) # Ensure admin role exists role_result = await session.execute(select(Role).where(Role.name == "admin")) admin_role = role_result.scalar_one_or_none() if admin_role is None: admin_role = Role(name="admin", description="Full administrative access") session.add(admin_role) await session.flush() # Create user with hashed password user = User( email=payload.email, display_name=payload.display_name, auth_provider="local", password_hash=hash_password(payload.password), is_admin=True, ) user.roles.append(admin_role) session.add(user) await session.commit() await session.refresh(user) return user @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_admin_user( user_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_admin_user), ): """Delete an admin user. Cannot delete yourself. At least one admin must remain.""" # Prevent self-deletion if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete your own account", ) result = await session.execute( select(User).options(joinedload(User.roles)).where(User.id == user_id) ) user = result.unique().scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") # Ensure at least one admin remains (don't count the user being deleted) other_admins = await session.execute( select(User).options(joinedload(User.roles)).where(User.id != user_id) ) admin_count = sum(1 for u in other_admins.scalars().unique().all() if u.is_admin_effective) if admin_count == 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete the last admin user", ) # Remove all roles user.roles.clear() await session.delete(user) await session.commit() @router.put("/users/{user_id}", response_model=AdminUserResponse) async def update_admin_user( user_id: int, payload: UpdateAdminRequest, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_admin_user), ): """Update admin user details.""" result = await session.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if payload.display_name is not None: user.display_name = payload.display_name await session.commit() await session.refresh(user) return user