feat(security): helmet + CORS 白名单 + 限流 + 定时清理 + 时间契约 Unix 毫秒
- helmet 安全头 + CORS_ORIGIN 白名单 + Swagger 仅非生产 - @nestjs/throttler 全局限流(60s/100 次) - @nestjs/schedule 定时清理过期 token 与旧日志 - API 时间统一 Unix 毫秒(TransformInterceptor 递归转换) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
606585c3bd
commit
ba1ae43e39
@ -34,11 +34,14 @@
|
|||||||
"@nestjs/core": "^12.0.1",
|
"@nestjs/core": "^12.0.1",
|
||||||
"@nestjs/jwt": "^12.0.2",
|
"@nestjs/jwt": "^12.0.2",
|
||||||
"@nestjs/platform-express": "^12.0.1",
|
"@nestjs/platform-express": "^12.0.1",
|
||||||
|
"@nestjs/schedule": "^12.0.2",
|
||||||
"@nestjs/swagger": "^12.0.2",
|
"@nestjs/swagger": "^12.0.2",
|
||||||
|
"@nestjs/throttler": "^6.7.1",
|
||||||
"@prisma/client": "^6.19.3",
|
"@prisma/client": "^6.19.3",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"ioredis": "^6.0.0",
|
"ioredis": "^6.0.0",
|
||||||
"nestjs-pino": "^5.3.1",
|
"nestjs-pino": "^5.3.1",
|
||||||
"pino": "^10.4.0",
|
"pino": "^10.4.0",
|
||||||
|
|||||||
62
pnpm-lock.yaml
generated
62
pnpm-lock.yaml
generated
@ -178,9 +178,15 @@ importers:
|
|||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^12.0.1
|
specifier: ^12.0.1
|
||||||
version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
|
version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
|
||||||
|
'@nestjs/schedule':
|
||||||
|
specifier: ^12.0.2
|
||||||
|
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)
|
||||||
'@nestjs/swagger':
|
'@nestjs/swagger':
|
||||||
specifier: ^12.0.2
|
specifier: ^12.0.2
|
||||||
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3)
|
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3)
|
||||||
|
'@nestjs/throttler':
|
||||||
|
specifier: ^6.7.1
|
||||||
|
version: 6.7.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(reflect-metadata@0.2.2)
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.19.3
|
specifier: ^6.19.3
|
||||||
version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)
|
version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)
|
||||||
@ -193,6 +199,9 @@ importers:
|
|||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.15.1
|
specifier: ^0.15.1
|
||||||
version: 0.15.1
|
version: 0.15.1
|
||||||
|
helmet:
|
||||||
|
specifier: ^8.3.0
|
||||||
|
version: 8.3.0
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0(supports-color@7.2.0)
|
version: 6.0.0(supports-color@7.2.0)
|
||||||
@ -726,6 +735,13 @@ packages:
|
|||||||
'@nestjs/common': ^12.0.0
|
'@nestjs/common': ^12.0.0
|
||||||
'@nestjs/core': ^12.0.0
|
'@nestjs/core': ^12.0.0
|
||||||
|
|
||||||
|
'@nestjs/schedule@12.0.2':
|
||||||
|
resolution: {integrity: sha512-5iAvJEtk0njbfTrG4JQr+SXw8ZrqgDM1wJH9nUs6tVLCX5pWGd1LJ0a8b089UT1FDeDWaThRRfSLixFz9zztcA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^11.0.0 || ^12.0.0
|
||||||
|
'@nestjs/core': ^11.0.0 || ^12.0.0
|
||||||
|
|
||||||
'@nestjs/schematics@12.0.6':
|
'@nestjs/schematics@12.0.6':
|
||||||
resolution: {integrity: sha512-2OG0BUhGPJqVvWFhiy+7oaQa4VQUvTS9Dw8YRfvweznl4KS3YyZrdPi4cVGuRAThoEIsbdfguZzSOv1+t8U1tg==}
|
resolution: {integrity: sha512-2OG0BUhGPJqVvWFhiy+7oaQa4VQUvTS9Dw8YRfvweznl4KS3YyZrdPi4cVGuRAThoEIsbdfguZzSOv1+t8U1tg==}
|
||||||
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0}
|
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0}
|
||||||
@ -770,6 +786,14 @@ packages:
|
|||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/throttler@6.7.1':
|
||||||
|
resolution: {integrity: sha512-gAyY1AkU+cSK5DCjFOB43xax9oakameH4xZa9kAgqBqSBQZgmv3DZylLV8KyZNzOGhceL3kDVtby+CF8VAdmEA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
|
||||||
|
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
|
||||||
|
reflect-metadata: ^0.1.13 || ^0.2.0
|
||||||
|
|
||||||
'@noble/hashes@1.8.0':
|
'@noble/hashes@1.8.0':
|
||||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
@ -1115,6 +1139,9 @@ packages:
|
|||||||
'@types/jsonwebtoken@9.0.10':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
|
'@types/luxon@3.7.6':
|
||||||
|
resolution: {integrity: sha512-6KSjliQAXK8ZLgFQO4B7iE4Rpf/B3rItzCFuXezBY/XIAGxOdLd7vRNK9/StRPwiS/yJsVbB7HKpW46B8tcEuQ==}
|
||||||
|
|
||||||
'@types/methods@1.1.4':
|
'@types/methods@1.1.4':
|
||||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||||
|
|
||||||
@ -1445,6 +1472,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
cron@4.4.0:
|
||||||
|
resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==}
|
||||||
|
engines: {node: '>=18.x'}
|
||||||
|
|
||||||
date-fns@4.4.0:
|
date-fns@4.4.0:
|
||||||
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
||||||
|
|
||||||
@ -1705,6 +1736,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
helmet@8.3.0:
|
||||||
|
resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
help-me@5.0.0:
|
help-me@5.0.0:
|
||||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||||
|
|
||||||
@ -1929,6 +1964,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
|
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
luxon@3.7.2:
|
||||||
|
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@ -2981,6 +3020,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@nestjs/schedule@12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
|
||||||
|
'@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
cron: 4.4.0
|
||||||
|
|
||||||
'@nestjs/schematics@12.0.6(chokidar@5.0.0)(prettier@3.9.9)(typescript@6.0.3)':
|
'@nestjs/schematics@12.0.6(chokidar@5.0.0)(prettier@3.9.9)(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 22.2.0(chokidar@5.0.0)
|
'@angular-devkit/core': 22.2.0(chokidar@5.0.0)
|
||||||
@ -3019,6 +3064,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
|
'@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
|
||||||
|
|
||||||
|
'@nestjs/throttler@6.7.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(reflect-metadata@0.2.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
|
||||||
|
'@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
|
||||||
'@noble/hashes@1.8.0': {}
|
'@noble/hashes@1.8.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.153.0': {}
|
'@oxc-project/types@0.153.0': {}
|
||||||
@ -3245,6 +3296,8 @@ snapshots:
|
|||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
'@types/node': 24.19.1
|
'@types/node': 24.19.1
|
||||||
|
|
||||||
|
'@types/luxon@3.7.6': {}
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
@ -3570,6 +3623,11 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
|
cron@4.4.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/luxon': 3.7.6
|
||||||
|
luxon: 3.7.2
|
||||||
|
|
||||||
date-fns@4.4.0: {}
|
date-fns@4.4.0: {}
|
||||||
|
|
||||||
dateformat@4.6.3: {}
|
dateformat@4.6.3: {}
|
||||||
@ -3859,6 +3917,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
helmet@8.3.0: {}
|
||||||
|
|
||||||
help-me@5.0.0: {}
|
help-me@5.0.0: {}
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
@ -4060,6 +4120,8 @@ snapshots:
|
|||||||
is-unicode-supported: 2.1.0
|
is-unicode-supported: 2.1.0
|
||||||
yoctocolors: 2.2.0
|
yoctocolors: 2.2.0
|
||||||
|
|
||||||
|
luxon@3.7.2: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import { BodyLoggingInterceptor } from './logging/body-logging.interceptor.js';
|
|||||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js';
|
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js';
|
||||||
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor.js';
|
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor.js';
|
||||||
import { AllExceptionsFilter } from './common/filters/http-exception.filter.js';
|
import { AllExceptionsFilter } from './common/filters/http-exception.filter.js';
|
||||||
|
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { CleanupService } from './common/cleanup.service.js';
|
||||||
import { TenantsModule } from './tenants/tenants.module.js';
|
import { TenantsModule } from './tenants/tenants.module.js';
|
||||||
import { UsersModule } from './users/users.module.js';
|
import { UsersModule } from './users/users.module.js';
|
||||||
import { RolesModule } from './roles/roles.module.js';
|
import { RolesModule } from './roles/roles.module.js';
|
||||||
@ -25,6 +28,8 @@ import { AppService } from './app.service.js';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
createLoggerModule(),
|
createLoggerModule(),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
@ -42,6 +47,8 @@ import { AppService } from './app.service.js';
|
|||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
|
CleanupService,
|
||||||
|
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||||
{ provide: APP_GUARD, useClass: PermissionsGuard },
|
{ provide: APP_GUARD, useClass: PermissionsGuard },
|
||||||
{ provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor },
|
{ provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor },
|
||||||
|
|||||||
33
src/common/cleanup.service.ts
Normal file
33
src/common/cleanup.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service.js';
|
||||||
|
|
||||||
|
/** 定时清理:过期/吊销 refresh token 与超过保留期的审计/登录日志。 */
|
||||||
|
@Injectable()
|
||||||
|
export class CleanupService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
@InjectPinoLogger(CleanupService.name) private readonly logger: PinoLogger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** 每天 03:07 执行。 */
|
||||||
|
@Cron('7 3 * * *')
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const db = this.prisma.raw;
|
||||||
|
const tokens = await db.refreshToken.deleteMany({
|
||||||
|
where: { OR: [{ revokedAt: { not: null } }, { expireAt: { lt: new Date() } }] },
|
||||||
|
});
|
||||||
|
const cutoff = new Date(Date.now() - 90 * 24 * 3600 * 1000);
|
||||||
|
const audit = await db.auditLog.deleteMany({ where: { createdAt: { lt: cutoff } } });
|
||||||
|
const login = await db.loginLog.deleteMany({ where: { createdAt: { lt: cutoff } } });
|
||||||
|
this.logger.info(
|
||||||
|
{ refreshTokens: tokens.count, auditLogs: audit.count, loginLogs: login.count },
|
||||||
|
'定时清理完成',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn({ err }, '定时清理失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,12 +2,24 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/** 递归将 Date 转为 Unix 毫秒 number(API 时间契约:统一毫秒时间戳)。 */
|
||||||
|
function toUnixMs(value: unknown): unknown {
|
||||||
|
if (value instanceof Date) return value.getTime();
|
||||||
|
if (Array.isArray(value)) return value.map(toUnixMs);
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = toUnixMs(v);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一响应封装:{ code, message, data },成功 code = 0。
|
* 统一响应封装:{ code, message, data },成功 code = 0;时间统一 Unix 毫秒。
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
|
export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
|
||||||
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<unknown> {
|
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<unknown> {
|
||||||
return next.handle().pipe(map((data) => ({ code: 0, message: 'ok', data: data ?? null })));
|
return next.handle().pipe(map((data) => ({ code: 0, message: 'ok', data: toUnixMs(data ?? null) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/main.ts
18
src/main.ts
@ -3,29 +3,33 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { Logger } from 'nestjs-pino';
|
import { Logger } from 'nestjs-pino';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import helmet from 'helmet';
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from './app.module.js';
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
||||||
app.setGlobalPrefix('api/v1');
|
app.setGlobalPrefix('api/v1');
|
||||||
app.enableCors();
|
app.use(helmet());
|
||||||
|
const corsOrigin = process.env.CORS_ORIGIN
|
||||||
|
? process.env.CORS_ORIGIN.split(',').map((s) => s.trim())
|
||||||
|
: ['http://localhost:5173'];
|
||||||
|
app.enableCors({ origin: corsOrigin, credentials: true });
|
||||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||||
app.useLogger(app.get(Logger));
|
app.useLogger(app.get(Logger));
|
||||||
|
|
||||||
|
// Swagger 文档仅非生产环境开放(生产默认关闭)
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const swaggerConfig = new DocumentBuilder()
|
const swaggerConfig = new DocumentBuilder()
|
||||||
.setTitle('系统管理 API')
|
.setTitle('系统管理 API')
|
||||||
.setDescription('投云科技系统管理 API 文档')
|
.setDescription('投云科技系统管理 API 文档')
|
||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
.build();
|
.build();
|
||||||
SwaggerModule.setup(
|
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swaggerConfig));
|
||||||
'docs',
|
}
|
||||||
app,
|
|
||||||
SwaggerModule.createDocument(app, swaggerConfig),
|
|
||||||
);
|
|
||||||
|
|
||||||
const port = Number(process.env.PORT ?? 3000);
|
const port = Number(process.env.PORT ?? 3000);
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
app.get(Logger).log(`服务启动成功,端口 ${port},Swagger 文档: http://localhost:${port}/docs`);
|
app.get(Logger).log(`服务启动成功,端口 ${port}`);
|
||||||
}
|
}
|
||||||
await bootstrap();
|
await bootstrap();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user