mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(api-key): show last used IP
This commit is contained in:
@@ -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 密钥',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user