hjjjj 7bb3ef8d09
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled
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.
2026-03-27 17:11:05 +08:00

571 lines
19 KiB
JavaScript

const jwt = require('jsonwebtoken')
const { ObjectId } = require('mongodb')
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_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 || '')
.split(',')
.map((e) => e.trim().toLowerCase())
.filter(Boolean)
const ALL_MODES = ['chat', 'clarify', 'cowork', 'video', 'code']
function getDefaultPermissions() {
return {
allowedModes: ['chat', 'clarify'],
canViewSkillsPage: false,
canViewAgentsPage: false,
canUseSshPage: false,
canUseDeveloperMode: false
}
}
function getAdminDefaultPermissions() {
return {
allowedModes: [...ALL_MODES],
canViewSkillsPage: true,
canViewAgentsPage: true,
canUseSshPage: true,
canUseDeveloperMode: true
}
}
function sanitizePermissions(raw) {
const defaults = getDefaultPermissions()
if (!raw || typeof raw !== 'object') return defaults
const next = { ...defaults }
const allowedModes = Array.isArray(raw.allowedModes)
? raw.allowedModes.filter((m) => ALL_MODES.includes(m))
: defaults.allowedModes
next.allowedModes = allowedModes.length > 0 ? allowedModes : ['chat']
next.canViewSkillsPage = !!raw.canViewSkillsPage
next.canViewAgentsPage = !!raw.canViewAgentsPage
next.canUseSshPage = !!raw.canUseSshPage
next.canUseDeveloperMode = !!raw.canUseDeveloperMode
return next
}
function normalizeRole(role) {
return role === 'admin' ? 'admin' : 'user'
}
function isRootAdminEmail(email) {
const emailLower = String(email || '').toLowerCase()
return emailLower.length > 0 && ROOT_ADMIN_EMAILS.includes(emailLower)
}
function sanitizeUserForClient(userDoc) {
const role = normalizeRole(userDoc.role)
const permissions = sanitizePermissions(userDoc.permissions)
const isRootAdmin = role === 'admin' && isRootAdminEmail(userDoc.email)
return {
id: userDoc._id.toString(),
email: userDoc.email,
nickname: userDoc.nickname,
avatar: userDoc.avatar ?? null,
created_at: userDoc.created_at,
role,
permissions,
is_root_admin: isRootAdmin
}
}
function canAccessFeature(user, permissionKey) {
const perms = sanitizePermissions(user.permissions)
return !!perms[permissionKey]
}
function canUseMode(user, mode) {
const perms = sanitizePermissions(user.permissions)
return perms.allowedModes.includes(mode)
}
function isAdmin(user) {
return normalizeRole(user.role) === 'admin'
}
function isRootAdmin(user) {
return isAdmin(user) && isRootAdminEmail(user.email)
}
function getGrantablePermissions(actor) {
if (isRootAdmin(actor)) {
return {
allowedModes: [...ALL_MODES],
canViewSkillsPage: true,
canViewAgentsPage: true,
canUseSshPage: true,
canUseDeveloperMode: true
}
}
return sanitizePermissions(actor.permissions)
}
function clampPermissionsByGrantable(perms, grantable) {
return {
allowedModes: perms.allowedModes.filter((m) => grantable.allowedModes.includes(m)),
canViewSkillsPage: !!perms.canViewSkillsPage && !!grantable.canViewSkillsPage,
canViewAgentsPage: !!perms.canViewAgentsPage && !!grantable.canViewAgentsPage,
canUseSshPage: !!perms.canUseSshPage && !!grantable.canUseSshPage,
canUseDeveloperMode: !!perms.canUseDeveloperMode && !!grantable.canUseDeveloperMode
}
}
function signUserToken(userDoc) {
return jwt.sign(
{
userId: userDoc._id.toString(),
email: userDoc.email,
role: normalizeRole(userDoc.role)
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
)
}
function createAuthRoutes(db) {
const usersCollection = db.collection('users')
const permissionAuditCollection = db.collection('permission_audit_logs')
async function ensureUserDefaults(userDoc) {
const now = new Date()
const role = isRootAdminEmail(userDoc.email) ? 'admin' : normalizeRole(userDoc.role)
const permissionBase = role === 'admin' ? getAdminDefaultPermissions() : getDefaultPermissions()
const permissions = sanitizePermissions({ ...permissionBase, ...(userDoc.permissions || {}) })
const needRoleFix = userDoc.role !== role
const needPermFix = JSON.stringify(userDoc.permissions || null) !== JSON.stringify(permissions)
if (needRoleFix || needPermFix) {
await usersCollection.updateOne(
{ _id: userDoc._id },
{ $set: { role, permissions, updated_at: now } }
)
}
return { ...userDoc, role, permissions }
}
async function appendPermissionAuditLog({ actor, targetBefore, targetAfter, req }) {
await permissionAuditCollection.insertOne({
actor_user_id: actor.id,
actor_email: actor.email,
actor_nickname: actor.nickname || '',
actor_role: actor.role,
actor_is_root_admin: isRootAdmin(actor),
target_user_id: targetBefore._id.toString(),
target_email: targetBefore.email,
target_nickname: targetBefore.nickname || '',
target_role_before: normalizeRole(targetBefore.role),
target_role_after: normalizeRole(targetAfter.role),
target_permissions_before: sanitizePermissions(targetBefore.permissions),
target_permissions_after: sanitizePermissions(targetAfter.permissions),
changed_by_self: actor.id === targetBefore._id.toString(),
ip: req.ip || '',
user_agent: req.headers['user-agent'] || '',
created_at: new Date()
})
}
return {
async ensureAdminBootstrap() {
try {
if (ROOT_ADMIN_EMAILS.length === 0) return
const now = new Date()
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: getAdminDefaultPermissions(),
status: 'active',
created_at: now,
updated_at: now,
last_login: null
})
console.log('[Auth] Root admin bootstrap user created:', rootEmail)
continue
}
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)
}
},
async sendCode(req, res) {
try {
const { email, adminOnly } = req.body
if (!email) {
return res.status(400).json({ success: false, error: '邮箱不能为空' })
}
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, emailLower)
if (!result.success) {
return res.status(400).json(result)
}
res.json({ success: true, message: '验证码已发送' })
} catch (err) {
console.error('[Auth] Send code error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async login(req, res) {
try {
const { email, code, nickname } = req.body
if (!email || !code) {
return res.status(400).json({ success: false, error: '邮箱和验证码不能为空' })
}
const emailLower = email.toLowerCase()
if (WHITELIST_EMAILS.includes(emailLower)) {
if (code !== WHITELIST_CODE) {
return res.status(400).json({ success: false, error: '验证码错误' })
}
} else {
const verifyResult = await verifyCode(db, emailLower, code)
if (!verifyResult.success) {
return res.status(400).json(verifyResult)
}
}
let user = await usersCollection.findOne({ email: emailLower })
const now = new Date()
if (!user) {
const role = isRootAdminEmail(emailLower) ? 'admin' : 'user'
const newUser = {
email: emailLower,
nickname: nickname || emailLower.split('@')[0],
avatar: null,
role,
permissions: role === 'admin' ? getAdminDefaultPermissions() : getDefaultPermissions(),
created_at: now,
updated_at: now,
last_login: now,
status: 'active'
}
const result = await usersCollection.insertOne(newUser)
user = { ...newUser, _id: result.insertedId }
} else {
user = await ensureUserDefaults(user)
await usersCollection.updateOne(
{ _id: user._id },
{ $set: { last_login: now, updated_at: now } }
)
user = { ...user, last_login: now, updated_at: now }
}
const token = signUserToken(user)
res.json({
success: true,
token,
user: sanitizeUserForClient(user)
})
} catch (err) {
console.error('[Auth] Login error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async adminLogin(req, res) {
try {
const { email, code } = req.body
if (!email || !code) {
return res.status(400).json({ success: false, error: '邮箱和验证码不能为空' })
}
const emailLower = String(email).trim().toLowerCase()
if (WHITELIST_EMAILS.includes(emailLower)) {
if (code !== WHITELIST_CODE) {
return res.status(400).json({ success: false, error: '验证码错误' })
}
} else {
const verifyResult = await verifyCode(db, emailLower, code)
if (!verifyResult.success) {
return res.status(400).json(verifyResult)
}
}
const user = await usersCollection.findOne({ email: emailLower })
if (!user) {
return res.status(403).json({ success: false, error: '仅管理员可登录管理系统' })
}
const safeUser = await ensureUserDefaults(user)
if (!isAdmin(safeUser)) {
return res.status(403).json({ success: false, error: '仅管理员可登录管理系统' })
}
await usersCollection.updateOne(
{ _id: safeUser._id },
{ $set: { last_login: new Date(), updated_at: new Date() } }
)
const token = signUserToken(safeUser)
res.json({
success: true,
token,
user: sanitizeUserForClient(safeUser)
})
} catch (err) {
console.error('[Auth] Admin login error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async verifyToken(req, res, next) {
try {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, error: '未登录' })
}
const token = authHeader.split(' ')[1]
try {
const decoded = jwt.verify(token, JWT_SECRET)
const user = await usersCollection.findOne({ _id: new ObjectId(decoded.userId) })
if (!user) {
return res.status(401).json({ success: false, error: '用户不存在' })
}
const safeUser = await ensureUserDefaults(user)
req.user = sanitizeUserForClient(safeUser)
next()
} catch {
return res.status(401).json({ success: false, error: 'Token无效或已过期' })
}
} catch (err) {
console.error('[Auth] Verify token error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async verifyAdmin(req, res, next) {
await this.verifyToken(req, res, () => {
if (!isAdmin(req.user)) {
return res.status(403).json({ success: false, error: '需要管理员权限' })
}
next()
})
},
hasPermission(user, permissionKey) {
return canAccessFeature(user, permissionKey)
},
isModeAllowed(user, mode) {
return canUseMode(user, mode)
},
getProfile(req, res) {
try {
res.json({ success: true, user: req.user })
} catch (err) {
console.error('[Auth] Get profile error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async updateProfile(req, res) {
try {
const { nickname, avatar } = req.body
const updateData = { updated_at: new Date() }
if (nickname) updateData.nickname = nickname
if (avatar) updateData.avatar = avatar
await usersCollection.updateOne({ _id: new ObjectId(req.user.id) }, { $set: updateData })
const updatedUser = await usersCollection.findOne({ _id: new ObjectId(req.user.id) })
const safeUser = updatedUser
? sanitizeUserForClient(updatedUser)
: { ...req.user, ...updateData }
res.json({ success: true, user: safeUser })
} catch (err) {
console.error('[Auth] Update profile error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async listUsers(req, res) {
try {
const keyword = String(req.query.query || '').trim()
const filter = keyword
? {
$or: [
{ email: { $regex: keyword, $options: 'i' } },
{ nickname: { $regex: keyword, $options: 'i' } }
]
}
: {}
const list = await usersCollection
.find(filter, {
projection: {
email: 1,
nickname: 1,
avatar: 1,
role: 1,
permissions: 1,
created_at: 1,
updated_at: 1,
last_login: 1,
status: 1
}
})
.sort({ created_at: -1 })
.toArray()
const actorIsRoot = isRootAdmin(req.user)
const grantable = getGrantablePermissions(req.user)
const users = list.map((u) => {
const safe = sanitizeUserForClient(u)
const targetIsAdmin = safe.role === 'admin'
const targetIsSelf = safe.id === req.user.id
const editable = actorIsRoot
? true
: !targetIsAdmin && !targetIsSelf
return { ...safe, editable }
})
res.json({
success: true,
total: users.length,
users,
admin_capabilities: {
grantable_permissions: grantable,
canEditAdmins: actorIsRoot,
canEditSelf: actorIsRoot
}
})
} catch (err) {
console.error('[Auth] listUsers error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async updateUserPermissions(req, res) {
try {
const userId = req.params.userId
if (!userId) {
return res.status(400).json({ success: false, error: 'userId required' })
}
const target = await usersCollection.findOne({ _id: new ObjectId(userId) })
if (!target) {
return res.status(404).json({ success: false, error: '用户不存在' })
}
const actor = req.user
const actorIsRoot = isRootAdmin(actor)
const grantable = getGrantablePermissions(actor)
const targetRole = normalizeRole(target.role)
const isSelf = target._id.toString() === actor.id
const patch = req.body || {}
const requestedRole =
patch.role !== undefined ? normalizeRole(patch.role) : targetRole
if (!actorIsRoot) {
if (isSelf) {
return res.status(403).json({ success: false, error: '不能修改自己的权限' })
}
if (targetRole === 'admin') {
return res.status(403).json({ success: false, error: '不能修改管理员权限' })
}
if (requestedRole === 'admin') {
return res.status(403).json({ success: false, error: '仅初始管理员可授予管理员权限' })
}
}
const nextRole = requestedRole
const mergedRawPerms = patch.permissions
? { ...(target.permissions || {}), ...patch.permissions }
: (target.permissions || {})
const nextPermissionsBase = sanitizePermissions(mergedRawPerms)
const nextPermissions = actorIsRoot
? nextPermissionsBase
: clampPermissionsByGrantable(nextPermissionsBase, grantable)
const now = new Date()
await usersCollection.updateOne(
{ _id: target._id },
{
$set: {
role: nextRole,
permissions: nextPermissions,
updated_at: now
}
}
)
const updated = await usersCollection.findOne({ _id: target._id })
await appendPermissionAuditLog({
actor,
targetBefore: target,
targetAfter: updated,
req
})
res.json({ success: true, user: sanitizeUserForClient(updated) })
} catch (err) {
console.error('[Auth] updateUserPermissions error:', err)
res.status(500).json({ success: false, error: err.message })
}
},
async listPermissionAuditLogs(req, res) {
try {
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200)
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0)
const logs = await permissionAuditCollection
.find({}, { projection: { _id: 0 } })
.sort({ created_at: -1 })
.skip(offset)
.limit(limit)
.toArray()
const total = await permissionAuditCollection.countDocuments({})
res.json({ success: true, total, logs })
} catch (err) {
console.error('[Auth] listPermissionAuditLogs error:', err)
res.status(500).json({ success: false, error: err.message })
}
}
}
}
module.exports = {
createAuthRoutes,
ALL_MODES,
getDefaultPermissions,
sanitizePermissions
}