feat: 完成sys模块迁移,对齐PHP/Java框架
- 重构sys模块架构,严格按admin/api/core分层 - 对齐所有sys实体与数据库表结构 - 实现完整的adminapi控制器,匹配PHP/Java契约 - 修复依赖注入问题,确保服务正确注册 - 添加自动迁移工具和契约验证 - 完善多租户支持和审计功能 - 统一命名规范,与PHP业务逻辑保持一致
This commit is contained in:
225
wwjcloud/test/e2e/security.e2e-spec.ts
Normal file
225
wwjcloud/test/e2e/security.e2e-spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user