feat(api-key): show last used IP

This commit is contained in:
wucm667
2026-07-08 15:26:54 +08:00
parent 6f43986c37
commit e0d149d511
12 changed files with 245 additions and 6 deletions
@@ -124,6 +124,7 @@ export default {
total: 'Last 30d',
quota: 'Quota',
lastUsedAt: 'Last Used',
lastUsedIP: 'Last Used IP',
useKey: 'Use Key',
useKeyModal: {
title: 'Use API Key',
@@ -124,6 +124,7 @@ export default {
total: '近30天',
quota: '额度',
lastUsedAt: '上次使用时间',
lastUsedIP: '最近使用 IP',
useKey: '使用密钥',
useKeyModal: {
title: '使用 API 密钥',
+1
View File
@@ -585,6 +585,7 @@ export interface ApiKey {
ip_whitelist: string[]
ip_blacklist: string[]
last_used_at: string | null
last_used_ip: string | null
quota: number // Quota limit in USD (0 = unlimited)
quota_used: number // Used quota amount in USD
expires_at: string | null // Expiration time (null = never expires)
+27 -3
View File
@@ -354,6 +354,13 @@
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
</template>
<template #cell-last_used_ip="{ value }">
<span v-if="value" class="text-sm text-gray-500 dark:text-dark-400">
{{ value }}
</span>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
</template>
<template #cell-created_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatDateTime(value) }}</span>
</template>
@@ -1174,15 +1181,19 @@ const allColumns = computed<Column[]>(() => [
{ key: 'expires_at', label: t('keys.expiresAt'), sortable: true },
{ key: 'status', label: t('common.status'), sortable: true },
{ key: 'last_used_at', label: t('keys.lastUsedAt'), sortable: true },
{ key: 'last_used_ip', label: t('keys.lastUsedIP'), sortable: false },
{ key: 'created_at', label: t('keys.created'), sortable: true },
{ key: 'actions', label: t('common.actions'), sortable: false }
])
const ALWAYS_VISIBLE_COLUMNS = new Set(['name', 'actions'])
const DEFAULT_HIDDEN_COLUMNS = ['rate_limit', 'last_used_at']
const DEFAULT_HIDDEN_COLUMNS = ['rate_limit', 'last_used_at', 'last_used_ip']
const HIDDEN_COLUMNS_KEY = 'api-key-hidden-columns'
const COLUMN_SETTINGS_VERSION_KEY = 'api-key-column-settings-version'
const COLUMN_SETTINGS_VERSION = 1
const COLUMN_SETTINGS_VERSION = 2
const VERSION_NEW_HIDDEN_COLUMNS: Record<number, string[]> = {
2: ['last_used_ip']
}
const toggleableColumns = computed(() =>
allColumns.value.filter((col) => !ALWAYS_VISIBLE_COLUMNS.has(col.key))
@@ -1213,10 +1224,23 @@ const loadSavedColumns = () => {
!ALWAYS_VISIBLE_COLUMNS.has(key)
)
.forEach((key) => hiddenColumns.add(key))
const storedVersion = Number(localStorage.getItem(COLUMN_SETTINGS_VERSION_KEY) ?? '1')
if (storedVersion < COLUMN_SETTINGS_VERSION) {
for (let v = storedVersion + 1; v <= COLUMN_SETTINGS_VERSION; v++) {
for (const key of VERSION_NEW_HIDDEN_COLUMNS[v] ?? []) {
if (validColumnKeys.has(key) && !ALWAYS_VISIBLE_COLUMNS.has(key)) {
hiddenColumns.add(key)
}
}
}
saveColumnsToStorage()
} else {
localStorage.setItem(COLUMN_SETTINGS_VERSION_KEY, String(COLUMN_SETTINGS_VERSION))
}
} else {
DEFAULT_HIDDEN_COLUMNS.forEach((key) => hiddenColumns.add(key))
localStorage.setItem(COLUMN_SETTINGS_VERSION_KEY, String(COLUMN_SETTINGS_VERSION))
}
localStorage.setItem(COLUMN_SETTINGS_VERSION_KEY, String(COLUMN_SETTINGS_VERSION))
} catch (error) {
console.error('Failed to load API key table columns:', error)
DEFAULT_HIDDEN_COLUMNS.forEach((key) => hiddenColumns.add(key))
@@ -44,6 +44,7 @@ const messages: Record<string, string> = {
'keys.group': 'Group',
'keys.currentConcurrency': 'Current Concurrency',
'keys.lastUsedAt': 'Last Used',
'keys.lastUsedIP': 'Last Used IP',
'keys.rateLimitColumn': 'Rate Limit',
'keys.searchPlaceholder': 'Search name or key...',
'keys.status.active': 'Active',
@@ -113,6 +114,7 @@ const createApiKey = (): ApiKey => ({
ip_whitelist: [],
ip_blacklist: [],
last_used_at: null,
last_used_ip: null,
quota: 0,
quota_used: 0,
expires_at: null,
@@ -159,6 +161,12 @@ const DataTableStub = {
<div data-test="current-concurrency">
<slot name="cell-current_concurrency" :value="row.current_concurrency" :row="row" />
</div>
<div
v-if="columns.some((col) => col.key === 'last_used_ip')"
data-test="last-used-ip"
>
<slot name="cell-last_used_ip" :value="row.last_used_ip" :row="row" />
</div>
</div>
<slot name="empty" />
</div>
@@ -265,6 +273,7 @@ describe('user KeysView column settings', () => {
])
expect(visibleColumnKeys(wrapper)).not.toContain('rate_limit')
expect(visibleColumnKeys(wrapper)).not.toContain('last_used_at')
expect(visibleColumnKeys(wrapper)).not.toContain('last_used_ip')
})
it('shows a hidden column when toggled and persists the preference', async () => {
@@ -275,8 +284,28 @@ describe('user KeysView column settings', () => {
await nextTick()
expect(visibleColumnKeys(wrapper)).toContain('rate_limit')
expect(localStorage.getItem('api-key-hidden-columns')).toBe(JSON.stringify(['last_used_at']))
expect(localStorage.getItem('api-key-column-settings-version')).toBe('1')
expect(localStorage.getItem('api-key-hidden-columns')).toBe(
JSON.stringify(['last_used_at', 'last_used_ip'])
)
expect(localStorage.getItem('api-key-column-settings-version')).toBe('2')
})
it('shows the last used IP column when toggled', async () => {
listKeys.mockResolvedValueOnce({
items: [{ ...createApiKey(), last_used_ip: '203.0.113.10' }],
total: 1,
page: 1,
page_size: 20,
pages: 1,
})
const wrapper = await mountView()
await wrapper.get('button[title="Column Settings"]').trigger('click')
await getButtonByText(wrapper, 'Last Used IP').trigger('click')
await nextTick()
expect(visibleColumnKeys(wrapper)).toContain('last_used_ip')
expect(wrapper.get('[data-test="last-used-ip"]').text()).toBe('203.0.113.10')
})
it('restores column preferences from localStorage on mount', async () => {
@@ -296,6 +325,10 @@ describe('user KeysView column settings', () => {
'last_used_at',
'actions',
])
expect(localStorage.getItem('api-key-hidden-columns')).toBe(
JSON.stringify(['group', 'created_at', 'last_used_ip'])
)
expect(localStorage.getItem('api-key-column-settings-version')).toBe('2')
})
it('does not include always-visible columns in the toggleable menu', async () => {
@@ -308,6 +341,7 @@ describe('user KeysView column settings', () => {
expect(columnMenuText).toContain('API Key')
expect(columnMenuText).toContain('Current Concurrency')
expect(columnMenuText).toContain('Rate Limit')
expect(columnMenuText).toContain('Last Used IP')
expect(columnMenuText).not.toContain('Name')
expect(columnMenuText).not.toContain('Actions')
})