Merge pull request #322 from f3n9/dev-um-8

用户API Key管理相关优化
This commit is contained in:
Wesley Liddick
2025-09-03 15:24:00 +08:00
committed by GitHub
10 changed files with 436 additions and 18 deletions

View File

@@ -24,6 +24,51 @@ const ProxyHelper = require('../utils/proxyHelper')
const router = express.Router()
// 👥 用户管理
// 获取所有用户列表用于API Key分配
router.get('/users', authenticateAdmin, async (req, res) => {
try {
const userService = require('../services/userService')
const result = await userService.getAllUsers({ isActive: true, limit: 1000 }) // Get all active users
// Extract users array from the paginated result
const allUsers = result.users || []
// Map to the format needed for the dropdown
const activeUsers = allUsers.map((user) => ({
id: user.id,
username: user.username,
displayName: user.displayName || user.username,
email: user.email,
role: user.role
}))
// 添加Admin选项作为第一个
const usersWithAdmin = [
{
id: 'admin',
username: 'admin',
displayName: 'Admin',
email: '',
role: 'admin'
},
...activeUsers
]
return res.json({
success: true,
data: usersWithAdmin
})
} catch (error) {
logger.error('❌ Failed to get users list:', error)
return res.status(500).json({
error: 'Failed to get users list',
message: error.message
})
}
})
// 🔑 API Keys 管理
// 调试获取API Key费用详情
@@ -63,6 +108,9 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
const { timeRange = 'all' } = req.query // all, 7days, monthly
const apiKeys = await apiKeyService.getAllApiKeys()
// 获取用户服务来补充owner信息
const userService = require('../services/userService')
// 根据时间范围计算查询模式
const now = new Date()
const searchPatterns = []
@@ -313,6 +361,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
}
}
// 为每个API Key添加owner的displayName
for (const apiKey of apiKeys) {
// 如果API Key有关联的用户ID获取用户信息
if (apiKey.userId) {
try {
const user = await userService.getUserById(apiKey.userId, false)
if (user) {
apiKey.ownerDisplayName = user.displayName || user.username || 'Unknown User'
} else {
apiKey.ownerDisplayName = 'Unknown User'
}
} catch (error) {
logger.debug(`无法获取用户 ${apiKey.userId} 的信息:`, error)
apiKey.ownerDisplayName = 'Unknown User'
}
} else {
// 如果没有userId使用createdBy字段或默认为Admin
apiKey.ownerDisplayName =
apiKey.createdBy === 'admin' ? 'Admin' : apiKey.createdBy || 'Admin'
}
}
return res.json({ success: true, data: apiKeys })
} catch (error) {
logger.error('❌ Failed to get API keys:', error)
@@ -822,7 +892,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
expiresAt,
dailyCostLimit,
weeklyOpusCostLimit,
tags
tags,
ownerId // 新增所有者ID字段
} = req.body
// 只允许更新指定字段
@@ -992,6 +1063,45 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
updates.isActive = isActive
}
// 处理所有者变更
if (ownerId !== undefined) {
const userService = require('../services/userService')
if (ownerId === 'admin') {
// 分配给Admin
updates.userId = ''
updates.userUsername = ''
updates.createdBy = 'admin'
} else if (ownerId) {
// 分配给用户
try {
const user = await userService.getUserById(ownerId, false)
if (!user) {
return res.status(400).json({ error: 'Invalid owner: User not found' })
}
if (!user.isActive) {
return res.status(400).json({ error: 'Cannot assign to inactive user' })
}
// 设置新的所有者信息
updates.userId = ownerId
updates.userUsername = user.username
updates.createdBy = user.username
// 管理员重新分配时不检查用户的API Key数量限制
logger.info(`🔄 Admin reassigning API key ${keyId} to user ${user.username}`)
} catch (error) {
logger.error('Error fetching user for owner reassignment:', error)
return res.status(400).json({ error: 'Invalid owner ID' })
}
} else {
// 清空所有者分配给Admin
updates.userId = ''
updates.userUsername = ''
updates.createdBy = 'admin'
}
}
await apiKeyService.updateApiKey(keyId, updates)
logger.success(`📝 Admin updated API key: ${keyId}`)

View File

@@ -208,7 +208,8 @@ router.get('/profile', authenticateUser, async (req, res) => {
totalUsage: user.totalUsage
},
config: {
maxApiKeysPerUser: config.userManagement.maxApiKeysPerUser
maxApiKeysPerUser: config.userManagement.maxApiKeysPerUser,
allowUserDeleteApiKeys: config.userManagement.allowUserDeleteApiKeys
}
})
} catch (error) {
@@ -352,6 +353,15 @@ router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => {
try {
const { keyId } = req.params
// 检查是否允许用户删除自己的API Keys
if (!config.userManagement.allowUserDeleteApiKeys) {
return res.status(403).json({
error: 'Operation not allowed',
message:
'Users are not allowed to delete their own API keys. Please contact an administrator.'
})
}
// 检查API Key是否属于当前用户
const existingKey = await apiKeyService.getApiKeyById(keyId)
if (!existingKey || existingKey.userId !== req.user.id) {

View File

@@ -368,7 +368,10 @@ class ApiKeyService {
'allowedClients',
'dailyCostLimit',
'weeklyOpusCostLimit',
'tags'
'tags',
'userId', // 新增用户ID所有者变更
'userUsername', // 新增:用户名(所有者变更)
'createdBy' // 新增:创建者(所有者变更)
]
const updatedData = { ...keyData }

View File

@@ -97,6 +97,38 @@ class LdapService {
return null
}
// 🌐 从DN中提取域名用于Windows AD UPN格式认证
extractDomainFromDN(dnString) {
try {
if (!dnString || typeof dnString !== 'string') {
return null
}
// 提取所有DC组件DC=test,DC=demo,DC=com
const dcMatches = dnString.match(/DC=([^,]+)/gi)
if (!dcMatches || dcMatches.length === 0) {
return null
}
// 提取DC值并连接成域名
const domainParts = dcMatches.map((match) => {
const value = match.replace(/DC=/i, '').trim()
return value
})
if (domainParts.length > 0) {
const domain = domainParts.join('.')
logger.debug(`🌐 从DN提取域名: ${domain}`)
return domain
}
return null
} catch (error) {
logger.debug('⚠️ 域名提取失败:', error.message)
return null
}
}
// 🔗 创建LDAP客户端连接
createClient() {
try {
@@ -336,6 +368,79 @@ class LdapService {
})
}
// 🔐 Windows AD兼容认证 - 在DN认证失败时尝试多种格式
async tryWindowsADAuthentication(username, password) {
if (!username || !password) {
return false
}
// 从searchBase提取域名
const domain = this.extractDomainFromDN(this.config.server.searchBase)
const adFormats = []
if (domain) {
// UPN格式Windows AD标准
adFormats.push(`${username}@${domain}`)
// 如果域名有多个部分,也尝试简化版本
const domainParts = domain.split('.')
if (domainParts.length > 1) {
adFormats.push(`${username}@${domainParts.slice(-2).join('.')}`) // 只取后两部分
}
// 域\用户名格式
const firstDomainPart = domainParts[0]
if (firstDomainPart) {
adFormats.push(`${firstDomainPart}\\${username}`)
adFormats.push(`${firstDomainPart.toUpperCase()}\\${username}`)
}
}
// 纯用户名(最后尝试)
adFormats.push(username)
logger.info(`🔄 尝试 ${adFormats.length} 种Windows AD认证格式...`)
for (const format of adFormats) {
try {
logger.info(`🔍 尝试格式: ${format}`)
const result = await this.tryDirectBind(format, password)
if (result) {
logger.info(`✅ Windows AD认证成功: ${format}`)
return true
}
logger.debug(`❌ 认证失败: ${format}`)
} catch (error) {
logger.debug(`认证异常 ${format}:`, error.message)
}
}
logger.info(`🚫 所有Windows AD格式认证都失败了`)
return false
}
// 🔐 直接尝试绑定认证的辅助方法
async tryDirectBind(identifier, password) {
return new Promise((resolve, reject) => {
const authClient = this.createClient()
authClient.bind(identifier, password, (err) => {
authClient.unbind()
if (err) {
if (err.name === 'InvalidCredentialsError') {
resolve(false)
} else {
reject(err)
}
} else {
resolve(true)
}
})
})
}
// 📝 提取用户信息
extractUserInfo(ldapEntry, username) {
try {
@@ -478,10 +583,32 @@ class LdapService {
return { success: false, message: 'Authentication service error' }
}
// 4. 验证用户密码
const isPasswordValid = await this.authenticateUser(userDN, password)
// 4. 验证用户密码 - 支持传统LDAP和Windows AD
let isPasswordValid = false
// 首先尝试传统的DN认证保持原有LDAP逻辑
try {
isPasswordValid = await this.authenticateUser(userDN, password)
if (isPasswordValid) {
logger.info(`✅ DN authentication successful for user: ${sanitizedUsername}`)
}
} catch (error) {
logger.debug(
`DN authentication failed for user: ${sanitizedUsername}, error: ${error.message}`
)
}
// 如果DN认证失败尝试Windows AD多格式认证
if (!isPasswordValid) {
logger.info(`🚫 Invalid password for user: ${sanitizedUsername}`)
logger.debug(`🔄 Trying Windows AD authentication formats for user: ${sanitizedUsername}`)
isPasswordValid = await this.tryWindowsADAuthentication(sanitizedUsername, password)
if (isPasswordValid) {
logger.info(`✅ Windows AD authentication successful for user: ${sanitizedUsername}`)
}
}
if (!isPasswordValid) {
logger.info(`🚫 All authentication methods failed for user: ${sanitizedUsername}`)
return { success: false, message: 'Invalid username or password' }
}

View File

@@ -75,6 +75,11 @@ class UserService {
await redis.set(`${this.userPrefix}${user.id}`, JSON.stringify(user))
await redis.set(`${this.usernamePrefix}${username}`, user.id)
// 如果是新用户尝试转移匹配的API Keys
if (isNewUser) {
await this.transferMatchingApiKeys(user)
}
logger.info(`📝 ${isNewUser ? 'Created' : 'Updated'} user: ${username} (${user.id})`)
return user
} catch (error) {
@@ -509,6 +514,80 @@ class UserService {
throw error
}
}
// 🔄 转移匹配的API Keys给新用户
async transferMatchingApiKeys(user) {
try {
const apiKeyService = require('./apiKeyService')
const { displayName, username, email } = user
// 获取所有API Keys
const allApiKeys = await apiKeyService.getAllApiKeys()
// 找到没有用户ID的API Keys即由Admin创建的
const unownedApiKeys = allApiKeys.filter((key) => !key.userId || key.userId === '')
if (unownedApiKeys.length === 0) {
logger.debug(`📝 No unowned API keys found for potential transfer to user: ${username}`)
return
}
// 构建匹配字符串数组只考虑displayName、username、email去除空值和重复值
const matchStrings = new Set()
if (displayName) {
matchStrings.add(displayName.toLowerCase().trim())
}
if (username) {
matchStrings.add(username.toLowerCase().trim())
}
if (email) {
matchStrings.add(email.toLowerCase().trim())
}
const matchingKeys = []
// 查找名称匹配的API Keys只进行完全匹配
for (const apiKey of unownedApiKeys) {
const keyName = apiKey.name ? apiKey.name.toLowerCase().trim() : ''
// 检查API Key名称是否与用户信息完全匹配
for (const matchString of matchStrings) {
if (keyName === matchString) {
matchingKeys.push(apiKey)
break // 找到匹配后跳出内层循环
}
}
}
// 转移匹配的API Keys
let transferredCount = 0
for (const apiKey of matchingKeys) {
try {
await apiKeyService.updateApiKey(apiKey.id, {
userId: user.id,
userUsername: user.username,
createdBy: user.username
})
transferredCount++
logger.info(`🔄 Transferred API key "${apiKey.name}" (${apiKey.id}) to user: ${username}`)
} catch (error) {
logger.error(`❌ Failed to transfer API key ${apiKey.id} to user ${username}:`, error)
}
}
if (transferredCount > 0) {
logger.success(
`🎉 Successfully transferred ${transferredCount} API key(s) to new user: ${username} (${displayName})`
)
} else if (matchingKeys.length === 0) {
logger.debug(`📝 No matching API keys found for user: ${username} (${displayName})`)
}
} catch (error) {
logger.error('❌ Error transferring matching API keys:', error)
// Don't throw error to prevent blocking user creation
}
}
}
module.exports = new UserService()