fix(api): 字典和 MinIO 接入认证守卫

This commit is contained in:
sunlei 2026-05-17 14:56:26 +08:00
parent 5c526106bd
commit a920a92ad0
9 changed files with 65 additions and 14 deletions

4
API.md
View File

@ -48,7 +48,7 @@
### 后台认证
Admin 与 Component 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer <accessToken>` 传递 accessToken也可以携带登录接口写入的 httpOnly `admin_access_token` cookie。未认证时接口返回 HTTP `401`
Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`。请求可以通过 `Authorization: Bearer <accessToken>` 传递 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` 表核心字段:

View File

@ -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/`
## 轻量验证

View File

@ -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,
],
})

View File

@ -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 {}

View File

@ -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,

View File

@ -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],

View File

@ -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,

View File

@ -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],

View File

@ -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();
});
});