Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class AppModule {
ThrottlerModule.forRoot([
{
ttl: parseInt(process.env.THROTTLE_TTL || '60'),
limit: parseInt(process.env.THROTTLE_LIMIT || '10'),
limit: parseInt(process.env.THROTTLE_LIMIT || '60'),
},
]),
HealthModule,
Expand Down
4 changes: 4 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class AuthController {
}

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

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

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

@Post('verify-email')
@Throttle({ default: { limit: 10, ttl: 3600000 } }) // 10 requests per hour
@ApiOperation({ summary: 'Verify email using token' })
async verifyEmail(@Body() verifyEmailDto: VerifyEmailDto) {
return this.authService.verifyEmail(verifyEmailDto.token);
Expand Down
37 changes: 12 additions & 25 deletions src/common/guards/throttle.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, ExecutionContext, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler';
import { Request, Response } from 'express';

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

/** Called by ThrottlerGuard when the limit is exceeded. */
protected override throwThrottlingException(context: ExecutionContext): Promise<void> {
protected override async throwThrottlingException(
context: ExecutionContext,
throttlerLimitDetail: ThrottlerLimitDetail,
): Promise<void> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

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

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

// Get throttle options from the decorator or default
const throttleOptions = this.getThrottleOptions(context);

// Inject standard rate-limit headers so clients can back off gracefully
response.setHeader('Retry-After', throttleOptions.ttl);
response.setHeader('X-RateLimit-Limit', throttleOptions.limit);
// TTL in v6 is in seconds if defined that way in config, but throttlerLimitDetail.ttl is the value from config
const ttlSeconds = throttlerLimitDetail.ttl;

response.setHeader('Retry-After', ttlSeconds);
response.setHeader('X-RateLimit-Limit', throttlerLimitDetail.limit);
response.setHeader('X-RateLimit-Remaining', 0);
response.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + throttleOptions.ttl);
response.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + ttlSeconds);

throw new HttpException(
{
statusCode: HttpStatus.TOO_MANY_REQUESTS,
error: 'Too Many Requests',
message: 'You have exceeded the request rate limit. Please wait before retrying.',
retryAfterSeconds: throttleOptions.ttl,
retryAfterSeconds: ttlSeconds,
},
HttpStatus.TOO_MANY_REQUESTS,
);
Expand All @@ -49,20 +52,4 @@ export class CustomThrottleGuard extends ThrottlerGuard {
if (typeof forwarded === 'string') return forwarded.split(',')[0].trim();
return request.ip ?? request.socket?.remoteAddress ?? 'unknown';
}

private getThrottleOptions(context: ExecutionContext): { limit: number; ttl: number } {
// Try to get throttle options from the decorator
const handler = context.getHandler();
const throttleDecorator = Reflect.getMetadata('__throttler__', handler);

if (throttleDecorator) {
return {
limit: throttleDecorator.limit || 10,
ttl: throttleDecorator.ttl || 60,
};
}

// Fallback to default options
return { limit: 10, ttl: 60 };
}
}
2 changes: 2 additions & 0 deletions src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Controller, Get } from '@nestjs/common';
import { DataSource } from 'typeorm';
import Redis from 'ioredis';
import { SkipThrottle } from '@nestjs/throttler';
import { HealthService } from './health.service';

@SkipThrottle()
@Controller('health')
export class HealthController {
private redis: Redis;
Expand Down
2 changes: 2 additions & 0 deletions src/media/media.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Body,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Throttle } from '@nestjs/throttler';
import {
ApiTags,
ApiOperation,
Expand All @@ -35,6 +36,7 @@ export class MediaController {
constructor(private readonly mediaService: MediaService) {}

@Post('upload')
@Throttle({ default: { limit: 10, ttl: 3600000 } })
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: 'Upload media file with full validation' })
Expand Down
2 changes: 2 additions & 0 deletions src/payments/webhooks/webhook.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
} from '@nestjs/common';
import { Request } from 'express';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { SkipThrottle } from '@nestjs/throttler';
import { WebhookService } from './webhook.service';

@SkipThrottle()
@ApiTags('webhooks')
@Controller('webhooks')
export class WebhookController {
Expand Down
2 changes: 2 additions & 0 deletions src/search/search.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { SearchService } from './search.service';

@Throttle({ default: { limit: 30, ttl: 60000 } })
@Controller('search')
export class SearchController {
constructor(private readonly searchService: SearchService) {}
Expand Down
Loading