feat(auth): support multiple root admin emails and enhance admin login verification
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled

- 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.
This commit is contained in:
hjjjj 2026-03-27 16:44:01 +08:00
parent 1778badacf
commit 7bb3ef8d09
4 changed files with 74 additions and 38 deletions

View File

@ -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
# 登录白名单:固定验证码,不发邮件,多个邮箱用逗号分隔

View File

@ -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 用户,且不能修改自己的权限
## 环境变量

View File

@ -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 = [
`<option value="user" ${u.role === 'user' ? 'selected' : ''}>user</option>`,
...(u.role === 'admin' || canEditAdmins
? [`<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>admin</option>`]
: [])
].join('')
const roleHint =
!canEditAdmins && u.role !== 'admin'
? '<div class="muted">仅初始管理员可授予 admin</div>'
: ''
return `
<tr data-id="${u.id}">
@ -505,9 +516,9 @@
<td>
<div class="role ${u.role === 'admin' ? 'role-admin' : 'role-user'}">${u.role}</div>
<select data-field="role" ${editable ? '' : 'disabled'}>
<option value="user" ${u.role === 'user' ? 'selected' : ''}>user</option>
<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>admin</option>
${roleOptions}
</select>
${roleHint}
</td>
<td><div class="mode-list">${modeChecks || '<span class="muted">未授权</span>'}</div></td>
<td>${boolCell('canViewSkillsPage', !!p.canViewSkillsPage, !!grantable.canViewSkillsPage)}</td>
@ -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')

View File

@ -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,13 +181,14 @@ 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 })
for (const rootEmail of ROOT_ADMIN_EMAILS) {
const existing = await usersCollection.findOne({ email: rootEmail })
if (!existing) {
await usersCollection.insertOne({
email: ROOT_ADMIN_EMAIL,
nickname: ROOT_ADMIN_EMAIL.split('@')[0],
email: rootEmail,
nickname: rootEmail.split('@')[0],
avatar: null,
role: 'admin',
permissions: getAdminDefaultPermissions(),
@ -185,8 +197,8 @@ function createAuthRoutes(db) {
updated_at: now,
last_login: null
})
console.log('[Auth] Root admin bootstrap user created')
return
console.log('[Auth] Root admin bootstrap user created:', rootEmail)
continue
}
await usersCollection.updateOne(
{ _id: existing._id },
@ -201,7 +213,8 @@ function createAuthRoutes(db) {
}
}
)
console.log('[Auth] Root admin bootstrap user ensured')
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)
}