From 82f545c3b0ab59be700de583b165eff4b78a06c8 Mon Sep 17 00:00:00 2001
From: iRubbish
Date: Tue, 26 Aug 2025 13:42:02 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=BD=93=E5=89=8DAPI=20Key?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E7=9A=84=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 统一用户创建和admin创建API Key的逻辑
- 修复admin更新用户创建的API Key功能
- 用户创建API Key名称改为displayName
- 默认无限制配置
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
src/routes/admin.js | 36 +-
src/routes/ldapRoutes.js | 203 ++++++++----
src/services/apiKeyService.js | 27 +-
test-fixed-auto-link.js | 54 +++
.../components/apikeys/EditApiKeyModal.vue | 31 +-
.../src/components/user/UserApiKeysView.vue | 312 +++++++++++++-----
web/admin-spa/src/views/UserDashboardView.vue | 15 +-
7 files changed, 507 insertions(+), 171 deletions(-)
create mode 100644 test-fixed-auto-link.js
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个字符(可选)
+
@@ -632,6 +651,7 @@ const unselectedTags = computed(() => {
// 表单数据
const form = reactive({
name: '',
+ description: '',
tokenLimit: '',
rateLimitWindow: '',
rateLimitRequests: '',
@@ -707,6 +727,8 @@ const updateApiKey = async () => {
try {
// 准备提交的数据
const data = {
+ name: form.name,
+ description: form.description,
tokenLimit:
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
rateLimitWindow:
@@ -893,6 +915,7 @@ onMounted(async () => {
}
form.name = props.apiKey.name
+ form.description = props.apiKey.description || ''
form.tokenLimit = props.apiKey.tokenLimit || ''
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
diff --git a/web/admin-spa/src/components/user/UserApiKeysView.vue b/web/admin-spa/src/components/user/UserApiKeysView.vue
index 7f472ab6..4e029731 100644
--- a/web/admin-spa/src/components/user/UserApiKeysView.vue
+++ b/web/admin-spa/src/components/user/UserApiKeysView.vue
@@ -26,18 +26,15 @@
API Key 将用于访问 Claude Relay Service