From 8ea150a975a03b685b59b45ebcbe71a2807942eb Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Thu, 14 Aug 2025 10:48:28 +0800 Subject: [PATCH] feat: enhance user API key management and implement soft delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redirect users to API Keys tab after login instead of overview - Remove Token Limit and Daily Cost Limit from user API key details modal - Implement soft delete for API keys to preserve usage statistics - Add admin endpoint to view deleted API keys with metadata - Track deletion metadata (deletedBy, deletedAt, deletedByType) - Ensure deleted API keys cannot be restored - Include deleted key stats in user totals while excluding from active count 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/admin.js | 28 ++++++++++- src/routes/userRoutes.js | 2 +- src/services/apiKeyService.js | 46 +++++++++++++++---- src/services/userService.js | 7 ++- .../src/components/user/ViewApiKeyModal.vue | 16 ------- web/admin-spa/src/views/UserDashboardView.vue | 2 +- 6 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 997bef0b..9c668f6d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -796,7 +796,7 @@ router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params - await apiKeyService.deleteApiKey(keyId) + await apiKeyService.deleteApiKey(keyId, req.admin.username, 'admin') logger.success(`🗑️ Admin deleted API key: ${keyId}`) return res.json({ success: true, message: 'API key deleted successfully' }) @@ -806,6 +806,32 @@ router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => { } }) +// 📋 获取已删除的API Keys +router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => { + try { + const deletedApiKeys = await apiKeyService.getAllApiKeys(true) // Include deleted + const onlyDeleted = deletedApiKeys.filter((key) => key.isDeleted === 'true') + + // Add additional metadata for deleted keys + const enrichedKeys = onlyDeleted.map((key) => ({ + ...key, + isDeleted: key.isDeleted === 'true', + deletedAt: key.deletedAt, + deletedBy: key.deletedBy, + deletedByType: key.deletedByType, + canRestore: false // Deleted keys cannot be restored per requirement + })) + + logger.success(`📋 Admin retrieved ${enrichedKeys.length} deleted API keys`) + return res.json({ success: true, apiKeys: enrichedKeys, total: enrichedKeys.length }) + } catch (error) { + logger.error('❌ Failed to get deleted API keys:', error) + return res + .status(500) + .json({ error: 'Failed to retrieve deleted API keys', message: error.message }) + } +}) + // 👥 账户分组管理 // 创建账户分组 diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index a1478de4..d5bd6326 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -304,7 +304,7 @@ router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => { }) } - await apiKeyService.deleteApiKey(keyId) + await apiKeyService.deleteApiKey(keyId, req.user.username, 'user') // 更新用户API Key数量 const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 77751f22..d1b14b1d 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -195,11 +195,16 @@ class ApiKeyService { } // 📋 获取所有API Keys - async getAllApiKeys() { + async getAllApiKeys(includeDeleted = false) { try { - const apiKeys = await redis.getAllApiKeys() + let apiKeys = await redis.getAllApiKeys() const client = redis.getClientSafe() + // 默认过滤掉已删除的API Keys + if (!includeDeleted) { + apiKeys = apiKeys.filter((key) => key.isDeleted !== 'true') + } + // 为每个key添加使用统计和当前并发数 for (const key of apiKeys) { key.usage = await redis.getUsageStats(key.id) @@ -345,16 +350,32 @@ class ApiKeyService { } } - // 🗑️ 删除API Key - async deleteApiKey(keyId) { + // 🗑️ 软删除API Key (保留使用统计) + async deleteApiKey(keyId, deletedBy = 'system', deletedByType = 'system') { try { - const result = await redis.deleteApiKey(keyId) - - if (result === 0) { + const keyData = await redis.getApiKey(keyId) + if (!keyData || Object.keys(keyData).length === 0) { throw new Error('API key not found') } - logger.success(`🗑️ Deleted API key: ${keyId}`) + // 标记为已删除,保留所有数据和统计信息 + const updatedData = { + ...keyData, + isDeleted: 'true', + deletedAt: new Date().toISOString(), + deletedBy, + deletedByType, // 'user', 'admin', 'system' + isActive: 'false' // 同时禁用 + } + + await redis.setApiKey(keyId, updatedData) + + // 从哈希映射中移除(这样就不能再使用这个key进行API调用) + if (keyData.apiKey) { + await redis.deleteApiKeyHash(keyData.apiKey) + } + + logger.success(`🗑️ Soft deleted API key: ${keyId} by ${deletedBy} (${deletedByType})`) return { success: true } } catch (error) { @@ -488,10 +509,15 @@ class ApiKeyService { } // 👤 获取用户的API Keys - async getUserApiKeys(userId) { + async getUserApiKeys(userId, includeDeleted = false) { try { const allKeys = await redis.getAllApiKeys() - const userKeys = allKeys.filter((key) => key.userId === userId) + let userKeys = allKeys.filter((key) => key.userId === userId) + + // 默认过滤掉已删除的API Keys + if (!includeDeleted) { + userKeys = userKeys.filter((key) => key.isDeleted !== 'true') + } // Populate usage stats for each user's API key (same as getAllApiKeys does) const userKeysWithUsage = [] diff --git a/src/services/userService.js b/src/services/userService.js index e03761c7..2ba25dd4 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -140,7 +140,7 @@ class UserService { try { // Use the existing apiKeyService method which already includes usage stats const apiKeyService = require('./apiKeyService') - const userApiKeys = await apiKeyService.getUserApiKeys(userId) + const userApiKeys = await apiKeyService.getUserApiKeys(userId, true) // Include deleted keys for stats const totalUsage = { requests: 0, @@ -162,9 +162,12 @@ class UserService { `📊 Calculated user ${userId} usage: ${totalUsage.requests} requests, ${totalUsage.inputTokens} input tokens, $${totalUsage.totalCost.toFixed(4)} total cost from ${userApiKeys.length} API keys` ) + // Count only non-deleted API keys for the user's active count + const activeApiKeyCount = userApiKeys.filter((key) => key.isDeleted !== 'true').length + return { totalUsage, - apiKeyCount: userApiKeys.length + apiKeyCount: activeApiKeyCount } } catch (error) { logger.error('❌ Error calculating user usage stats:', error) diff --git a/web/admin-spa/src/components/user/ViewApiKeyModal.vue b/web/admin-spa/src/components/user/ViewApiKeyModal.vue index bcd316c7..99ea1738 100644 --- a/web/admin-spa/src/components/user/ViewApiKeyModal.vue +++ b/web/admin-spa/src/components/user/ViewApiKeyModal.vue @@ -129,22 +129,6 @@ - -
-
- -

- {{ apiKey.tokenLimit ? apiKey.tokenLimit.toLocaleString() : 'Unlimited' }} -

-
-
- -

- {{ apiKey.dailyCostLimit ? `$${apiKey.dailyCostLimit.toFixed(2)}` : 'Unlimited' }} -

-
-
-
diff --git a/web/admin-spa/src/views/UserDashboardView.vue b/web/admin-spa/src/views/UserDashboardView.vue index 17a5229f..2b0b75d3 100644 --- a/web/admin-spa/src/views/UserDashboardView.vue +++ b/web/admin-spa/src/views/UserDashboardView.vue @@ -282,7 +282,7 @@ import UserUsageStats from '@/components/user/UserUsageStats.vue' const router = useRouter() const userStore = useUserStore() -const activeTab = ref('overview') +const activeTab = ref('api-keys') const userProfile = ref(null) const formatNumber = (num) => {