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.
This commit is contained in:
parent
0a84043cc2
commit
6ecd1e2b37
@ -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
|
||||
|
||||
# 登录白名单:固定验证码,不发邮件,多个邮箱用逗号分隔
|
||||
|
||||
@ -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 用户,且不能修改自己的权限
|
||||
|
||||
## 环境变量
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user