From 7898beef5a9d3bd473197a98c838a40ba9e73b22 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Mon, 20 Apr 2026 10:40:48 +0800 Subject: [PATCH] update api key manage --- .../Api/V1/AdminApiKeyController.php | 48 +++++++++++++++---- .../constants/__tests__/permissions.spec.ts | 5 +- frontend/src/constants/permissions.ts | 1 + .../pages/api-keys/__tests__/index.spec.ts | 6 ++- frontend/src/pages/api-keys/index.vue | 36 +++++++++----- .../stores/__tests__/admin-api-key.spec.ts | 42 +++++++++++++++- frontend/src/stores/admin-api-key.ts | 9 ++-- frontend/src/types/api.ts | 5 +- 8 files changed, 121 insertions(+), 31 deletions(-) diff --git a/backend/app/Controller/Api/V1/AdminApiKeyController.php b/backend/app/Controller/Api/V1/AdminApiKeyController.php index 0969a79..dab9f6b 100644 --- a/backend/app/Controller/Api/V1/AdminApiKeyController.php +++ b/backend/app/Controller/Api/V1/AdminApiKeyController.php @@ -21,18 +21,19 @@ class AdminApiKeyController extends AbstractController /** * 管理员列出所有 API Keys * - * 支持按 user_id、enabled 筛选,关联用户信息 + * 支持按 username、email、enabled 筛选,关联用户信息(含 email) */ #[OA\Get( path: '/admin/api-keys', summary: '管理员列出所有 API Keys', - description: '分页列出所有用户的 API Keys,支持按 user_id、enabled 筛选,关联用户基本信息', + description: '分页列出所有用户的 API Keys,支持按 username、email、enabled 筛选,关联用户基本信息', security: [['bearerAuth' => []]], tags: ['Admin API Keys'], parameters: [ new OA\Parameter(name: 'page', in: 'query', required: false, description: '页码,默认 1', schema: new OA\Schema(type: 'integer', default: 1)), new OA\Parameter(name: 'per_page', in: 'query', required: false, description: '每页条数,默认 15,最大 100', schema: new OA\Schema(type: 'integer', default: 15)), - new OA\Parameter(name: 'user_id', in: 'query', required: false, description: '按用户 ID 筛选', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'username', in: 'query', required: false, description: '按用户名模糊搜索', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'email', in: 'query', required: false, description: '按邮箱模糊搜索', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'enabled', in: 'query', required: false, description: '按启用状态筛选(0/1)', schema: new OA\Schema(type: 'integer', enum: [0, 1])), ], responses: [ @@ -55,6 +56,7 @@ class AdminApiKeyController extends AbstractController new OA\Property(property: 'user', properties: [ new OA\Property(property: 'id', type: 'integer'), new OA\Property(property: 'username', type: 'string'), + new OA\Property(property: 'email', type: 'string'), new OA\Property(property: 'api_key_enabled', type: 'boolean'), ], type: 'object'), ])), @@ -71,17 +73,29 @@ class AdminApiKeyController extends AbstractController #[RequestMapping(path: "", methods: "GET")] #[Middleware(AuthMiddleware::class)] #[Middleware(PermissionMiddleware::class)] - public function index(): array + public function index(): ResponseInterface|array { + if ($forbidden = $this->requireAdmin()) return $forbidden; + $page = (int) $this->request->input('page', 1); $per_page = min((int) $this->request->input('per_page', 15), 100); - $query = ApiKey::query()->with('user:id,username,api_key_enabled'); + $query = ApiKey::query()->with('user:id,username,email,api_key_enabled'); - // 按用户 ID 筛选 - $user_id = $this->request->input('user_id'); - if ($user_id !== null && $user_id !== '') { - $query->where('user_id', (int) $user_id); + // 按用户名模糊搜索 + $username = $this->request->input('username'); + if ($username !== null && $username !== '') { + $query->whereHas('user', function ($q) use ($username) { + $q->where('username', 'like', '%' . $username . '%'); + }); + } + + // 按邮箱模糊搜索 + $email = $this->request->input('email'); + if ($email !== null && $email !== '') { + $query->whereHas('user', function ($q) use ($email) { + $q->where('email', 'like', '%' . $email . '%'); + }); } // 按启用状态筛选 @@ -155,6 +169,8 @@ class AdminApiKeyController extends AbstractController #[Middleware(PermissionMiddleware::class)] public function toggle(int $id): ResponseInterface|array { + if ($forbidden = $this->requireAdmin()) return $forbidden; + $api_key = ApiKey::query()->find($id); if (!$api_key) { @@ -213,6 +229,8 @@ class AdminApiKeyController extends AbstractController #[Middleware(PermissionMiddleware::class)] public function destroy(int $id): ResponseInterface|array { + if ($forbidden = $this->requireAdmin()) return $forbidden; + $api_key = ApiKey::query()->find($id); if (!$api_key) { @@ -229,4 +247,16 @@ class AdminApiKeyController extends AbstractController 'message' => '删除成功', ]; } + + private function requireAdmin(): ?ResponseInterface + { + $user = $this->getAuthUser(); + if (!$user || !$user->isAdministrator()) { + return $this->response->json([ + 'code' => 403, + 'message' => '仅管理员可访问', + ])->withStatus(403); + } + return null; + } } diff --git a/frontend/src/constants/__tests__/permissions.spec.ts b/frontend/src/constants/__tests__/permissions.spec.ts index 8b9f016..ed81732 100644 --- a/frontend/src/constants/__tests__/permissions.spec.ts +++ b/frontend/src/constants/__tests__/permissions.spec.ts @@ -2,11 +2,12 @@ import { describe, it, expect } from 'vitest' import { ADMIN_ONLY_PATH_PREFIXES, isAdminOnlyPath } from '../permissions' describe('ADMIN_ONLY_PATH_PREFIXES', () => { - it('contains all 7 admin-only paths', () => { - expect(ADMIN_ONLY_PATH_PREFIXES).toHaveLength(7) + it('contains all 8 admin-only paths', () => { + expect(ADMIN_ONLY_PATH_PREFIXES).toHaveLength(8) expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/users') expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/roles') expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/route-groups') + expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/api-keys') expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/mq-status') expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/failed-messages') expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/logs/requests') diff --git a/frontend/src/constants/permissions.ts b/frontend/src/constants/permissions.ts index f710e9c..d6b1366 100644 --- a/frontend/src/constants/permissions.ts +++ b/frontend/src/constants/permissions.ts @@ -8,6 +8,7 @@ export const ADMIN_ONLY_PATH_PREFIXES: readonly string[] = [ '/users', '/roles', '/route-groups', + '/api-keys', '/mq-status', '/failed-messages', '/logs/requests', diff --git a/frontend/src/pages/api-keys/__tests__/index.spec.ts b/frontend/src/pages/api-keys/__tests__/index.spec.ts index c0501d7..639ba44 100644 --- a/frontend/src/pages/api-keys/__tests__/index.spec.ts +++ b/frontend/src/pages/api-keys/__tests__/index.spec.ts @@ -108,7 +108,8 @@ describe('AdminApiKeyPage', () => { it('重置按钮调用 resetFilters 并 fetchAllKeys', async () => { await mountPage() const store = useAdminApiKeyStore() - store.filters.user_id = 5 + store.filters.username = 'testuser' + store.filters.email = 'test@example.com' vi.mocked(api.get).mockClear() setupApi([]) @@ -119,7 +120,8 @@ describe('AdminApiKeyPage', () => { btn?.click() await flushPromises() - expect(store.filters.user_id).toBeUndefined() + expect(store.filters.username).toBeUndefined() + expect(store.filters.email).toBeUndefined() expect(vi.mocked(api.get)).toHaveBeenCalled() }) diff --git a/frontend/src/pages/api-keys/index.vue b/frontend/src/pages/api-keys/index.vue index eaec9f1..f10829e 100644 --- a/frontend/src/pages/api-keys/index.vue +++ b/frontend/src/pages/api-keys/index.vue @@ -62,12 +62,19 @@ function handleDelete(id: number) {
- +