diff --git a/API.md b/API.md index 425b59b..4947b7a 100644 --- a/API.md +++ b/API.md @@ -48,7 +48,7 @@ ### 后台认证 -Admin 与 Component 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer ` 传递 accessToken,也可以携带登录接口写入的 httpOnly `admin_access_token` cookie。未认证时接口返回 HTTP `401`。 +Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer ` 传递 accessToken,也可以携带登录接口写入的 httpOnly `admin_access_token` cookie。未认证时接口返回 HTTP `401`。 `ADMIN_COOKIE_SECURE=false` 适用于当前内网 HTTP 访问;如果后续切到 HTTPS 域名,可以改为 `true`,cookie 会使用 `Secure + SameSite=None`。 @@ -56,7 +56,7 @@ Admin 与 Component 业务接口统一走 `JwtAuthGuard`。请求可以通过 `A ### 数据库字典翻译 -组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容。 +组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容,但仍需要登录态。 `admin_dict` 表核心字段: diff --git a/README.md b/README.md index fe9b742..64b83c9 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ pnpm test:e2e # e2e 测试 - 如果基础后台菜单的 `meta` 被旧数据覆盖为空,执行 `sql/fix-admin-menu-meta.sql` 可以恢复初始化菜单的 `title/icon/order` 等元数据。 - 旧 `component` 表迁移到 `admin_component` 时,执行 `sql/migrate-component-to-admin-component.sql`,脚本会把旧表重命名为备份表。 - 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。 -- Admin 与 Component 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。 +- Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。 - `kt-template-admin` 登录会写入 access token 与刷新 token cookie,`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。 - `kt-template-admin` 开发环境通过 `/api` 代理到本服务 `48085`,已关闭 Vben Nitro Mock。 - `POST /component/save` 新增组件,`POST /component/update` 编辑组件。 @@ -114,8 +114,8 @@ pnpm test:e2e # e2e 测试 ## 联调关系 -- `kt-template-online-web` 读取 `/component/list`、`/component/detail`、`/dict/*` 展示组件列表,并生成 Playground 跳转链接;组件接口返回 `401` 时跳转到 `kt-template-admin` 登录。 -- `kt-template-online-playground` 读取 `/dict/*` 初始化分类,保存时上传截图到 `/minio/upload`,再调用 `/component/save` 或 `/component/update`;组件接口返回 `401` 时跳转到 `kt-template-admin` 登录并在回跳后刷新 token。 +- `kt-template-online-web` 读取 `/component/list`、`/component/detail`、`/dict/*` 展示组件列表,并生成 Playground 跳转链接;业务接口返回 `401` 时跳转到 `kt-template-admin` 登录。 +- `kt-template-online-playground` 读取 `/dict/*` 初始化分类,保存时上传截图到 `/minio/upload`,再调用 `/component/save` 或 `/component/update`;业务接口返回 `401` 时跳转到 `kt-template-admin` 登录并在回跳后刷新 token。 - 前端项目通过 Vite 代理把 `/api` 转发到 `http://localhost:48085/`。 ## 轻量验证 diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 9c2fffa..7971b9f 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -1,9 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminAuthGuardModule } from './auth/admin-auth-guard.module'; import { AdminAuthController } from './auth/admin-auth.controller'; -import { AdminAuthService } from './auth/admin-auth.service'; -import { JwtAuthGuard } from './auth/jwt-auth.guard'; -import { AdminTokenService } from './auth/admin-token.service'; import { ComponentController } from './component/component.controller'; import { Component } from './component/component.entity'; import { ComponentService } from './component/component.service'; @@ -35,6 +33,7 @@ import { MinioClientModule } from '@/minio/minio.module'; AdminDept, Component, ]), + AdminAuthGuardModule, DictModule, MinioClientModule, ], @@ -49,15 +48,12 @@ import { MinioClientModule } from '@/minio/minio.module'; AdminExampleController, ], providers: [ - AdminAuthService, ComponentService, AdminDeptService, AdminMenuService, AdminRoleService, AdminTimezoneService, - AdminTokenService, AdminUserService, - JwtAuthGuard, ToolsService, ], }) diff --git a/src/admin/auth/admin-auth-guard.module.ts b/src/admin/auth/admin-auth-guard.module.ts new file mode 100644 index 0000000..0bae3b1 --- /dev/null +++ b/src/admin/auth/admin-auth-guard.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminUser } from '../user/admin-user.entity'; +import { AdminAuthService } from './admin-auth.service'; +import { AdminTokenService } from './admin-token.service'; +import { JwtAuthGuard } from './jwt-auth.guard'; + +@Module({ + imports: [ConfigModule, TypeOrmModule.forFeature([AdminUser])], + providers: [AdminAuthService, AdminTokenService, JwtAuthGuard], + exports: [AdminAuthService, AdminTokenService, JwtAuthGuard], +}) +export class AdminAuthGuardModule {} diff --git a/src/admin/dict/dict.controller.ts b/src/admin/dict/dict.controller.ts index 4ab74cb..80f4039 100644 --- a/src/admin/dict/dict.controller.ts +++ b/src/admin/dict/dict.controller.ts @@ -5,11 +5,13 @@ import { ParseIntPipe, Query, Res, + UseGuards, } from '@nestjs/common'; import { DictService } from './dict.service'; import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiArrayResponse, ToolsService } from '@/common'; import { DictDto } from './dict.dto'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; const componentTypeDictExample = [ { @@ -35,6 +37,7 @@ const chartDictExample = [ @ApiTags('dict') @Controller('dict') +@UseGuards(JwtAuthGuard) export class DictController { constructor( private readonly toolsService: ToolsService, diff --git a/src/admin/dict/dict.module.ts b/src/admin/dict/dict.module.ts index 5042d83..ff6c4e1 100644 --- a/src/admin/dict/dict.module.ts +++ b/src/admin/dict/dict.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module'; import { DictController } from './dict.controller'; import { DictService } from './dict.service'; import { ToolsService } from '@/common'; import { AdminDict } from './admin-dict.entity'; @Module({ - imports: [TypeOrmModule.forFeature([AdminDict])], + imports: [AdminAuthGuardModule, TypeOrmModule.forFeature([AdminDict])], controllers: [DictController], providers: [DictService, ToolsService], exports: [DictService], diff --git a/src/minio/minio.controller.ts b/src/minio/minio.controller.ts index 128cbe6..4899bc4 100644 --- a/src/minio/minio.controller.ts +++ b/src/minio/minio.controller.ts @@ -9,6 +9,7 @@ import { Query, Res, UploadedFile, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -34,6 +35,7 @@ import { MinioObjectDto, MinioUploadResultDto, } from './minio.dto'; +import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard'; const PROXY_RESOURCE_TIMEOUT = 1000 * 15; const PROXY_RESOURCE_CONTENT_TYPES = [ @@ -49,6 +51,7 @@ const PROXY_RESOURCE_EXTENSION_RE = @Controller('minio') @ApiTags('minio') +@UseGuards(JwtAuthGuard) export class MinioClientController { constructor( private readonly toolsService: ToolsService, diff --git a/src/minio/minio.module.ts b/src/minio/minio.module.ts index c5bdff5..b74f673 100644 --- a/src/minio/minio.module.ts +++ b/src/minio/minio.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module'; import { MinioClientController } from './minio.controller'; import { MinioClientService } from './minio.service'; import { ToolsService } from '@/common'; @Module({ - imports: [ConfigModule], + imports: [AdminAuthGuardModule, ConfigModule], controllers: [MinioClientController], providers: [MinioClientService, ToolsService], exports: [MinioClientService], diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index e88ee6f..fc6e913 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { HttpException, HttpStatus, INestApplication } from '@nestjs/common'; import { APP_INTERCEPTOR, Reflector } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import request = require('supertest'); @@ -76,6 +76,17 @@ const authServiceMock = { currentUser: jest.fn(), }; +const unauthorizedException = () => + new HttpException( + { + code: -1, + data: null, + error: 'Unauthorized Exception', + message: 'Unauthorized Exception', + }, + HttpStatus.UNAUTHORIZED, + ); + const dictServiceMock = { getDictByKey: jest.fn(), getComponentDictByType: jest.fn(), @@ -518,6 +529,10 @@ describe('KT Template Online API (e2e)', () => { beforeEach(() => { jest.clearAllMocks(); + authServiceMock.currentUser.mockResolvedValue({ + id: '2041739550026043001', + username: 'admin', + }); }); afterAll(async () => { @@ -560,4 +575,22 @@ describe('KT Template Online API (e2e)', () => { data: false, }); }); + + it('protects dict and minio endpoints with jwt auth', async () => { + authServiceMock.currentUser.mockRejectedValue(unauthorizedException()); + + await request(app.getHttpServer()) + .get('/dict/getDictByKey') + .query({ dictKey: 'COMPONENT_TYPE' }) + .expect(401); + + expect(dictServiceMock.getDictByKey).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + authServiceMock.currentUser.mockRejectedValue(unauthorizedException()); + + await request(app.getHttpServer()).get('/minio/check').expect(401); + + expect(minioServiceMock.checkConnection).not.toHaveBeenCalled(); + }); });