Skip to content

Commit 38f1c5e

Browse files
Merge pull request #301 from gabito1451/Global-Rate-Limiting-Configuration
implement consistent rate limiting across controllers and update throttler guard compatibility
2 parents 6c89a66 + 0151684 commit 38f1c5e

7 files changed

Lines changed: 25 additions & 26 deletions

File tree

src/app.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class AppModule {
121121
ThrottlerModule.forRoot([
122122
{
123123
ttl: parseInt(process.env.THROTTLE_TTL || '60'),
124-
limit: parseInt(process.env.THROTTLE_LIMIT || '10'),
124+
limit: parseInt(process.env.THROTTLE_LIMIT || '60'),
125125
},
126126
]),
127127
HealthModule,

src/auth/auth.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class AuthController {
3939
}
4040

4141
@Post('refresh')
42+
@Throttle({ default: { limit: 20, ttl: 60000 } }) // 20 requests per minute
4243
@ApiOperation({ summary: 'Refresh access token using refresh token' })
4344
async refresh(@Body() refreshTokenDto: RefreshTokenDto) {
4445
return this.authService.refreshToken(refreshTokenDto.refreshToken);
@@ -55,12 +56,14 @@ export class AuthController {
5556
}
5657

5758
@Post('forgot-password')
59+
@Throttle({ default: { limit: 5, ttl: 3600000 } }) // 5 requests per hour
5860
@ApiOperation({ summary: 'Request a password reset link' })
5961
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
6062
return this.authService.forgotPassword(forgotPasswordDto.email);
6163
}
6264

6365
@Post('reset-password')
66+
@Throttle({ default: { limit: 5, ttl: 3600000 } }) // 5 requests per hour
6467
@ApiOperation({ summary: 'Reset password using token' })
6568
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
6669
return this.authService.resetPassword(resetPasswordDto);
@@ -81,6 +84,7 @@ export class AuthController {
8184
}
8285

8386
@Post('verify-email')
87+
@Throttle({ default: { limit: 10, ttl: 3600000 } }) // 10 requests per hour
8488
@ApiOperation({ summary: 'Verify email using token' })
8589
async verifyEmail(@Body() verifyEmailDto: VerifyEmailDto) {
8690
return this.authService.verifyEmail(verifyEmailDto.token);
Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable, ExecutionContext, Logger, HttpException, HttpStatus } from '@nestjs/common';
2-
import { ThrottlerGuard } from '@nestjs/throttler';
2+
import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler';
33
import { Request, Response } from 'express';
44

55
/**
@@ -15,7 +15,10 @@ export class CustomThrottleGuard extends ThrottlerGuard {
1515
private readonly logger = new Logger(CustomThrottleGuard.name);
1616

1717
/** Called by ThrottlerGuard when the limit is exceeded. */
18-
protected override throwThrottlingException(context: ExecutionContext): Promise<void> {
18+
protected override async throwThrottlingException(
19+
context: ExecutionContext,
20+
throttlerLimitDetail: ThrottlerLimitDetail,
21+
): Promise<void> {
1922
const request = context.switchToHttp().getRequest<Request>();
2023
const response = context.switchToHttp().getResponse<Response>();
2124

@@ -24,21 +27,21 @@ export class CustomThrottleGuard extends ThrottlerGuard {
2427

2528
this.logger.warn(`Rate limit exceeded: ip=${ip} method=${request.method} route=${route}`);
2629

27-
// Get throttle options from the decorator or default
28-
const throttleOptions = this.getThrottleOptions(context);
29-
3030
// Inject standard rate-limit headers so clients can back off gracefully
31-
response.setHeader('Retry-After', throttleOptions.ttl);
32-
response.setHeader('X-RateLimit-Limit', throttleOptions.limit);
31+
// TTL in v6 is in seconds if defined that way in config, but throttlerLimitDetail.ttl is the value from config
32+
const ttlSeconds = throttlerLimitDetail.ttl;
33+
34+
response.setHeader('Retry-After', ttlSeconds);
35+
response.setHeader('X-RateLimit-Limit', throttlerLimitDetail.limit);
3336
response.setHeader('X-RateLimit-Remaining', 0);
34-
response.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + throttleOptions.ttl);
37+
response.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + ttlSeconds);
3538

3639
throw new HttpException(
3740
{
3841
statusCode: HttpStatus.TOO_MANY_REQUESTS,
3942
error: 'Too Many Requests',
4043
message: 'You have exceeded the request rate limit. Please wait before retrying.',
41-
retryAfterSeconds: throttleOptions.ttl,
44+
retryAfterSeconds: ttlSeconds,
4245
},
4346
HttpStatus.TOO_MANY_REQUESTS,
4447
);
@@ -49,20 +52,4 @@ export class CustomThrottleGuard extends ThrottlerGuard {
4952
if (typeof forwarded === 'string') return forwarded.split(',')[0].trim();
5053
return request.ip ?? request.socket?.remoteAddress ?? 'unknown';
5154
}
52-
53-
private getThrottleOptions(context: ExecutionContext): { limit: number; ttl: number } {
54-
// Try to get throttle options from the decorator
55-
const handler = context.getHandler();
56-
const throttleDecorator = Reflect.getMetadata('__throttler__', handler);
57-
58-
if (throttleDecorator) {
59-
return {
60-
limit: throttleDecorator.limit || 10,
61-
ttl: throttleDecorator.ttl || 60,
62-
};
63-
}
64-
65-
// Fallback to default options
66-
return { limit: 10, ttl: 60 };
67-
}
6855
}

src/health/health.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Controller, Get } from '@nestjs/common';
22
import { DataSource } from 'typeorm';
33
import Redis from 'ioredis';
4+
import { SkipThrottle } from '@nestjs/throttler';
45
import { HealthService } from './health.service';
56

7+
@SkipThrottle()
68
@Controller('health')
79
export class HealthController {
810
private redis: Redis;

src/media/media.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Body,
1414
} from '@nestjs/common';
1515
import { FileInterceptor } from '@nestjs/platform-express';
16+
import { Throttle } from '@nestjs/throttler';
1617
import {
1718
ApiTags,
1819
ApiOperation,
@@ -35,6 +36,7 @@ export class MediaController {
3536
constructor(private readonly mediaService: MediaService) {}
3637

3738
@Post('upload')
39+
@Throttle({ default: { limit: 10, ttl: 3600000 } })
3840
@UseGuards(JwtAuthGuard)
3941
@UseInterceptors(FileInterceptor('file'))
4042
@ApiOperation({ summary: 'Upload media file with full validation' })

src/payments/webhooks/webhook.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
} from '@nestjs/common';
1111
import { Request } from 'express';
1212
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
13+
import { SkipThrottle } from '@nestjs/throttler';
1314
import { WebhookService } from './webhook.service';
1415

16+
@SkipThrottle()
1517
@ApiTags('webhooks')
1618
@Controller('webhooks')
1719
export class WebhookController {

src/search/search.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
2+
import { Throttle } from '@nestjs/throttler';
23
import { SearchService } from './search.service';
34

5+
@Throttle({ default: { limit: 30, ttl: 60000 } })
46
@Controller('search')
57
export class SearchController {
68
constructor(private readonly searchService: SearchService) {}

0 commit comments

Comments
 (0)