"""User management router (admin only).""" import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import require_role from app.models.user import User from app.schemas.user import UserCreate, UserUpdate, UserOut from app.auth import hash_password from app.services.audit_service import log_action router = APIRouter(prefix="/users", tags=["users"]) VALID_ROLES = {"admin", "red_tech", "blue_tech", "red_lead", "blue_lead", "viewer"} # --------------------------------------------------------------------------- # GET /users — list all users # --------------------------------------------------------------------------- @router.get("", response_model=list[UserOut]) def list_users( db: Session = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """Return a list of all users. **Requires admin role.**""" return db.query(User).order_by(User.username).all() # --------------------------------------------------------------------------- # POST /users — create a new user # --------------------------------------------------------------------------- @router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) def create_user( payload: UserCreate, db: Session = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """Create a new user. **Requires admin role.**""" # Check if username already exists existing = db.query(User).filter(User.username == payload.username).first() if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Username '{payload.username}' already exists", ) # Validate role if payload.role not in VALID_ROLES: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid role '{payload.role}'. Must be one of: {', '.join(sorted(VALID_ROLES))}", ) user = User( username=payload.username, email=payload.email, hashed_password=hash_password(payload.password), role=payload.role, ) db.add(user) db.commit() db.refresh(user) log_action( db, user_id=current_user.id, action="create_user", entity_type="user", entity_id=user.id, details={"username": user.username, "role": user.role}, ) return user # --------------------------------------------------------------------------- # GET /users/{id} — get a single user # --------------------------------------------------------------------------- @router.get("/{user_id}", response_model=UserOut) def get_user( user_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """Return a single user by ID. **Requires admin role.**""" user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) return user # --------------------------------------------------------------------------- # PATCH /users/{id} — update a user # --------------------------------------------------------------------------- @router.patch("/{user_id}", response_model=UserOut) def update_user( user_id: uuid.UUID, payload: UserUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """Update one or more fields of an existing user. **Requires admin role.**""" user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) update_data = payload.model_dump(exclude_unset=True) # Validate role if being updated if "role" in update_data and update_data["role"] not in VALID_ROLES: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid role '{update_data['role']}'. Must be one of: {', '.join(sorted(VALID_ROLES))}", ) # Hash password if being updated if "password" in update_data: update_data["hashed_password"] = hash_password(update_data.pop("password")) for field, value in update_data.items(): setattr(user, field, value) db.commit() db.refresh(user) log_action( db, user_id=current_user.id, action="update_user", entity_type="user", entity_id=user.id, details={"updated_fields": list(payload.model_dump(exclude_unset=True).keys())}, ) return user