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
+61
View File
@@ -4,6 +4,67 @@ from database import db
import bcrypt
# ── 后台管理员 ──────────────────────────────────────────────────
class AdminUser(db.Model):
"""Web 后台管理员账户(存数据库,支持多账户、角色权限)。"""
__tablename__ = "admin_users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), nullable=False, unique=True, index=True)
password = db.Column(db.String(128), nullable=False) # bcrypt 哈希
role = db.Column(db.String(16), default="viewer") # superadmin | admin | viewer
enabled = db.Column(db.Boolean, default=True)
last_login_at = db.Column(db.DateTime, nullable=True)
last_login_ip = db.Column(db.String(64), nullable=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
ROLE_LABELS = {
"superadmin": "超级管理员",
"admin": "管理员",
"viewer": "只读用户",
}
# 角色权限位(可扩展)
ROLE_PERMISSIONS = {
"superadmin": {"users:read", "users:write", "instances:read", "instances:write",
"system:read", "system:write", "admins:read", "admins:write",
"logs:read", "stats:read"},
"admin": {"users:read", "users:write", "instances:read", "instances:write",
"system:read", "system:write", "admins:read",
"logs:read", "stats:read"},
"viewer": {"users:read", "instances:read", "system:read",
"logs:read", "stats:read"},
}
def set_password(self, password):
self.password = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
def check_password(self, password):
if not self.password:
return False
try:
return bcrypt.checkpw(
password.encode("utf-8"), self.password.encode("utf-8")
)
except Exception:
return False
@property
def role_label(self):
return self.ROLE_LABELS.get(self.role, self.role)
def has_permission(self, perm):
perms = self.ROLE_PERMISSIONS.get(self.role, set())
return perm in perms
def __repr__(self):
return f"<AdminUser {self.username} ({self.role})>"
# ── SOCKS5 实例 ──────────────────────────────────────────────────
class Instance(db.Model):
__tablename__ = "instances"