mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +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 {
|
try {
|
||||||
const { keyId } = req.params
|
const { keyId } = req.params
|
||||||
|
|
||||||
await apiKeyService.deleteApiKey(keyId)
|
await apiKeyService.deleteApiKey(keyId, req.admin.username, 'admin')
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted API key: ${keyId}`)
|
logger.success(`🗑️ Admin deleted API key: ${keyId}`)
|
||||||
return res.json({ success: true, message: 'API key deleted successfully' })
|
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数量
|
// 更新用户API Key数量
|
||||||
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
||||||
|
|||||||
@@ -195,11 +195,16 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 📋 获取所有API Keys
|
// 📋 获取所有API Keys
|
||||||
async getAllApiKeys() {
|
async getAllApiKeys(includeDeleted = false) {
|
||||||
try {
|
try {
|
||||||
const apiKeys = await redis.getAllApiKeys()
|
let apiKeys = await redis.getAllApiKeys()
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
|
|
||||||
|
// 默认过滤掉已删除的API Keys
|
||||||
|
if (!includeDeleted) {
|
||||||
|
apiKeys = apiKeys.filter((key) => key.isDeleted !== 'true')
|
||||||
|
}
|
||||||
|
|
||||||
// 为每个key添加使用统计和当前并发数
|
// 为每个key添加使用统计和当前并发数
|
||||||
for (const key of apiKeys) {
|
for (const key of apiKeys) {
|
||||||
key.usage = await redis.getUsageStats(key.id)
|
key.usage = await redis.getUsageStats(key.id)
|
||||||
@@ -345,16 +350,32 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ 删除API Key
|
// 🗑️ 软删除API Key (保留使用统计)
|
||||||
async deleteApiKey(keyId) {
|
async deleteApiKey(keyId, deletedBy = 'system', deletedByType = 'system') {
|
||||||
try {
|
try {
|
||||||
const result = await redis.deleteApiKey(keyId)
|
const keyData = await redis.getApiKey(keyId)
|
||||||
|
if (!keyData || Object.keys(keyData).length === 0) {
|
||||||
if (result === 0) {
|
|
||||||
throw new Error('API key not found')
|
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 }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -488,10 +509,15 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 👤 获取用户的API Keys
|
// 👤 获取用户的API Keys
|
||||||
async getUserApiKeys(userId) {
|
async getUserApiKeys(userId, includeDeleted = false) {
|
||||||
try {
|
try {
|
||||||
const allKeys = await redis.getAllApiKeys()
|
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)
|
// Populate usage stats for each user's API key (same as getAllApiKeys does)
|
||||||
const userKeysWithUsage = []
|
const userKeysWithUsage = []
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class UserService {
|
|||||||
try {
|
try {
|
||||||
// Use the existing apiKeyService method which already includes usage stats
|
// Use the existing apiKeyService method which already includes usage stats
|
||||||
const apiKeyService = require('./apiKeyService')
|
const apiKeyService = require('./apiKeyService')
|
||||||
const userApiKeys = await apiKeyService.getUserApiKeys(userId)
|
const userApiKeys = await apiKeyService.getUserApiKeys(userId, true) // Include deleted keys for stats
|
||||||
|
|
||||||
const totalUsage = {
|
const totalUsage = {
|
||||||
requests: 0,
|
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`
|
`📊 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 {
|
return {
|
||||||
totalUsage,
|
totalUsage,
|
||||||
apiKeyCount: userApiKeys.length
|
apiKeyCount: activeApiKeyCount
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Error calculating user usage stats:', error)
|
logger.error('❌ Error calculating user usage stats:', error)
|
||||||
|
|||||||
@@ -129,22 +129,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Limits -->
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Token Limit</label>
|
|
||||||
<p class="mt-1 text-sm text-gray-900">
|
|
||||||
{{ apiKey.tokenLimit ? apiKey.tokenLimit.toLocaleString() : 'Unlimited' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Daily Cost Limit</label>
|
|
||||||
<p class="mt-1 text-sm text-gray-900">
|
|
||||||
{{ apiKey.dailyCostLimit ? `$${apiKey.dailyCostLimit.toFixed(2)}` : 'Unlimited' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Usage Stats -->
|
<!-- Usage Stats -->
|
||||||
<div v-if="apiKey.usage" class="border-t border-gray-200 pt-4">
|
<div v-if="apiKey.usage" class="border-t border-gray-200 pt-4">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700">Usage Statistics</label>
|
<label class="mb-2 block text-sm font-medium text-gray-700">Usage Statistics</label>
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ import UserUsageStats from '@/components/user/UserUsageStats.vue'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const activeTab = ref('overview')
|
const activeTab = ref('api-keys')
|
||||||
const userProfile = ref(null)
|
const userProfile = ref(null)
|
||||||
|
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user