feat: add system admin account management with RBAC
- AdminUser model: multi-admin accounts with bcrypt hashing (admin_users table) - Three roles: superadmin / admin / viewer with granular permission bits - auth.py: login migrated from env-var single admin to DB-backed accounts; seed_initial_admin() auto-creates first superadmin from SM_ADMIN_PASSWORD - Web UI: /system/admins page (add/edit role/toggle/reset pwd/delete) + change-my-password; sidebar entry; permission-guarded routes - REST API: /api/admins CRUD with protection checks - Protections: cannot delete/disable self; keep >=1 enabled superadmin; disabled accounts fail permission checks immediately - README: document roles, permission bits, API
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""REST API v1 — 全部路由。"""
|
||||
import os
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from auth import login_required
|
||||
from auth import login_required, permission_required
|
||||
from database import db
|
||||
from models import Instance, User, AuditLog
|
||||
from services import user_service, stats_service, backup_service
|
||||
@@ -279,3 +279,105 @@ def cleanup_backups():
|
||||
keep = request.json.get("keep", 10) if request.is_json else 10
|
||||
backup_service.cleanup_old_backups(keep=int(keep))
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 后台管理员账户
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
def _serialize_admin(a):
|
||||
return {
|
||||
"id": a.id,
|
||||
"username": a.username,
|
||||
"role": a.role,
|
||||
"role_label": a.role_label,
|
||||
"enabled": a.enabled,
|
||||
"last_login_at": a.last_login_at.isoformat() if a.last_login_at else None,
|
||||
"last_login_ip": a.last_login_ip,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/admins")
|
||||
@login_required
|
||||
@permission_required("admins:read")
|
||||
def list_admins():
|
||||
from models import AdminUser
|
||||
admins = AdminUser.query.order_by(AdminUser.id.asc()).all()
|
||||
return jsonify([_serialize_admin(a) for a in admins])
|
||||
|
||||
|
||||
@bp.route("/admins", methods=["POST"])
|
||||
@login_required
|
||||
@permission_required("admins:write")
|
||||
def create_admin():
|
||||
from models import AdminUser
|
||||
data = request.get_json(silent=True) or {}
|
||||
username = (data.get("username") or "").strip()
|
||||
password = data.get("password") or ""
|
||||
role = data.get("role", "viewer")
|
||||
if not username or not password:
|
||||
return jsonify({"error": "用户名和密码不能为空"}), 400
|
||||
if len(password) < 4:
|
||||
return jsonify({"error": "密码至少 4 位"}), 400
|
||||
if role not in AdminUser.ROLE_LABELS:
|
||||
role = "viewer"
|
||||
if AdminUser.query.filter_by(username=username).first():
|
||||
return jsonify({"error": "用户名已存在"}), 409
|
||||
admin = AdminUser()
|
||||
admin.username = username
|
||||
admin.role = role
|
||||
admin.enabled = True
|
||||
admin.set_password(password)
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
return jsonify(_serialize_admin(admin)), 201
|
||||
|
||||
|
||||
@bp.route("/admins/<int:aid>", methods=["DELETE"])
|
||||
@login_required
|
||||
@permission_required("admins:write")
|
||||
def delete_admin_api(aid):
|
||||
from models import AdminUser
|
||||
from auth import current_admin
|
||||
admin = AdminUser.query.get_or_404(aid)
|
||||
curr = current_admin()
|
||||
if curr and curr.id == admin.id:
|
||||
return jsonify({"error": "不能删除当前登录账户"}), 400
|
||||
if admin.role == "superadmin":
|
||||
super_count = AdminUser.query.filter_by(role="superadmin", enabled=True).count()
|
||||
if super_count <= 1:
|
||||
return jsonify({"error": "至少需要保留一个启用的超级管理员"}), 400
|
||||
db.session.delete(admin)
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "deleted": admin.username})
|
||||
|
||||
|
||||
@bp.route("/admins/<int:aid>", methods=["PUT"])
|
||||
@login_required
|
||||
@permission_required("admins:write")
|
||||
def update_admin_api(aid):
|
||||
from models import AdminUser
|
||||
admin = AdminUser.query.get_or_404(aid)
|
||||
data = request.get_json(silent=True) or {}
|
||||
# 角色更新
|
||||
role = data.get("role")
|
||||
if role:
|
||||
if role not in AdminUser.ROLE_LABELS:
|
||||
return jsonify({"error": "无效的角色"}), 400
|
||||
admin.role = role
|
||||
# 密码更新
|
||||
password = data.get("password")
|
||||
if password:
|
||||
if len(password) < 4:
|
||||
return jsonify({"error": "密码至少 4 位"}), 400
|
||||
admin.set_password(password)
|
||||
# 启停
|
||||
if "enabled" in data:
|
||||
enabled = bool(data["enabled"])
|
||||
from auth import current_admin
|
||||
curr = current_admin()
|
||||
if curr and curr.id == admin.id and not enabled:
|
||||
return jsonify({"error": "不能禁用当前登录账户"}), 400
|
||||
admin.enabled = enabled
|
||||
db.session.commit()
|
||||
return jsonify(_serialize_admin(admin))
|
||||
|
||||
Reference in New Issue
Block a user