mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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:
@@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
// 👥 账户分组管理
|
||||
|
||||
// 创建账户分组
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user