"""User management endpoints (admin only).""" from __future__ import annotations from flask import Blueprint, current_app, jsonify, request from backend.app.auth import hash_password, role_required from backend.app.extensions import db from backend.app.models import User, UserRole from backend.app.serializers import serialize_user users_bp = Blueprint("users", __name__, url_prefix="/api/users") def _parse_role(value: object) -> UserRole | None: if not isinstance(value, str): return None try: return UserRole(value) except ValueError: return None @users_bp.get("") @role_required("admin") def list_users(): users = User.query.order_by(User.id.asc()).all() return jsonify([serialize_user(u) for u in users]), 200 @users_bp.post("") @role_required("admin") def create_user(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() password = data.get("password") or "" role_raw = (data.get("role") or "").strip() if not username: return jsonify({"error": "username is required"}), 400 min_len = current_app.config["MIN_PASSWORD_LENGTH"] if len(password) < min_len: return jsonify({"error": f"password must be at least {min_len} characters"}), 400 role = _parse_role(role_raw) if role is None: return jsonify({"error": "role must be one of: admin, redteam, soc"}), 400 if User.query.filter_by(username=username).first() is not None: return jsonify({"error": "username already exists"}), 400 user = User(username=username, password_hash=hash_password(password), role=role) db.session.add(user) db.session.commit() return jsonify(serialize_user(user)), 201 @users_bp.patch("/") @role_required("admin") def update_user(user_id: int): user = db.session.get(User, user_id) if user is None: return jsonify({"error": "User not found"}), 404 data = request.get_json(silent=True) or {} if "role" in data: new_role = _parse_role((data.get("role") or "").strip()) if new_role is None: return jsonify({"error": "role must be one of: admin, redteam, soc"}), 400 # Refuse to demote the last admin. if user.role == UserRole.ADMIN and new_role != UserRole.ADMIN: admin_count = User.query.filter_by(role=UserRole.ADMIN).count() if admin_count <= 1: return jsonify({"error": "Cannot demote the last admin"}), 409 user.role = new_role if "password" in data: password = data.get("password") or "" min_len = current_app.config["MIN_PASSWORD_LENGTH"] if len(password) < min_len: return ( jsonify({"error": f"password must be at least {min_len} characters"}), 400, ) user.password_hash = hash_password(password) db.session.commit() return jsonify(serialize_user(user)), 200 @users_bp.delete("/") @role_required("admin") def delete_user(user_id: int): user = db.session.get(User, user_id) if user is None: return jsonify({"error": "User not found"}), 404 if user.role == UserRole.ADMIN: admin_count = User.query.filter_by(role=UserRole.ADMIN).count() if admin_count <= 1: return jsonify({"error": "Cannot delete the last admin"}), 409 db.session.delete(user) db.session.commit() return "", 204