- 重构sys模块架构,严格按admin/api/core分层 - 对齐所有sys实体与数据库表结构 - 实现完整的adminapi控制器,匹配PHP/Java契约 - 修复依赖注入问题,确保服务正确注册 - 添加自动迁移工具和契约验证 - 完善多租户支持和审计功能 - 统一命名规范,与PHP业务逻辑保持一致
226 lines
6.3 KiB
TypeScript
226 lines
6.3 KiB
TypeScript
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);
|
||
});
|
||
});
|