import uuid from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import get_db, require_operations_manager from app.core.security import hash_password from app.models.user import User from app.schemas import CreateUserRequest, CurrentUser, UpdateUserRequest, UserOut from app.services.audit import record_audit_event router = APIRouter(prefix="/api/v1/users", tags=["users"]) def _to_out(user: User) -> UserOut: return UserOut( public_ref=user.public_ref, email=user.email, display_name=user.display_name, role=user.role, # type: ignore[arg-type] active=user.active, ) @router.get("", response_model=list[UserOut]) def list_users( db: Session = Depends(get_db), _manager: CurrentUser = Depends(require_operations_manager), ) -> list[UserOut]: return [_to_out(user) for user in db.scalars(select(User).order_by(User.display_name)).all()] @router.post("", response_model=UserOut, status_code=201) def create_user( body: CreateUserRequest, db: Session = Depends(get_db), manager: CurrentUser = Depends(require_operations_manager), ) -> UserOut: email = body.email.strip().lower() if db.scalar(select(User.id).where(User.email == email)) is not None: raise HTTPException(status_code=409, detail="A user with this email already exists") user = User( public_ref=f"USR-{uuid.uuid4().hex[:8].upper()}", email=email, password_hash=hash_password(body.password), display_name=body.display_name.strip(), role=body.role, active=True, ) db.add(user) db.flush() record_audit_event( db, actor_type="user", actor_label=manager.display_name, action="user_created", entity_type="user", entity_id=user.id, after={"public_ref": user.public_ref, "role": user.role, "active": user.active}, ) db.commit() return _to_out(user) @router.patch("/{public_ref}", response_model=UserOut) def update_user( public_ref: str, body: UpdateUserRequest, db: Session = Depends(get_db), manager: CurrentUser = Depends(require_operations_manager), ) -> UserOut: user = db.scalar(select(User).where(User.public_ref == public_ref).with_for_update()) if user is None: raise HTTPException(status_code=404, detail="User not found") if user.public_ref == manager.public_ref and body.active is False: raise HTTPException(status_code=409, detail="You cannot deactivate your own account") if user.public_ref == manager.public_ref and body.role not in (None, "operations_manager"): raise HTTPException(status_code=409, detail="You cannot remove your own manager role") before = {"display_name": user.display_name, "role": user.role, "active": user.active} if body.display_name is not None: user.display_name = body.display_name.strip() if body.role is not None: user.role = body.role if body.active is not None: user.active = body.active if body.password is not None: user.password_hash = hash_password(body.password) record_audit_event( db, actor_type="user", actor_label=manager.display_name, action="user_updated", entity_type="user", entity_id=user.id, before=before, after={"display_name": user.display_name, "role": user.role, "active": user.active}, ) db.commit() return _to_out(user)