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:
Your Name
2026-09-09 14:33:59 +08:00
parent 3232159d29
commit 9890cfc488
8 changed files with 719 additions and 51 deletions
+103 -1
View File
@@ -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))