feat: enhance user API key management and implement soft delete

- 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 <noreply@anthropic.com>
This commit is contained in:
Feng Yue
2025-08-14 10:48:28 +08:00
parent c413fddec0
commit 8ea150a975
6 changed files with 70 additions and 31 deletions

View File

@@ -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 })
}
})
// 👥 账户分组管理
// 创建账户分组

View File

@@ -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)

View File

@@ -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 = []

View File

@@ -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)