diff --git a/src/routes/admin.js b/src/routes/admin.js index 86556904..645bcebc 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -791,6 +791,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const { + name, + description, tokenLimit, concurrencyLimit, rateLimitWindow, @@ -814,6 +816,30 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { // 只允许更新指定字段 const updates = {} + // 处理name字段 + if (name !== undefined) { + if (name === null || name === '') { + return res.status(400).json({ error: 'Name cannot be empty' }) + } + if (typeof name !== 'string' || name.trim().length === 0) { + return res.status(400).json({ error: 'Name must be a non-empty string' }) + } + if (name.length > 100) { + return res.status(400).json({ error: 'Name must be less than 100 characters' }) + } + updates.name = name.trim() + } + + // 处理description字段 + if (description !== undefined) { + if (description && (typeof description !== 'string' || description.length > 500)) { + return res + .status(400) + .json({ error: 'Description must be a string with less than 500 characters' }) + } + updates.description = description || '' + } + if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') { if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) { return res.status(400).json({ error: 'Token limit must be a non-negative integer' }) @@ -954,12 +980,20 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.isActive = isActive } + logger.info(`🔧 Admin updating API key: ${keyId}`, { + updates: Object.keys(updates), + updatesData: updates + }) + await apiKeyService.updateApiKey(keyId, updates) logger.success(`📝 Admin updated API key: ${keyId}`) return res.json({ success: true, message: 'API key updated successfully' }) } catch (error) { - logger.error('❌ Failed to update API key:', error) + logger.error(`❌ Failed to update API key ${req.params.keyId}:`, { + error: error.message, + stack: error.stack + }) return res.status(500).json({ error: 'Failed to update API key', message: error.message }) } }) diff --git a/src/routes/ldapRoutes.js b/src/routes/ldapRoutes.js index 133a558b..5b207fff 100644 --- a/src/routes/ldapRoutes.js +++ b/src/routes/ldapRoutes.js @@ -397,57 +397,44 @@ const authenticateUser = (req, res, next) => { */ router.get('/user/api-keys', authenticateUser, async (req, res) => { try { + const apiKeyService = require('../services/apiKeyService') const redis = require('../models/redis') const { username, displayName } = req.user logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`) - logger.info(`用户完整信息: ${JSON.stringify(req.user)}`) - // 获取所有API Keys - const allKeysPattern = 'api_key:*' - const keys = await redis.getClient().keys(allKeysPattern) + // 使用与admin相同的API Key服务,获取所有API Keys的完整信息 + const allApiKeys = await apiKeyService.getAllApiKeys() const userKeys = [] let foundHistoricalKey = false - // 筛选属于该用户的API Keys - for (const key of keys) { - const apiKeyData = await redis.getClient().hgetall(key) - if (!apiKeyData) { - continue - } + // 筛选属于该用户的API Keys,并处理自动关联 + for (const apiKey of allApiKeys) { + logger.debug( + `检查API Key: ${apiKey.id}, name: "${apiKey.name}", owner: "${apiKey.owner || '无'}", displayName: "${displayName}"` + ) // 规则1: 直接owner匹配(已关联的Key) - if (apiKeyData.owner === username) { - userKeys.push({ - id: apiKeyData.id, - name: apiKeyData.name || '未命名', - key: apiKeyData.key, - limit: parseInt(apiKeyData.limit) || 1000000, - used: parseInt(apiKeyData.used) || 0, - createdAt: apiKeyData.createdAt, - status: apiKeyData.status || 'active' - }) + if (apiKey.owner === username) { + logger.info(`找到已关联的API Key: ${apiKey.id}`) + userKeys.push(apiKey) } // 规则2: 历史Key自动关联(name字段匹配displayName且无owner) - else if (displayName && apiKeyData.name === displayName && !apiKeyData.owner) { - logger.info(`发现历史API Key需要关联: name=${apiKeyData.name}, displayName=${displayName}`) + else if (displayName && apiKey.name === displayName && !apiKey.owner) { + logger.info( + `🔗 发现历史API Key需要关联: id=${apiKey.id}, name="${apiKey.name}", displayName="${displayName}"` + ) // 自动关联: 设置owner为当前用户 - await redis.getClient().hset(key, 'owner', username) + await redis.getClient().hset(`apikey:${apiKey.id}`, 'owner', username) foundHistoricalKey = true - userKeys.push({ - id: apiKeyData.id, - name: apiKeyData.name || '未命名', - key: apiKeyData.key, - limit: parseInt(apiKeyData.limit) || 1000000, - used: parseInt(apiKeyData.used) || 0, - createdAt: apiKeyData.createdAt, - status: apiKeyData.status || 'active' - }) + // 更新本地数据并添加到用户Key列表 + apiKey.owner = username + userKeys.push(apiKey) - logger.info(`历史API Key关联成功: ${apiKeyData.id} -> ${username}`) + logger.info(`✅ 历史API Key关联成功: ${apiKey.id} -> ${username}`) } } @@ -474,11 +461,11 @@ router.get('/user/api-keys', authenticateUser, async (req, res) => { router.post('/user/api-keys', authenticateUser, async (req, res) => { try { const { username } = req.user - const { name, limit } = req.body + const { limit } = req.body // 检查用户是否已有API Key const redis = require('../models/redis') - const allKeysPattern = 'api_key:*' + const allKeysPattern = 'apikey:*' const keys = await redis.getClient().keys(allKeysPattern) let userKeyCount = 0 @@ -496,45 +483,53 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => { }) } - // 生成API Key - const crypto = require('crypto') - const uuid = require('uuid') + // 使用与admin相同的API Key生成服务,确保数据结构一致性 + const apiKeyService = require('../services/apiKeyService') - const keyId = uuid.v4() - const apiKey = `cr_${crypto.randomBytes(32).toString('hex')}` + // 获取用户的显示名称 + const { displayName } = req.user + // 用户创建的API Key名称固定为displayName,不允许自定义 + const defaultName = displayName || username - const keyData = { - id: keyId, - key: apiKey, - name: name || 'AD用户密钥', - limit: limit || 100000, - used: 0, + const keyParams = { + name: defaultName, // 忽略用户输入的name,强制使用displayName + tokenLimit: limit || 0, + description: `AD用户${username}创建的API Key`, + // AD用户创建的Key添加owner信息以区分用户归属 owner: username, ownerType: 'ad_user', - createdAt: new Date().toISOString(), - status: 'active' + // 确保用户创建的Key默认激活 + isActive: true, + // 设置基本权限(与admin创建保持一致) + permissions: 'all', + // 设置合理的并发和速率限制(与admin创建保持一致) + concurrencyLimit: 0, + rateLimitWindow: 0, + rateLimitRequests: 0, + // 添加标签标识AD用户创建 + tags: ['ad-user', 'user-created'] } - // 存储到Redis - await redis.getClient().hset(`api_key:${keyId}`, keyData) + const newKey = await apiKeyService.generateApiKey(keyParams) - // 创建哈希映射以快速查找 - const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex') - await redis.getClient().set(`api_key_hash:${keyHash}`, keyId) - - logger.info(`用户${username}创建API Key成功: ${keyId}`) + logger.info(`用户${username}创建API Key成功: ${newKey.id}`) res.json({ success: true, message: 'API Key创建成功', apiKey: { - id: keyId, - key: apiKey, - name: keyData.name, - limit: keyData.limit, + id: newKey.id, + key: newKey.apiKey, // 返回完整的API Key + name: newKey.name, + tokenLimit: newKey.tokenLimit || limit || 0, used: 0, - createdAt: keyData.createdAt, - status: keyData.status + createdAt: newKey.createdAt, + isActive: true, + usage: { + daily: { requests: 0, tokens: 0 }, + total: { requests: 0, tokens: 0 } + }, + dailyCost: 0 } }) } catch (error) { @@ -555,7 +550,7 @@ router.get('/user/usage-stats', authenticateUser, async (req, res) => { const redis = require('../models/redis') // 获取用户的API Keys - const allKeysPattern = 'api_key:*' + const allKeysPattern = 'apikey:*' const keys = await redis.getClient().keys(allKeysPattern) let totalUsage = 0 @@ -600,4 +595,88 @@ router.get('/user/usage-stats', authenticateUser, async (req, res) => { } }) +/** + * 更新用户API Key + */ +router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => { + try { + const { username } = req.user + const { keyId } = req.params + const updates = req.body + + // 验证用户只能编辑自己的API Key + const apiKeyService = require('../services/apiKeyService') + const allApiKeys = await apiKeyService.getAllApiKeys() + const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username) + + if (!apiKey) { + return res.status(404).json({ + success: false, + message: 'API Key 不存在或无权限' + }) + } + + // 限制用户只能修改特定字段 + const allowedFields = ['name', 'description', 'isActive'] + const filteredUpdates = {} + for (const [key, value] of Object.entries(updates)) { + if (allowedFields.includes(key)) { + filteredUpdates[key] = value + } + } + + await apiKeyService.updateApiKey(keyId, filteredUpdates) + + logger.info(`用户 ${username} 更新了 API Key: ${keyId}`) + + res.json({ + success: true, + message: 'API Key 更新成功' + }) + } catch (error) { + logger.error('更新用户API Key失败:', error) + res.status(500).json({ + success: false, + message: '更新 API Key 失败' + }) + } +}) + +/** + * 删除用户API Key + */ +router.delete('/user/api-keys/:keyId', authenticateUser, async (req, res) => { + try { + const { username } = req.user + const { keyId } = req.params + + // 验证用户只能删除自己的API Key + const apiKeyService = require('../services/apiKeyService') + const allApiKeys = await apiKeyService.getAllApiKeys() + const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username) + + if (!apiKey) { + return res.status(404).json({ + success: false, + message: 'API Key 不存在或无权限' + }) + } + + await apiKeyService.deleteApiKey(keyId) + + logger.info(`用户 ${username} 删除了 API Key: ${keyId}`) + + res.json({ + success: true, + message: 'API Key 删除成功' + }) + } catch (error) { + logger.error('删除用户API Key失败:', error) + res.status(500).json({ + success: false, + message: '删除 API Key 失败' + }) + } +}) + module.exports = router diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 46be6352..9986736b 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -32,7 +32,9 @@ class ApiKeyService { enableClientRestriction = false, allowedClients = [], dailyCostLimit = 0, - tags = [] + tags = [], + owner = null, + ownerType = null } = options // 生成简单的API Key (64字符十六进制) @@ -66,7 +68,9 @@ class ApiKeyService { createdAt: new Date().toISOString(), lastUsedAt: '', expiresAt: expiresAt || '', - createdBy: 'admin' // 可以根据需要扩展用户系统 + createdBy: 'admin', // 可以根据需要扩展用户系统 + owner: owner || '', + ownerType: ownerType || '' } // 保存API Key数据并建立哈希映射 @@ -99,7 +103,9 @@ class ApiKeyService { tags: JSON.parse(keyData.tags || '[]'), createdAt: keyData.createdAt, expiresAt: keyData.expiresAt, - createdBy: keyData.createdBy + createdBy: keyData.createdBy, + owner: keyData.owner, + ownerType: keyData.ownerType } } @@ -294,11 +300,21 @@ class ApiKeyService { // 📝 更新API Key async updateApiKey(keyId, updates) { try { + logger.debug(`🔧 Updating API key ${keyId} with:`, updates) + const keyData = await redis.getApiKey(keyId) if (!keyData || Object.keys(keyData).length === 0) { + logger.error(`❌ API key not found: ${keyId}`) throw new Error('API key not found') } + logger.debug(`📋 Current API key data:`, { + id: keyData.id, + name: keyData.name, + owner: keyData.owner, + ownerType: keyData.ownerType + }) + // 允许更新的字段 const allowedUpdates = [ 'name', @@ -344,7 +360,10 @@ class ApiKeyService { // 更新时不需要重新建立哈希映射,因为API Key本身没有变化 await redis.setApiKey(keyId, updatedData) - logger.success(`📝 Updated API key: ${keyId}`) + logger.success(`📝 Updated API key: ${keyId}`, { + updatedFields: Object.keys(updates), + newName: updatedData.name + }) return { success: true } } catch (error) { diff --git a/test-fixed-auto-link.js b/test-fixed-auto-link.js new file mode 100644 index 00000000..e13b8ac1 --- /dev/null +++ b/test-fixed-auto-link.js @@ -0,0 +1,54 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config/config'); +const fetch = require('node-fetch'); + +// 模拟创建一个包含displayName的JWT token +const userInfo = { + type: 'ad_user', + username: 'zhangji', + displayName: '张佶', + email: 'zhangji@weidian.com', + groups: ['CN=Weidian-IT组,OU=Weidian Groups,OU=微店,DC=corp,DC=weidian-inc,DC=com'], + loginTime: new Date().toISOString() +}; + +const token = jwt.sign(userInfo, config.security.jwtSecret, { + expiresIn: '8h' +}); + +console.log('测试修正后的自动关联功能'); +console.log('用户displayName: 张佶'); + +async function testFixedAutoLink() { + try { + console.log('\n=== 测试获取用户API Keys ==='); + + const response = await fetch('http://localhost:3000/admin/ldap/user/api-keys', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const result = await response.json(); + + console.log('\n结果:', JSON.stringify(result, null, 2)); + + if (result.success && result.apiKeys && result.apiKeys.length > 0) { + console.log('\n✅ 成功!找到了关联的API Key:'); + result.apiKeys.forEach(key => { + console.log(`- ID: ${key.id}`); + console.log(`- Name: ${key.name}`); + console.log(`- Key: ${key.key.substring(0, 10)}...${key.key.substring(key.key.length-10)}`); + console.log(`- Limit: ${key.limit}`); + console.log(`- Status: ${key.status}`); + }); + } else { + console.log('\n❌ 没有找到关联的API Key'); + } + + } catch (error) { + console.error('测试错误:', error.message); + } +} + +testFixedAutoLink(); \ No newline at end of file diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index 7d8069cf..f72b32be 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -33,12 +33,31 @@ >名称 -
名称不可修改
+最多100个字符
+ + ++ 最多500个字符(可选) +