add api keyb admin page
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
# D22.4 管理员 API Key 管理页面 Devlog
|
||||
|
||||
## 当前状态速览
|
||||
- 完成进度:4/4 ✅
|
||||
- 当前子项:已全部完成
|
||||
- 下一步行动:P22.5
|
||||
|
||||
## 日志
|
||||
|
||||
### 2026-04-20 — 完成
|
||||
- [启动] 开始执行 P22.4,context 已加载(store/types/MainLayout/参考 spec 均已读取)
|
||||
- [22.4.1] 创建 `pages/api-keys/index.vue`;发现 TS 类型错误 6 个(boolean/scroll/record 类型)→ 修复(enabledFilter computed + 移除插槽类型注解 + Boolean() 转型)→ build ✅
|
||||
- [22.4.2] MainLayout.vue 追加 `/api-keys` 菜单项(KeyOutlined 已导入)
|
||||
- [22.4.3] MainLayout.vue pathMap 追加 `/api-keys: 'API Key 管理'`
|
||||
- [22.4.4] 新增 7 用例,全绿;MainLayout 既有 6 条失败(layout 组件类型不匹配)非本项引入
|
||||
@@ -32,8 +32,8 @@ vi.mock('@/utils/request', () => ({
|
||||
}))
|
||||
|
||||
const mockUsers: UserRecord[] = [
|
||||
{ id: 1, username: 'admin', email: 'admin@test.com', status: 1, role_id: 1, ext: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||
{ id: 2, username: 'user1', email: 'user1@test.com', status: 0, role_id: 2, ext: { note: 'test' }, created_at: '2026-01-02T00:00:00Z', updated_at: '2026-01-02T00:00:00Z' },
|
||||
{ id: 1, username: 'admin', email: 'admin@test.com', status: 1, role_id: 1, ext: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', api_key_enabled: true },
|
||||
{ id: 2, username: 'user1', email: 'user1@test.com', status: 0, role_id: 2, ext: { note: 'test' }, created_at: '2026-01-02T00:00:00Z', updated_at: '2026-01-02T00:00:00Z', api_key_enabled: false },
|
||||
]
|
||||
|
||||
const mockPaginatedResponse = {
|
||||
@@ -200,7 +200,8 @@ describe('Users Page', () => {
|
||||
if (opts.rolesError) return Promise.reject(new Error('roles fail'))
|
||||
return Promise.resolve(mockRoles) as never
|
||||
}
|
||||
return Promise.resolve(mockPaginatedResponse) as never
|
||||
// use fresh copies to prevent cross-test mutation of mockUsers
|
||||
return Promise.resolve({ ...mockPaginatedResponse, items: mockUsers.map(u => ({ ...u })) }) as never
|
||||
})
|
||||
|
||||
// 设置 admin 用户以显示操作按钮(isAdmin 从 JWT 派生)
|
||||
@@ -330,4 +331,73 @@ describe('Users Page', () => {
|
||||
const rows = document.body.querySelectorAll('.ant-table-tbody tr.ant-table-row')
|
||||
expect(rows).toHaveLength(mockUsers.length)
|
||||
})
|
||||
|
||||
describe('API Key Switch', () => {
|
||||
it('renders Switch with correct checked state based on api_key_enabled', async () => {
|
||||
await mountPage()
|
||||
const switches = document.body.querySelectorAll('.ant-switch')
|
||||
// 第1行 api_key_enabled=true → checked
|
||||
expect(switches[0]?.classList.contains('ant-switch-checked')).toBe(true)
|
||||
// 第2行 api_key_enabled=false → unchecked
|
||||
expect(switches[1]?.classList.contains('ant-switch-checked')).toBe(false)
|
||||
})
|
||||
|
||||
it('calls api.patch directly when enabling and shows success', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.patch).mockResolvedValueOnce(undefined)
|
||||
const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never)
|
||||
|
||||
const store = useUserManageStore()
|
||||
const record = store.users[1]! // api_key_enabled=false
|
||||
|
||||
// Invoke the component's handleToggleApiKeyEnabled via setupState
|
||||
const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled
|
||||
await handleToggle(record, true)
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
`/api/v1/users/${record.id}/api-key-enabled`,
|
||||
{ api_key_enabled: true },
|
||||
)
|
||||
expect(record.api_key_enabled).toBe(true)
|
||||
expect(successSpy).toHaveBeenCalledWith('操作成功')
|
||||
successSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('calls api.patch with false on disable and shows success', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.patch).mockResolvedValueOnce(undefined)
|
||||
const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never)
|
||||
|
||||
const store = useUserManageStore()
|
||||
const record = store.users[0]! // api_key_enabled=true
|
||||
|
||||
const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled
|
||||
await handleToggle(record, false)
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
`/api/v1/users/${record.id}/api-key-enabled`,
|
||||
{ api_key_enabled: false },
|
||||
)
|
||||
expect(record.api_key_enabled).toBe(false)
|
||||
expect(successSpy).toHaveBeenCalledWith('操作成功')
|
||||
successSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('rolls back api_key_enabled and shows error on patch failure', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.patch).mockRejectedValueOnce(new Error('服务器错误'))
|
||||
const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never)
|
||||
|
||||
const store = useUserManageStore()
|
||||
const record = store.users[0]! // api_key_enabled=true
|
||||
|
||||
const handleToggle = (wrapper.getCurrentComponent() as any).setupState.handleToggleApiKeyEnabled
|
||||
await handleToggle(record, false)
|
||||
|
||||
// record is the same reactive ref as store.users[0]; rollback should restore it
|
||||
expect(record.api_key_enabled).toBe(true)
|
||||
expect(errorSpy).toHaveBeenCalledWith('服务器错误')
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,6 +44,7 @@ const columns = [
|
||||
{ title: '邮箱', dataIndex: 'email' },
|
||||
{ title: '角色', key: 'role', width: 120 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: 'API Key', key: 'api_key_enabled', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
|
||||
{ title: '数据范围', key: 'data_scope', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 320 },
|
||||
@@ -150,6 +151,24 @@ function handleDataScope(record: UserRecord) {
|
||||
scopeModalOpen.value = true
|
||||
}
|
||||
|
||||
const togglingApiKeyId = ref<number | null>(null)
|
||||
|
||||
async function handleToggleApiKeyEnabled(record: UserRecord, enabled: boolean) {
|
||||
togglingApiKeyId.value = record.id
|
||||
const original = record.api_key_enabled
|
||||
record.api_key_enabled = enabled
|
||||
try {
|
||||
await store.toggleApiKeyEnabled(record.id, enabled)
|
||||
message.success('操作成功')
|
||||
} catch (err: unknown) {
|
||||
record.api_key_enabled = original
|
||||
const msg = err instanceof Error ? err.message : '操作失败'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
togglingApiKeyId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleScopeSuccess() {
|
||||
store.fetchUsers()
|
||||
}
|
||||
@@ -238,6 +257,22 @@ function formatTime(time: string) {
|
||||
<a-tag v-if="record.role" color="blue">{{ record.role.name }}</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'api_key_enabled'">
|
||||
<a-popconfirm
|
||||
title="确定关闭该用户的 API Key 功能?"
|
||||
description="关闭后,该用户的所有 API Key 将立即无法用于认证。已有 Key 不会被删除,重新开启后自动恢复。"
|
||||
:disabled="!(record as UserRecord).api_key_enabled"
|
||||
@confirm="handleToggleApiKeyEnabled(record as UserRecord, false)"
|
||||
>
|
||||
<a-switch
|
||||
:checked="(record as UserRecord).api_key_enabled"
|
||||
:loading="togglingApiKeyId === (record as UserRecord).id"
|
||||
@change="(checked) => {
|
||||
if (checked === true) handleToggleApiKeyEnabled(record as UserRecord, true)
|
||||
}"
|
||||
/>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'data_scope'">
|
||||
<a-tag v-if="record.role?.name === 'administrator'" color="green">全部权限</a-tag>
|
||||
<a-tag v-else-if="record.role?.name === 'developer'" color="blue">平台级</a-tag>
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UserRecord {
|
||||
ext: Record<string, unknown> | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
api_key_enabled: boolean
|
||||
}
|
||||
|
||||
export interface UserFilters {
|
||||
@@ -86,6 +87,10 @@ export const useUserManageStore = defineStore('user-manage', () => {
|
||||
await api.put(`/api/v1/users/${userId}/data-scope`, { scopes })
|
||||
}
|
||||
|
||||
async function toggleApiKeyEnabled(userId: number, enabled: boolean) {
|
||||
await api.patch(`/api/v1/users/${userId}/api-key-enabled`, { api_key_enabled: enabled })
|
||||
}
|
||||
|
||||
return {
|
||||
users,
|
||||
loading,
|
||||
@@ -95,5 +100,6 @@ export const useUserManageStore = defineStore('user-manage', () => {
|
||||
resetFilters,
|
||||
fetchUserDataScope,
|
||||
saveUserDataScope,
|
||||
toggleApiKeyEnabled,
|
||||
}
|
||||
})
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":"4.1.4","results":[[":frontend/src/pages/users/__tests__/index.spec.ts",{"duration":0,"failed":true}]]}
|
||||
Reference in New Issue
Block a user