From c180d0e48755b614731ff5e92b900eb2365b0543 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:38:44 +0100 Subject: [PATCH] feat: ratelimits (#2073) * feat: rate limits * ip --- apps/server/package.json | 3 ++ apps/server/src/app.module.ts | 2 + apps/server/src/common/logger/pino.config.ts | 20 +++----- .../middlewares/audit-context.middleware.ts | 20 +------- apps/server/src/core/auth/auth.controller.ts | 5 ++ apps/server/src/ee | 2 +- .../integrations/throttle/throttle.module.ts | 35 ++++++++++++++ apps/server/src/main.ts | 2 + pnpm-lock.yaml | 48 +++++++++++++++++++ 9 files changed, 104 insertions(+), 33 deletions(-) create mode 100644 apps/server/src/integrations/throttle/throttle.module.ts diff --git a/apps/server/package.json b/apps/server/package.json index d8bab08c..a8869302 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -44,6 +44,7 @@ "@langchain/core": "1.1.34", "@langchain/textsplitters": "1.0.1", "@modelcontextprotocol/sdk": "^1.27.1", + "@nest-lab/throttler-storage-redis": "^1.2.0", "@nestjs-labs/nestjs-ioredis": "^11.0.4", "@nestjs/bullmq": "^11.0.4", "@nestjs/cache-manager": "^3.1.0", @@ -58,6 +59,7 @@ "@nestjs/platform-socket.io": "^11.1.17", "@nestjs/schedule": "^6.1.1", "@nestjs/terminus": "^11.1.1", + "@nestjs/throttler": "^6.5.0", "@nestjs/websockets": "^11.1.17", "@node-saml/passport-saml": "^5.1.0", "@react-email/components": "1.0.10", @@ -73,6 +75,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "cookie": "^1.1.1", + "fastify-ip": "^2.0.0", "fs-extra": "^11.3.4", "happy-dom": "20.8.9", "ioredis": "^5.10.1", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 6280ee09..b8cfc587 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -26,6 +26,7 @@ import KeyvRedis from '@keyv/redis'; import { LoggerModule } from './common/logger/logger.module'; import { ClsModule } from 'nestjs-cls'; import { NoopAuditModule } from './integrations/audit/audit.module'; +import { ThrottleModule } from './integrations/throttle/throttle.module'; const enterpriseModules = []; try { @@ -83,6 +84,7 @@ try { EventEmitterModule.forRoot(), SecurityModule, TelemetryModule, + ThrottleModule, ...enterpriseModules, ], controllers: [AppController], diff --git a/apps/server/src/common/logger/pino.config.ts b/apps/server/src/common/logger/pino.config.ts index 7299a8e9..0b8cd11a 100644 --- a/apps/server/src/common/logger/pino.config.ts +++ b/apps/server/src/common/logger/pino.config.ts @@ -50,20 +50,12 @@ export function createPinoConfig(): Params { }, }, serializers: { - req: (req) => { - const forwardedFor = req.headers?.['x-forwarded-for']; - const ip = - req.headers?.['cf-connecting-ip'] || - (typeof forwardedFor === 'string' ? forwardedFor.split(',')[0]?.trim() : undefined) || - req.remoteAddress; - - return { - method: req.method, - url: req.url, - ip, - userAgent: req.headers?.['user-agent'], - }; - }, + req: (req) => ({ + method: req.method, + url: req.url, + ip: req.ip || req.remoteAddress, + userAgent: req.headers?.['user-agent'], + }), res: (res) => ({ statusCode: res.statusCode, }), diff --git a/apps/server/src/common/middlewares/audit-context.middleware.ts b/apps/server/src/common/middlewares/audit-context.middleware.ts index f5066535..52956219 100644 --- a/apps/server/src/common/middlewares/audit-context.middleware.ts +++ b/apps/server/src/common/middlewares/audit-context.middleware.ts @@ -18,7 +18,8 @@ export class AuditContextMiddleware implements NestMiddleware { use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) { const workspaceId = (req as any).workspaceId ?? null; - const ipAddress = this.extractIpAddress(req); + + const ipAddress = (req as any).ip ?? (req as any).socket?.remoteAddress ?? null; const userAgent = (req.headers['user-agent'] as string) ?? null; @@ -35,21 +36,4 @@ export class AuditContextMiddleware implements NestMiddleware { next(); } - - private extractIpAddress(req: FastifyRequest['raw']): string | null { - const xForwardedFor = req.headers['x-forwarded-for']; - if (xForwardedFor) { - const ips = Array.isArray(xForwardedFor) - ? xForwardedFor[0] - : xForwardedFor.split(',')[0]; - return ips?.trim() ?? null; - } - - const xRealIp = req.headers['x-real-ip']; - if (xRealIp) { - return Array.isArray(xRealIp) ? xRealIp[0] : xRealIp; - } - - return (req as any).socket?.remoteAddress ?? null; - } } diff --git a/apps/server/src/core/auth/auth.controller.ts b/apps/server/src/core/auth/auth.controller.ts index 6eab6539..441bfc1c 100644 --- a/apps/server/src/core/auth/auth.controller.ts +++ b/apps/server/src/core/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { UseGuards, Logger, } from '@nestjs/common'; +import { SkipThrottle, ThrottlerGuard } from '@nestjs/throttler'; import { LoginDto } from './dto/login.dto'; import { AuthService } from './services/auth.service'; import { SessionService } from '../session/session.service'; @@ -33,6 +34,7 @@ import { IAuditService, } from '../../integrations/audit/audit.service'; +@UseGuards(ThrottlerGuard) @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); @@ -111,6 +113,7 @@ export class AuthController { return workspace; } + @SkipThrottle() @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('change-password') @@ -173,6 +176,7 @@ export class AuthController { return this.authService.verifyUserToken(verifyUserTokenDto, workspace.id); } + @SkipThrottle() @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('collab-token') @@ -183,6 +187,7 @@ export class AuthController { return this.authService.getCollabToken(user, workspace.id); } + @SkipThrottle() @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('logout') diff --git a/apps/server/src/ee b/apps/server/src/ee index 05f1c816..350ef574 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 05f1c816a839072efc1143cce71322a9ed6b4a0a +Subproject commit 350ef574e398c318aa57ce5f79ab12e9d8329dcb diff --git a/apps/server/src/integrations/throttle/throttle.module.ts b/apps/server/src/integrations/throttle/throttle.module.ts new file mode 100644 index 00000000..8f080e1d --- /dev/null +++ b/apps/server/src/integrations/throttle/throttle.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; +import { EnvironmentService } from '../environment/environment.service'; +import { EnvironmentModule } from '../environment/environment.module'; +import { parseRedisUrl } from '../../common/helpers'; +import Redis from 'ioredis'; + +@Module({ + imports: [ + ThrottlerModule.forRootAsync({ + imports: [EnvironmentModule], + useFactory: (environmentService: EnvironmentService) => { + const redisConfig = parseRedisUrl(environmentService.getRedisUrl()); + + return { + throttlers: [{ name: 'auth', ttl: 60_000, limit: 10 }], + errorMessage: 'Too many requests', + storage: new ThrottlerStorageRedisService( + new Redis({ + host: redisConfig.host, + port: redisConfig.port, + password: redisConfig.password, + db: redisConfig.db, + family: redisConfig.family, + keyPrefix: 'throttle:', + }), + ), + }; + }, + inject: [EnvironmentService], + }), + ], +}) +export class ThrottleModule {} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 0f2a82a1..d47bf547 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -10,6 +10,7 @@ import { TransformHttpResponseInterceptor } from './common/interceptors/http-res import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter'; import fastifyMultipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; +import fastifyIp from 'fastify-ip'; import { InternalLogFilter } from './common/logger/internal-log-filter'; async function bootstrap() { @@ -45,6 +46,7 @@ async function bootstrap() { app.useWebSocketAdapter(redisIoAdapter); + await app.register(fastifyIp); await app.register(fastifyMultipart); await app.register(fastifyCookie); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0389d23..6599b07a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -493,6 +493,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.27.1 version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6) + '@nest-lab/throttler-storage-redis': + specifier: ^1.2.0 + version: 1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2) '@nestjs-labs/nestjs-ioredis': specifier: ^11.0.4 version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1) @@ -535,6 +538,9 @@ importers: '@nestjs/terminus': specifier: ^11.1.1 version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.1.17 version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -580,6 +586,9 @@ importers: cookie: specifier: ^1.1.1 version: 1.1.1 + fastify-ip: + specifier: ^2.0.0 + version: 2.0.0 fs-extra: specifier: ^11.3.4 version: 11.3.4 @@ -2925,6 +2934,15 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nest-lab/throttler-storage-redis@1.2.0': + resolution: {integrity: sha512-tMkUyo68NCKTR+zILk+EC35SMYBtDPZY2mCj7ZaCietWGVTnuP4zwq9ERYfvU6kJv6h8teNZrC6MJCmY6/dljw==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/throttler': '>=6.0.0' + ioredis: '>=5.0.0' + reflect-metadata: ^0.2.1 + '@nestjs-labs/nestjs-ioredis@11.0.4': resolution: {integrity: sha512-4jPNOrxDiwNMIN5OLmsMWhA782kxv/ZBxkySX9l8n6sr55acHX/BciaFsOXVa/ILsm+Y7893y98/6WNhmEoiNQ==} engines: {node: '>=16'} @@ -3127,6 +3145,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/websockets@11.1.17': resolution: {integrity: sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==} peerDependencies: @@ -7016,6 +7041,10 @@ packages: resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} hasBin: true + fastify-ip@2.0.0: + resolution: {integrity: sha512-7mQyAc7sapawpiriEFoJyQIs41nNIO42UCzgMKrjNGsIegnevj2VhOlXLLTa+q7cxXfJ5fDGmOAdQpaIgA9ObA==} + engines: {node: '>=20.x'} + fastify-plugin@5.0.1: resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} @@ -13463,6 +13492,15 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) + ioredis: 5.10.1 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -13643,6 +13681,12 @@ snapshots: '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/websockets@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -18072,6 +18116,10 @@ snapshots: path-expression-matcher: 1.2.0 strnum: 2.2.1 + fastify-ip@2.0.0: + dependencies: + fastify-plugin: 5.1.0 + fastify-plugin@5.0.1: {} fastify-plugin@5.1.0: {}