From 7bb3ef8d092962c347b367a805a6647e6e271c2e Mon Sep 17 00:00:00 2001
From: hjjjj <1311711287@qq.com>
Date: Fri, 27 Mar 2026 16:44:01 +0800
Subject: [PATCH] feat(auth): support multiple root admin emails and enhance
admin login verification
- Updated the environment variable configuration to allow multiple root admin emails via `ADMIN_EMAILS`, while maintaining compatibility with the legacy `ADMIN_EMAIL`.
- Modified the admin login verification process to check against the list of root admin emails.
- Enhanced the admin role management in the frontend to reflect the new multiple admin structure.
---
.env.example | 2 +
README.md | 2 +-
admin-web/index.html | 17 +++++++--
routes/auth.js | 91 +++++++++++++++++++++++++++-----------------
4 files changed, 74 insertions(+), 38 deletions(-)
diff --git a/.env.example b/.env.example
index addf9e1..d8447f3 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,8 @@ DB_NAME=skills_market
JWT_SECRET=your-jwt-secret-key-change-in-production
JWT_EXPIRES_IN=7d
+# Root admins (comma-separated). Legacy ADMIN_EMAIL is still supported.
+ADMIN_EMAILS=admin@example.com,ops@example.com
ADMIN_EMAIL=admin@example.com
# 登录白名单:固定验证码,不发邮件,多个邮箱用逗号分隔
diff --git a/README.md b/README.md
index 23ad657..8b66e44 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ npm run dev
- 登录方式:管理员邮箱验证码登录(`/api/auth/send-code` + `/api/admin/login`)
- 管理能力:用户列表、角色切换、权限配置(模式可见性 / 技能页 / 智能体页 / SSH 页 / 开发者模式)
- 相关接口:`/api/admin/login`、`/api/admin/users`、`/api/admin/users/:userId/permissions`、`/api/admin/audit-logs`
-- 权限规则:`ADMIN_EMAIL` 为初始管理员(root admin);其他管理员只能编辑非 admin 用户,且不能修改自己的权限
+- 权限规则:`ADMIN_EMAILS`(逗号分隔)定义初始管理员(root admin,兼容旧的 `ADMIN_EMAIL`);其他管理员只能编辑非 admin 用户,且不能修改自己的权限
## 环境变量
diff --git a/admin-web/index.html b/admin-web/index.html
index af49880..14bbce0 100644
--- a/admin-web/index.html
+++ b/admin-web/index.html
@@ -482,6 +482,7 @@
document.getElementById('userCount').textContent = String(users.length || 0)
document.getElementById('editableCount').textContent = String(users.filter((u) => !!u.editable).length)
const grantable = adminCapabilities.grantable_permissions || {}
+ const canEditAdmins = !!adminCapabilities.canEditAdmins
const grantableModes = Array.isArray(grantable.allowedModes) ? grantable.allowedModes : []
tbody.innerHTML = users
.map((u) => {
@@ -495,6 +496,16 @@
)
.join(' ')
const editable = !!u.editable
+ const roleOptions = [
+ ``,
+ ...(u.role === 'admin' || canEditAdmins
+ ? [``]
+ : [])
+ ].join('')
+ const roleHint =
+ !canEditAdmins && u.role !== 'admin'
+ ? '
仅初始管理员可授予 admin
'
+ : ''
return `
@@ -505,9 +516,9 @@
|
${u.role}
+ ${roleHint}
|
${modeChecks || '未授权'} |
${boolCell('canViewSkillsPage', !!p.canViewSkillsPage, !!grantable.canViewSkillsPage)} |
@@ -559,7 +570,7 @@
}
const data = await api('/api/auth/send-code', {
method: 'POST',
- body: JSON.stringify({ email })
+ body: JSON.stringify({ email, adminOnly: true })
})
if (!data.success) {
notify(data.error || '发送验证码失败', 'error')
diff --git a/routes/auth.js b/routes/auth.js
index da56829..7c5b422 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -4,7 +4,17 @@ const { sendVerificationCode, verifyCode } = require('../services/auth')
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'
-const ROOT_ADMIN_EMAIL = (process.env.ADMIN_EMAIL || '').trim().toLowerCase()
+const ROOT_ADMIN_EMAILS = Array.from(
+ new Set(
+ [
+ ...(process.env.ADMIN_EMAILS || '')
+ .split(',')
+ .map((e) => e.trim().toLowerCase())
+ .filter(Boolean),
+ (process.env.ADMIN_EMAIL || '').trim().toLowerCase()
+ ].filter(Boolean)
+ )
+)
const WHITELIST_CODE = process.env.WHITELIST_CODE || '888888'
const WHITELIST_EMAILS = (process.env.WHITELIST_EMAILS || '')
@@ -54,7 +64,8 @@ function normalizeRole(role) {
}
function isRootAdminEmail(email) {
- return !!ROOT_ADMIN_EMAIL && String(email || '').toLowerCase() === ROOT_ADMIN_EMAIL
+ const emailLower = String(email || '').toLowerCase()
+ return emailLower.length > 0 && ROOT_ADMIN_EMAILS.includes(emailLower)
}
function sanitizeUserForClient(userDoc) {
@@ -170,38 +181,40 @@ function createAuthRoutes(db) {
return {
async ensureAdminBootstrap() {
try {
- if (!ROOT_ADMIN_EMAIL) return
+ if (ROOT_ADMIN_EMAILS.length === 0) return
const now = new Date()
- const existing = await usersCollection.findOne({ email: ROOT_ADMIN_EMAIL })
- if (!existing) {
- await usersCollection.insertOne({
- email: ROOT_ADMIN_EMAIL,
- nickname: ROOT_ADMIN_EMAIL.split('@')[0],
- avatar: null,
- role: 'admin',
- permissions: getAdminDefaultPermissions(),
- status: 'active',
- created_at: now,
- updated_at: now,
- last_login: null
- })
- console.log('[Auth] Root admin bootstrap user created')
- return
- }
- await usersCollection.updateOne(
- { _id: existing._id },
- {
- $set: {
+ for (const rootEmail of ROOT_ADMIN_EMAILS) {
+ const existing = await usersCollection.findOne({ email: rootEmail })
+ if (!existing) {
+ await usersCollection.insertOne({
+ email: rootEmail,
+ nickname: rootEmail.split('@')[0],
+ avatar: null,
role: 'admin',
- permissions: sanitizePermissions({
- ...getAdminDefaultPermissions(),
- ...(existing.permissions || {})
- }),
- updated_at: now
- }
+ permissions: getAdminDefaultPermissions(),
+ status: 'active',
+ created_at: now,
+ updated_at: now,
+ last_login: null
+ })
+ console.log('[Auth] Root admin bootstrap user created:', rootEmail)
+ continue
}
- )
- console.log('[Auth] Root admin bootstrap user ensured')
+ await usersCollection.updateOne(
+ { _id: existing._id },
+ {
+ $set: {
+ role: 'admin',
+ permissions: sanitizePermissions({
+ ...getAdminDefaultPermissions(),
+ ...(existing.permissions || {})
+ }),
+ updated_at: now
+ }
+ }
+ )
+ console.log('[Auth] Root admin bootstrap user ensured:', rootEmail)
+ }
} catch (err) {
console.error('[Auth] ensureAdminBootstrap error:', err)
}
@@ -209,14 +222,24 @@ function createAuthRoutes(db) {
async sendCode(req, res) {
try {
- const { email } = req.body
+ const { email, adminOnly } = req.body
if (!email) {
return res.status(400).json({ success: false, error: '邮箱不能为空' })
}
- if (WHITELIST_EMAILS.includes(email.toLowerCase())) {
+ const emailLower = String(email).trim().toLowerCase()
+
+ if (adminOnly === true) {
+ const user = await usersCollection.findOne({ email: emailLower })
+ const canLoginAdmin = !!(user && isAdmin(user))
+ if (!canLoginAdmin) {
+ return res.status(403).json({ success: false, error: '仅管理员可获取登录验证码' })
+ }
+ }
+
+ if (WHITELIST_EMAILS.includes(emailLower)) {
return res.json({ success: true, message: '验证码已发送' })
}
- const result = await sendVerificationCode(db, email.toLowerCase())
+ const result = await sendVerificationCode(db, emailLower)
if (!result.success) {
return res.status(400).json(result)
}