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_EMAIL = (process.env.ADMIN_EMAIL || '').trim().toLowerCase() 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) { return !!ROOT_ADMIN_EMAIL && String(email || '').toLowerCase() === ROOT_ADMIN_EMAIL } 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_EMAIL) 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: { role: 'admin', permissions: sanitizePermissions({ ...getAdminDefaultPermissions(), ...(existing.permissions || {}) }), updated_at: now } } ) console.log('[Auth] Root admin bootstrap user ensured') } catch (err) { console.error('[Auth] ensureAdminBootstrap error:', err) } }, async sendCode(req, res) { try { const { email } = req.body if (!email) { return res.status(400).json({ success: false, error: '邮箱不能为空' }) } if (WHITELIST_EMAILS.includes(email.toLowerCase())) { return res.json({ success: true, message: '验证码已发送' }) } const result = await sendVerificationCode(db, email.toLowerCase()) 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 }