feat: 完成sys模块迁移,对齐PHP/Java框架

- 重构sys模块架构,严格按admin/api/core分层
- 对齐所有sys实体与数据库表结构
- 实现完整的adminapi控制器,匹配PHP/Java契约
- 修复依赖注入问题,确保服务正确注册
- 添加自动迁移工具和契约验证
- 完善多租户支持和审计功能
- 统一命名规范,与PHP业务逻辑保持一致
This commit is contained in:
万物街
2025-09-21 21:29:28 +08:00
parent 2e361795d9
commit 127a4db1e3
839 changed files with 24932 additions and 57988 deletions

View File

@@ -0,0 +1,225 @@
import { Controller, Get, Req, UseGuards, Module } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { SecurityModule } from '../../src/core/security/securityModule';
import { AdminCheckTokenGuard } from '../../src/core/security/adminCheckToken.guard';
import { ApiCheckTokenGuard } from '../../src/core/security/apiCheckToken.guard';
import { ApiOptionalAuthGuard } from '../../src/core/security/apiOptionalAuth.guard';
import { SiteScopeGuard } from '../../src/core/security/siteScopeGuard';
import { TokenAuthService } from '../../src/core/security/tokenAuth.service';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
@Controller()
class SecurityTestController {
// 管理端受保护路由(仅校验 token
@Get('adminapi/secure')
@UseGuards(AdminCheckTokenGuard)
adminSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 管理端受保护路由(校验 token + 站点隔离)
@Get('adminapi/secure-site')
@UseGuards(AdminCheckTokenGuard, SiteScopeGuard)
adminSecureSite(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台受保护路由(必须登录)
@Get('api/secure')
@UseGuards(ApiCheckTokenGuard)
apiSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
memberId: req.auth?.('member_id') ?? req.memberId ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台可选登录路由(未登录也可访问)
@Get('api/optional')
@UseGuards(ApiOptionalAuthGuard)
apiOptional(@Req() req: any) {
const hasUser = !!(req.auth?.('member_id') || req.memberId);
return {
appType: req.auth?.('app_type') || req.appType || '',
hasUser,
};
}
}
@Module({
imports: [SecurityModule, NestCacheModule.register()],
controllers: [SecurityTestController],
})
class SecurityTestModule {}
describe('Security Guards (e2e)', () => {
let app: any;
let tokenService: TokenAuthService;
// 内存版 Redis 替身(仅实现 get/set/del
const memory = new Map<string, string>();
const mockRedis = {
async get(key: string) {
return memory.has(key) ? memory.get(key)! : null;
},
async set(key: string, value: string) {
memory.set(key, value);
return 'OK';
},
async del(key: string) {
const existed = memory.delete(key);
return existed ? 1 : 0;
},
} as any;
// 轻量 cache-manager 替身,满足 CACHE_MANAGER 依赖
const mockCache = {
async get<T>(_key: string): Promise<T | null> {
return null;
},
async set<T>(_key: string, _value: T, _ttl?: number): Promise<void> {
return;
},
async del(_key: string): Promise<void> {
return;
},
} as any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [SecurityTestModule],
})
.overrideProvider('REDIS_CLIENT')
.useValue(mockRedis)
.overrideProvider(CACHE_MANAGER)
.useValue(mockCache)
.compile();
app = (await moduleFixture.createNestApplication()).getHttpServer
? await moduleFixture.createNestApplication()
: await moduleFixture.createNestApplication();
await app.init();
tokenService = moduleFixture.get(TokenAuthService);
});
afterAll(async () => {
if (app) {
await app.close();
}
});
it('Admin: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/adminapi/secure').expect(401);
});
it('Admin: 200 with valid token (no site header)', async () => {
const { token } = await tokenService.createToken(
1001,
'admin',
{
uid: 1001,
username: 'root',
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure')
.set('token', token)
.expect(200);
expect(res.body.data?.uid ?? res.body.uid).toBe(1001);
const appType = res.body.data?.appType ?? res.body.appType;
expect(appType).toBe('admin');
});
it('Admin: 403 on site mismatch with SiteScopeGuard', async () => {
const { token, params } = await tokenService.createToken(
2002,
'admin',
{
uid: 2002,
username: 'admin2',
site_id: 2,
},
600,
);
// header 指定不同 site-id 触发越权
await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '3')
.expect(403);
});
it('Admin: 200 when site matches with SiteScopeGuard', async () => {
const { token } = await tokenService.createToken(
2003,
'admin',
{
uid: 2003,
username: 'admin3',
site_id: 5,
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '5')
.expect(200);
expect(res.body.data?.siteId ?? res.body.siteId).toBe(5);
});
it('API: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/api/secure').expect(401);
});
it('API: 200 with valid token', async () => {
const { token } = await tokenService.createToken(
3001,
'api',
{
member_id: 3001,
username: 'member1',
site_id: 8,
},
600,
);
const res = await request(app.getHttpServer())
.get('/api/secure')
.set('token', token)
.expect(200);
const memberId = res.body.data?.memberId ?? res.body.memberId;
expect(memberId).toBe(3001);
});
it('API Optional: 200 without token and no user', async () => {
const res = await request(app.getHttpServer())
.get('/api/optional')
.expect(200);
const hasUser = res.body.data?.hasUser ?? res.body.hasUser;
expect(hasUser).toBe(false);
});
});