mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: enhance user API keys view and fix admin cost display
- Add deleted API keys display to user's My API Keys view - Show deleted status with gray indicator and "Deleted" badge - Display deletion date and hide delete button for deleted keys - Fix cost calculation in admin deleted API keys tab - Add getCostStats call to properly populate cost data - Support includeDeleted parameter in user API keys endpoint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,8 @@ router.get('/profile', authenticateUser, async (req, res) => {
|
|||||||
// 🔑 获取用户的API Keys
|
// 🔑 获取用户的API Keys
|
||||||
router.get('/api-keys', authenticateUser, async (req, res) => {
|
router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
const { includeDeleted = 'false' } = req.query
|
||||||
|
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id, includeDeleted === 'true')
|
||||||
|
|
||||||
// 移除敏感信息并格式化usage数据
|
// 移除敏感信息并格式化usage数据
|
||||||
const safeApiKeys = apiKeys.map((key) => {
|
const safeApiKeys = apiKeys.map((key) => {
|
||||||
|
|||||||
@@ -208,6 +208,14 @@ class ApiKeyService {
|
|||||||
// 为每个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)
|
||||||
|
const costStats = await redis.getCostStats(key.id)
|
||||||
|
// Add cost information to usage object for frontend compatibility
|
||||||
|
if (key.usage && costStats) {
|
||||||
|
key.usage.total = key.usage.total || {}
|
||||||
|
key.usage.total.cost = costStats.total
|
||||||
|
key.usage.totalCost = costStats.total
|
||||||
|
}
|
||||||
|
key.totalCost = costStats ? costStats.total : 0
|
||||||
key.tokenLimit = parseInt(key.tokenLimit)
|
key.tokenLimit = parseInt(key.tokenLimit)
|
||||||
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0)
|
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0)
|
||||||
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
|
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
|
||||||
|
|||||||
@@ -83,14 +83,27 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
:class="['h-2 w-2 rounded-full', apiKey.isActive ? 'bg-green-400' : 'bg-red-400']"
|
:class="[
|
||||||
|
'h-2 w-2 rounded-full',
|
||||||
|
apiKey.isDeleted === 'true' || apiKey.deletedAt
|
||||||
|
? 'bg-gray-400'
|
||||||
|
: apiKey.isActive
|
||||||
|
? 'bg-green-400'
|
||||||
|
: 'bg-red-400'
|
||||||
|
]"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p>
|
<p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p>
|
||||||
<span
|
<span
|
||||||
v-if="!apiKey.isActive"
|
v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
|
||||||
|
class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800"
|
||||||
|
>
|
||||||
|
Deleted
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="!apiKey.isActive"
|
||||||
class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
|
class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
|
||||||
>
|
>
|
||||||
Disabled
|
Disabled
|
||||||
@@ -100,11 +113,17 @@
|
|||||||
<p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p>
|
<p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p>
|
||||||
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
|
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
|
||||||
<span>Created: {{ formatDate(apiKey.createdAt) }}</span>
|
<span>Created: {{ formatDate(apiKey.createdAt) }}</span>
|
||||||
<span v-if="apiKey.lastUsedAt"
|
<span v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
|
||||||
|
>Deleted: {{ formatDate(apiKey.deletedAt) }}</span
|
||||||
|
>
|
||||||
|
<span v-else-if="apiKey.lastUsedAt"
|
||||||
>Last used: {{ formatDate(apiKey.lastUsedAt) }}</span
|
>Last used: {{ formatDate(apiKey.lastUsedAt) }}</span
|
||||||
>
|
>
|
||||||
<span v-else>Never used</span>
|
<span v-else>Never used</span>
|
||||||
<span v-if="apiKey.expiresAt">Expires: {{ formatDate(apiKey.expiresAt) }}</span>
|
<span
|
||||||
|
v-if="apiKey.expiresAt && !(apiKey.isDeleted === 'true' || apiKey.deletedAt)"
|
||||||
|
>Expires: {{ formatDate(apiKey.expiresAt) }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +159,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="!(apiKey.isDeleted === 'true' || apiKey.deletedAt)"
|
||||||
class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600"
|
class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600"
|
||||||
title="Delete API Key"
|
title="Delete API Key"
|
||||||
@click="deleteApiKey(apiKey)"
|
@click="deleteApiKey(apiKey)"
|
||||||
@@ -264,7 +284,7 @@ const formatDate = (dateString) => {
|
|||||||
const loadApiKeys = async () => {
|
const loadApiKeys = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
apiKeys.value = await userStore.getUserApiKeys()
|
apiKeys.value = await userStore.getUserApiKeys(true) // Include deleted keys
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load API keys:', error)
|
console.error('Failed to load API keys:', error)
|
||||||
showToast('Failed to load API keys', 'error')
|
showToast('Failed to load API keys', 'error')
|
||||||
|
|||||||
@@ -118,9 +118,13 @@ export const useUserStore = defineStore('user', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 🔑 获取用户API Keys
|
// 🔑 获取用户API Keys
|
||||||
async getUserApiKeys() {
|
async getUserApiKeys(includeDeleted = false) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_BASE}/api-keys`)
|
const params = {}
|
||||||
|
if (includeDeleted) {
|
||||||
|
params.includeDeleted = 'true'
|
||||||
|
}
|
||||||
|
const response = await axios.get(`${API_BASE}/api-keys`, { params })
|
||||||
return response.data.success ? response.data.apiKeys : []
|
return response.data.success ? response.data.apiKeys : []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch API keys:', error)
|
console.error('Failed to fetch API keys:', error)
|
||||||
|
|||||||
Reference in New Issue
Block a user