""" User management endpoints (admin only). GET /users – list all users GET /users/{id} – get one user PATCH /users/{id} – update display name, active status, or admin flag DELETE /users/{id} – soft-delete (deactivate) a user All endpoints require admin role. Self-deactivation / self-demotion is blocked to prevent admins from accidentally locking themselves out. """ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.auth.security import require_admin from app.config import settings from app.database import get_db from app.models.user import User from app.schemas.user import UserRead, UserUpdate router = APIRouter(prefix="/users", tags=["users"]) def _get_user_or_404(user_id: int, db: Session) -> User: """Fetch a user by PK or raise 404.""" user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found", ) return user @router.get("", response_model=list[UserRead]) def list_users( db: Session = Depends(get_db), _admin: User = Depends(require_admin), ) -> list[User]: """Return all registered users. Admin only.""" return db.query(User).order_by(User.created_at.desc()).all() @router.get("/{user_id}", response_model=UserRead) def get_user( user_id: int, db: Session = Depends(get_db), _admin: User = Depends(require_admin), ) -> User: """Return a single user by ID. Admin only.""" return _get_user_or_404(user_id, db) @router.patch("/{user_id}", response_model=UserRead) def update_user( user_id: int, body: UserUpdate, db: Session = Depends(get_db), admin: User = Depends(require_admin), ) -> User: """ Update a user's display name, active state, or admin flag. Guards: - Cannot demote the permanent admin e-mail address. - Cannot deactivate your own account (you'd be locked out). """ user = _get_user_or_404(user_id, db) # Block demotion of the hardcoded admin. if ( user.email == settings.admin_email.lower() and body.is_admin is not None and body.is_admin is False ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot demote the permanent admin account", ) # Block self-deactivation. if user.id == admin.id and body.is_active is False: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot deactivate your own account", ) # Apply updates – only fields explicitly provided in the request body. update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(user, field, value) db.commit() db.refresh(user) return user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) def deactivate_user( user_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin), ) -> None: """ Soft-delete a user by setting is_active=False. We do not hard-delete because items reference users via created_by_id / updated_by_id and we need the audit trail to remain intact. Cannot deactivate the permanent admin or yourself. """ user = _get_user_or_404(user_id, db) if user.email == settings.admin_email.lower(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot deactivate the permanent admin account", ) if user.id == admin.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot deactivate your own account", ) user.is_active = False db.commit()