From 6ecd1e2b3731cc978956513bf76873ef0ed58453 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) }