diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 34e828ca..48e53f22 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -6,14 +6,34 @@ import { EmailModule } from '../email/email.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { LoginRateLimitService } from './login-rate-limit.service'; +import { RateLimitService } from './rate-limit.service'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { ApiKeyAuthGuard } from './guards/api-key-auth.guard'; import { RolesGuard } from './guards/roles.guard'; +import { RateLimitGuard } from './guards/rate-limit.guard'; +import { RateLimitHeadersInterceptor } from './interceptors/rate-limit-headers.interceptor'; +import { RateLimitAdminController } from './controllers/rate-limit-admin.controller'; @Module({ imports: [PrismaModule, UsersModule, SessionsModule, EmailModule], - controllers: [AuthController], - providers: [AuthService, LoginRateLimitService, JwtAuthGuard, ApiKeyAuthGuard, RolesGuard], - exports: [AuthService, RolesGuard, LoginRateLimitService], + controllers: [AuthController, RateLimitAdminController], + providers: [ + AuthService, + LoginRateLimitService, + RateLimitService, + JwtAuthGuard, + ApiKeyAuthGuard, + RolesGuard, + RateLimitGuard, + RateLimitHeadersInterceptor, + ], + exports: [ + AuthService, + RolesGuard, + LoginRateLimitService, + RateLimitService, + RateLimitGuard, + RateLimitHeadersInterceptor, + ], }) export class AuthModule {} diff --git a/src/auth/controllers/rate-limit-admin.controller.ts b/src/auth/controllers/rate-limit-admin.controller.ts new file mode 100644 index 00000000..3d241cf1 --- /dev/null +++ b/src/auth/controllers/rate-limit-admin.controller.ts @@ -0,0 +1,188 @@ +import { + Controller, + Get, + Post, + Delete, + Param, + UseGuards, + HttpCode, + HttpStatus, + Body, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { RateLimitService, RateLimitStatus } from '../rate-limit.service'; +import { SkipRateLimit } from '../guards/rate-limit.guard'; + +@ApiTags('Admin - Rate Limiting') +@Controller('admin/rate-limits') +@ApiBearerAuth('JWT') +export class RateLimitAdminController { + constructor(private rateLimitService: RateLimitService) {} + + @Get('user/:userId') + @ApiOperation({ + summary: 'Get rate limit status for a user', + description: 'Retrieve current rate limit status and remaining requests for a user', + }) + @ApiResponse({ + status: 200, + description: 'Rate limit status retrieved successfully', + schema: { + example: { + user: { + limit: 5000, + remaining: 4999, + reset: 1703088000, + isExceeded: false, + }, + reset: '2024-12-21T00:00:00Z', + }, + }, + }) + async getUserRateLimitStatus( + @Param('userId') userId: string, + ): Promise { + return this.rateLimitService.getUserRateLimitStats(userId); + } + + @Get('endpoint/:endpoint') + @SkipRateLimit() + @ApiOperation({ + summary: 'Get rate limit status for an endpoint', + description: 'Retrieve current rate limit status for a specific endpoint', + }) + @ApiResponse({ + status: 200, + description: 'Endpoint rate limit status retrieved successfully', + }) + async getEndpointRateLimitStatus( + @Param('endpoint') endpoint: string, + ): Promise { + return this.rateLimitService.checkEndpointRateLimit(endpoint); + } + + @Delete('user/:userId/reset') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: 'Reset rate limit for a user', + description: 'Reset rate limit counter for a specific user', + }) + @ApiResponse({ + status: 204, + description: 'Rate limit reset successfully', + }) + async resetUserRateLimit(@Param('userId') userId: string): Promise { + return this.rateLimitService.resetUserRateLimit(userId); + } + + @Delete('ip/:ip/reset') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: 'Reset rate limit for an IP', + description: 'Reset rate limit counter for a specific IP address', + }) + @ApiResponse({ + status: 204, + description: 'IP rate limit reset successfully', + }) + async resetIpRateLimit(@Param('ip') ip: string): Promise { + return this.rateLimitService.resetIpRateLimit(ip); + } + + @Delete('endpoint/:endpoint/reset') + @HttpCode(HttpStatus.NO_CONTENT) + @SkipRateLimit() + @ApiOperation({ + summary: 'Reset rate limit for an endpoint', + description: 'Reset rate limit counter for a specific endpoint', + }) + @ApiResponse({ + status: 204, + description: 'Endpoint rate limit reset successfully', + }) + async resetEndpointRateLimit( + @Param('endpoint') endpoint: string, + ): Promise { + return this.rateLimitService.resetEndpointRateLimit(endpoint); + } + + @Delete('api-key/:apiKey/reset') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: 'Reset rate limit for an API key', + description: 'Reset rate limit counter for a specific API key', + }) + @ApiResponse({ + status: 204, + description: 'API key rate limit reset successfully', + }) + async resetApiKeyRateLimit(@Param('apiKey') apiKey: string): Promise { + return this.rateLimitService.resetApiKeyRateLimit(apiKey); + } + + @Get('summary') + @SkipRateLimit() + @ApiOperation({ + summary: 'Get rate limiting summary', + description: + 'Retrieve information about all configured rate limits and their current status', + }) + @ApiResponse({ + status: 200, + description: 'Rate limit summary retrieved successfully', + schema: { + example: { + globalLimit: 1000, + globalWindow: '15 minutes', + userTiers: { + free: '100 req/hour', + premium: '5000 req/hour', + enterprise: 'unlimited', + }, + endpointLimits: { + 'POST /auth/login': '5 req/15 minutes', + 'POST /auth/register': '5 req/hour', + 'GET /properties': '100 req/minute', + }, + }, + }, + }) + async getRateLimitSummary(): Promise { + return { + globalLimit: 1000, + globalWindow: '15 minutes', + globalWindowMs: 15 * 60 * 1000, + userTiers: { + free: { + hourlyLimit: 100, + monthlyLimit: 10000, + }, + premium: { + hourlyLimit: 5000, + monthlyLimit: 500000, + }, + enterprise: { + hourlyLimit: 50000, + monthlyLimit: 'unlimited', + }, + apiKey: { + hourlyLimit: 10000, + monthlyLimit: 1000000, + }, + }, + strictEndpoints: { + 'POST /auth/register': '5 requests per 1 hour', + 'POST /auth/login': '5 requests per 15 minutes', + 'POST /auth/request-password-reset': '3 requests per 1 hour', + }, + moderateEndpoints: { + 'POST /users': '10 requests per hour', + 'POST /properties': '20 requests per hour', + 'GET /users': '100 requests per minute', + 'GET /properties': '100 requests per minute', + }, + description: + 'Authentication and sensitive endpoints have strict limits. Standard endpoints have moderate limits. Unauthenticated requests are limited by IP.', + }; + } +} diff --git a/src/auth/decorators/rate-limit.decorator.ts b/src/auth/decorators/rate-limit.decorator.ts new file mode 100644 index 00000000..2ccb3eff --- /dev/null +++ b/src/auth/decorators/rate-limit.decorator.ts @@ -0,0 +1,73 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { RateLimitGuard, SkipRateLimit, CustomRateLimit } from '../guards/rate-limit.guard'; + +/** + * Decorator to apply rate limiting to a route + * Usage: @RateLimited() on controller methods + */ +export function RateLimited(options?: { + windowMs?: number; + max?: number; + by?: 'user' | 'ip' | 'apiKey'; +}) { + if (options) { + return applyDecorators( + UseGuards(RateLimitGuard), + CustomRateLimit(options), + ); + } + return UseGuards(RateLimitGuard); +} + +/** + * Decorator to disable rate limiting for a route + * Usage: @NoRateLimit() on controller methods + */ +export function NoRateLimit() { + return SkipRateLimit(); +} + +/** + * Decorator to enable strict rate limiting + * Usage: @StrictRateLimit() on controller methods (auth endpoints) + */ +export function StrictRateLimit() { + return applyDecorators( + UseGuards(RateLimitGuard), + CustomRateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // 5 requests + by: 'user', + }), + ); +} + +/** + * Decorator to enable moderate rate limiting + * Usage: @ModerateRateLimit() on controller methods + */ +export function ModerateRateLimit() { + return applyDecorators( + UseGuards(RateLimitGuard), + CustomRateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests + by: 'user', + }), + ); +} + +/** + * Decorator to enable loose rate limiting + * Usage: @LooseRateLimit() on controller methods + */ +export function LooseRateLimit() { + return applyDecorators( + UseGuards(RateLimitGuard), + CustomRateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 300, // 300 requests + by: 'user', + }), + ); +} diff --git a/src/auth/examples/rate-limit.examples.ts b/src/auth/examples/rate-limit.examples.ts new file mode 100644 index 00000000..7286a5e1 --- /dev/null +++ b/src/auth/examples/rate-limit.examples.ts @@ -0,0 +1,211 @@ +/** + * Rate Limiting Usage Examples + * + * This file demonstrates how to use the rate limiting features + * in the PropChain API + */ + +import { + Controller, + Get, + Post, + Body, + UseGuards, +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { + RateLimited, + StrictRateLimit, + ModerateRateLimit, + LooseRateLimit, + NoRateLimit, +} from '../decorators/rate-limit.decorator'; + +@ApiTags('Examples - Rate Limiting') +@Controller('examples') +export class RateLimitExamplesController { + /** + * Example 1: Strict rate limiting (authentication) + * - 5 requests per 15 minutes per user + * - Perfect for login, register, password reset + */ + @Post('strict-auth') + @StrictRateLimit() + @ApiOperation({ + summary: 'Example: Strict Rate Limited Endpoint', + description: '5 requests per 15 minutes', + }) + exampleStrictRateLimit() { + return { + message: 'This endpoint has strict rate limiting (5 req / 15 min)', + example: 'POST /auth/login', + }; + } + + /** + * Example 2: Moderate rate limiting (standard operations) + * - 100 requests per 1 minute per user + * - Perfect for standard CRUD operations + */ + @Get('moderate-crud') + @ModerateRateLimit() + @ApiOperation({ + summary: 'Example: Moderate Rate Limited Endpoint', + description: '100 requests per minute', + }) + exampleModerateRateLimit() { + return { + message: 'This endpoint has moderate rate limiting (100 req / min)', + example: 'GET /properties, POST /users', + }; + } + + /** + * Example 3: Loose rate limiting (bulk operations) + * - 300 requests per 1 minute per user + * - Perfect for high-volume operations + */ + @Get('loose-bulk') + @LooseRateLimit() + @ApiOperation({ + summary: 'Example: Loose Rate Limited Endpoint', + description: '300 requests per minute', + }) + exampleLooseRateLimit() { + return { + message: 'This endpoint has loose rate limiting (300 req / min)', + example: 'Bulk operations, data exports', + }; + } + + /** + * Example 4: Default rate limiting (global) + * - Applied automatically to all routes + * - User-based or IP-based depending on authentication + */ + @Get('default-limit') + @RateLimited() + @ApiOperation({ + summary: 'Example: Default Rate Limited Endpoint', + description: 'Default global rate limiting applied', + }) + exampleDefaultRateLimit() { + return { + message: 'This endpoint uses default rate limiting', + limit: '1000 requests per 15 minutes (global)', + }; + } + + /** + * Example 5: Custom rate limiting + * - Define specific window and max requests + */ + @Get('custom-limit') + @RateLimited({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 50, // 50 requests + by: 'user', + }) + @ApiOperation({ + summary: 'Example: Custom Rate Limited Endpoint', + description: '50 requests per hour', + }) + exampleCustomRateLimit() { + return { + message: 'This endpoint has custom rate limiting', + limit: '50 requests per 1 hour', + }; + } + + /** + * Example 6: No rate limiting (admin operations) + * - Exempt from rate limiting + * - Use with caution, only for admin operations + */ + @Get('no-limit') + @NoRateLimit() + @ApiOperation({ + summary: 'Example: No Rate Limiting', + description: 'This endpoint is exempt from rate limiting', + }) + exampleNoRateLimit() { + return { + message: 'This endpoint has no rate limiting', + warning: 'Use @NoRateLimit() only for admin operations', + }; + } + + /** + * Example 7: Rate limiting with JWT guard + * - Combine with authentication guards + */ + @Post('protected-limited') + @UseGuards(JwtAuthGuard) + @ModerateRateLimit() + @ApiOperation({ + summary: 'Example: Protected and Rate Limited Endpoint', + description: 'Requires JWT token, 100 requests per minute', + }) + exampleProtectedAndRateLimited() { + return { + message: 'This endpoint requires JWT and has rate limiting', + }; + } +} + +/** + * RATE LIMIT HEADERS IN RESPONSES + * + * All rate-limited endpoints include these headers: + * + * X-RateLimit-Limit: 100 // Max requests allowed in window + * X-RateLimit-Remaining: 99 // Requests remaining in window + * X-RateLimit-Reset: 1703088000 // Unix timestamp when limit resets + * + * On rate limit exceeded (429 response): + * Retry-After: 45 // Seconds to wait before retrying + */ + +/** + * RATE LIMIT STATUSES + * + * 1. User-based rate limiting (authenticated requests) + * - Free tier: 100 req/hour, 10k req/month + * - Premium tier: 5000 req/hour, 500k req/month + * - Enterprise tier: 50k req/hour, unlimited/month + * - API Key: 10k req/hour, 1M req/month + * + * 2. IP-based rate limiting (unauthenticated requests) + * - 1000 requests per 15 minutes per IP + * + * 3. Endpoint-specific rate limiting + * - Authentication: 5 req/15 min + * - User creation: 10 req/hour + * - Property creation: 20 req/hour + * - Data retrieval: 100 req/minute + */ + +/** + * ERROR RESPONSE (429 Too Many Requests) + * + * { + * "statusCode": 429, + * "message": "Rate limit exceeded. Max 100 requests per 15 minutes.", + * "retryAfter": 45, + * "timestamp": "2024-12-21T12:00:00Z", + * "path": "/api/v2/users" + * } + */ + +/** + * ADMIN ENDPOINTS FOR RATE LIMIT MANAGEMENT + * + * GET /admin/rate-limits/user/:userId - Get user rate limit status + * GET /admin/rate-limits/endpoint/:endpoint - Get endpoint rate limit status + * GET /admin/rate-limits/summary - Get all rate limits summary + * DELETE /admin/rate-limits/user/:userId/reset + * DELETE /admin/rate-limits/ip/:ip/reset + * DELETE /admin/rate-limits/endpoint/:endpoint/reset + * DELETE /admin/rate-limits/api-key/:apiKey/reset + */ diff --git a/src/auth/guards/rate-limit.guard.ts b/src/auth/guards/rate-limit.guard.ts new file mode 100644 index 00000000..5e0501b9 --- /dev/null +++ b/src/auth/guards/rate-limit.guard.ts @@ -0,0 +1,159 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Inject, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { RateLimitService } from '../rate-limit.service'; +import { RATE_LIMIT_HEADERS } from '../rate-limit.config'; + +export const RATE_LIMIT_SKIP_KEY = 'rate-limit-skip'; +export const RATE_LIMIT_CUSTOM_KEY = 'rate-limit-custom'; + +/** + * Decorator to skip rate limiting for a route + */ +export const SkipRateLimit = () => + Reflect.metadata(RATE_LIMIT_SKIP_KEY, true); + +/** + * Decorator to apply custom rate limiting + */ +export const CustomRateLimit = (options: { + windowMs?: number; + max?: number; + by?: 'user' | 'ip' | 'apiKey'; +}) => Reflect.metadata(RATE_LIMIT_CUSTOM_KEY, options); + +@Injectable() +export class RateLimitGuard implements CanActivate { + constructor( + private reflector: Reflector, + @Inject(RateLimitService) private rateLimitService: RateLimitService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // Check if rate limiting is skipped for this route + const skip = this.reflector.getAllAndOverride( + RATE_LIMIT_SKIP_KEY, + [context.getHandler(), context.getClass()], + ); + + if (skip) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const endpoint = `${request.method} ${request.route?.path || request.url}`; + + try { + // Check by user if authenticated + if (request.user?.id) { + const userTier = request.user.tier || 'free'; + const userStatus = await this.rateLimitService.checkUserRateLimit( + request.user.id, + userTier, + ); + + // Apply rate limit headers + Object.entries(this.rateLimitService.getHeaders(userStatus)).forEach( + ([key, value]) => { + response.setHeader(key, value); + }, + ); + + if (userStatus.isExceeded) { + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: `Rate limit exceeded. Max ${userStatus.limit} requests per 15 minutes.`, + retryAfter: userStatus.retryAfter, + }, + HttpStatus.TOO_MANY_REQUESTS, + { + cause: 'user_rate_limit_exceeded', + }, + ); + } + } else { + // Check by IP for unauthenticated requests + const ip = this.getClientIp(request); + const ipStatus = await this.rateLimitService.checkIpRateLimit(ip); + + // Apply rate limit headers + Object.entries(this.rateLimitService.getHeaders(ipStatus)).forEach( + ([key, value]) => { + response.setHeader(key, value); + }, + ); + + if (ipStatus.isExceeded) { + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: 'Too many requests from your IP. Please try again later.', + retryAfter: ipStatus.retryAfter, + }, + HttpStatus.TOO_MANY_REQUESTS, + { + cause: 'ip_rate_limit_exceeded', + }, + ); + } + } + + // Check endpoint-specific limits + const endpointStatus = await this.rateLimitService.checkEndpointRateLimit( + endpoint, + ); + + if (endpointStatus.limit > 0) { + Object.entries(this.rateLimitService.getHeaders(endpointStatus)).forEach( + ([key, value]) => { + response.setHeader(key, value); + }, + ); + + if (endpointStatus.isExceeded) { + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: `Too many requests to this endpoint. Please try again later.`, + retryAfter: endpointStatus.retryAfter, + }, + HttpStatus.TOO_MANY_REQUESTS, + { + cause: 'endpoint_rate_limit_exceeded', + }, + ); + } + } + + return true; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + // If rate limit check fails, allow the request + console.error('Rate limit check error:', error); + return true; + } + } + + /** + * Extract client IP from request + */ + private getClientIp(request: any): string { + return ( + request.headers['x-forwarded-for']?.split(',')[0].trim() || + request.connection?.remoteAddress || + request.socket?.remoteAddress || + request.ip || + 'unknown' + ); + } +} diff --git a/src/auth/interceptors/rate-limit-headers.interceptor.ts b/src/auth/interceptors/rate-limit-headers.interceptor.ts new file mode 100644 index 00000000..37ab3ca1 --- /dev/null +++ b/src/auth/interceptors/rate-limit-headers.interceptor.ts @@ -0,0 +1,38 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { RATE_LIMIT_HEADERS } from '../rate-limit.config'; + +/** + * Interceptor to add rate limit headers to response + * Provides visibility into rate limit status + */ +@Injectable() +export class RateLimitHeadersInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + + return next.handle().pipe( + tap(() => { + // Headers should already be set by the guard + // This interceptor ensures they persist through the response + const existingLimit = response.getHeader(RATE_LIMIT_HEADERS.LIMIT); + if (!existingLimit) { + // If no limit header was set, set defaults + response.setHeader(RATE_LIMIT_HEADERS.LIMIT, '1000'); + response.setHeader(RATE_LIMIT_HEADERS.REMAINING, '999'); + response.setHeader( + RATE_LIMIT_HEADERS.RESET, + Math.floor((Date.now() + 15 * 60 * 1000) / 1000), + ); + } + }), + ); + } +} diff --git a/src/auth/modules/rate-limit.module.ts b/src/auth/modules/rate-limit.module.ts new file mode 100644 index 00000000..c054eaf4 --- /dev/null +++ b/src/auth/modules/rate-limit.module.ts @@ -0,0 +1,17 @@ +import { Global, Module } from '@nestjs/common'; +import { RateLimitService } from '../rate-limit.service'; +import { RateLimitGuard } from '../guards/rate-limit.guard'; +import { RateLimitAdminController } from '../controllers/rate-limit-admin.controller'; +import { RateLimitHeadersInterceptor } from '../interceptors/rate-limit-headers.interceptor'; + +@Global() +@Module({ + providers: [RateLimitService, RateLimitGuard, RateLimitHeadersInterceptor], + controllers: [RateLimitAdminController], + exports: [ + RateLimitService, + RateLimitGuard, + RateLimitHeadersInterceptor, + ], +}) +export class RateLimitModule {} diff --git a/src/auth/rate-limit.config.ts b/src/auth/rate-limit.config.ts new file mode 100644 index 00000000..1483dc3a --- /dev/null +++ b/src/auth/rate-limit.config.ts @@ -0,0 +1,177 @@ +/** + * Rate Limiting Configuration + * Defines rate limiting strategies and constants + */ + +export interface RateLimitConfig { + windowMs: number; // Time window in milliseconds + max: number; // Maximum requests per window + message?: string; + statusCode?: number; +} + +export interface RateLimitOptions { + windowMs: number; + max: number; + keyGenerator?: (req: any) => string; + skip?: (req: any) => boolean; + handler?: (req: any, res: any) => void; +} + +/** + * Global rate limiting configuration + */ +export const RATE_LIMIT_CONFIG: RateLimitConfig = { + windowMs: 15 * 60 * 1000, // 15 minutes + max: 1000, // 1000 requests per 15 minutes + statusCode: 429, + message: 'Too many requests from this IP, please try again later.', +}; + +/** + * Per-endpoint rate limiting configurations + */ +export const ENDPOINT_RATE_LIMITS: Record = { + // Authentication endpoints (strict) + 'POST /auth/register': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 5, // 5 requests per hour + }, + 'POST /auth/login': { + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // 5 attempts per 15 minutes + }, + 'POST /auth/refresh': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 20, // 20 refreshes per hour + }, + 'POST /auth/request-password-reset': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 3, // 3 requests per hour + }, + + // Email verification (moderate) + 'POST /email-verification/send': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 5, // 5 verification emails per hour + }, + 'POST /email-verification/verify': { + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // 10 verification attempts + }, + + // User endpoints (moderate) + 'GET /users': { + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + }, + 'POST /users': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 10, // 10 user creations per hour + }, + + // Property endpoints (moderate) + 'GET /properties': { + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + }, + 'POST /properties': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 20, // 20 property creations per hour + }, + + // Dashboard (loose) + 'GET /dashboard': { + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + }, + + // API Key endpoints + 'POST /auth/api-keys': { + windowMs: 60 * 60 * 1000, // 1 hour + max: 10, // 10 key creations per hour + }, +}; + +/** + * Per-user rate limiting configurations + */ +export const USER_TIER_RATE_LIMITS = { + // Free tier + free: { + windowMs: 60 * 60 * 1000, // 1 hour + max: 100, // 100 requests per hour + monthlyLimit: 10000, // 10k requests per month + }, + + // Premium tier + premium: { + windowMs: 60 * 60 * 1000, // 1 hour + max: 5000, // 5000 requests per hour + monthlyLimit: 500000, // 500k requests per month + }, + + // Enterprise tier + enterprise: { + windowMs: 60 * 60 * 1000, // 1 hour + max: 50000, // 50000 requests per hour + monthlyLimit: Infinity, // Unlimited + }, + + // API Key special tier + apiKey: { + windowMs: 60 * 60 * 1000, // 1 hour + max: 10000, // 10000 requests per hour + monthlyLimit: 1000000, // 1M requests per month + }, +}; + +/** + * Rate limit header names + */ +export const RATE_LIMIT_HEADERS = { + LIMIT: 'X-RateLimit-Limit', + REMAINING: 'X-RateLimit-Remaining', + RESET: 'X-RateLimit-Reset', + RETRY_AFTER: 'Retry-After', +}; + +/** + * Rate limit key prefixes for Redis + */ +export const RATE_LIMIT_KEYS = { + GLOBAL: 'rate-limit:global', + ENDPOINT: (endpoint: string) => `rate-limit:endpoint:${endpoint}`, + USER: (userId: string) => `rate-limit:user:${userId}`, + IP: (ip: string) => `rate-limit:ip:${ip}`, + API_KEY: (apiKey: string) => `rate-limit:api-key:${apiKey}`, +}; + +/** + * Get rate limit config for user tier + */ +export function getUserTierRateLimit( + tier: 'free' | 'premium' | 'enterprise' | 'apiKey' = 'free', +): RateLimitConfig { + const tierConfig = USER_TIER_RATE_LIMITS[tier]; + return { + windowMs: tierConfig.windowMs, + max: tierConfig.max, + statusCode: 429, + message: `Rate limit exceeded for ${tier} tier. Max ${tierConfig.max} requests per ${tierConfig.windowMs / 1000} seconds.`, + }; +} + +/** + * Get rate limit config for endpoint + */ +export function getEndpointRateLimit(endpoint: string): RateLimitConfig | null { + const config = ENDPOINT_RATE_LIMITS[endpoint]; + if (!config) return null; + + return { + ...config, + statusCode: 429, + message: `Too many requests to ${endpoint}. Please try again later.`, + }; +} diff --git a/src/auth/rate-limit.service.ts b/src/auth/rate-limit.service.ts new file mode 100644 index 00000000..f99993c8 --- /dev/null +++ b/src/auth/rate-limit.service.ts @@ -0,0 +1,209 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { + RATE_LIMIT_KEYS, + RATE_LIMIT_HEADERS, + getEndpointRateLimit, + getUserTierRateLimit, +} from './rate-limit.config'; + +export interface RateLimitStatus { + limit: number; + remaining: number; + reset: number; + retryAfter?: number; + isExceeded: boolean; +} + +export interface RateLimitRecord { + count: number; + resetAt: number; + windowMs: number; +} + +@Injectable() +export class RateLimitService { + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + /** + * Check rate limit for a user + */ + async checkUserRateLimit( + userId: string, + userTier: 'free' | 'premium' | 'enterprise' | 'apiKey' = 'free', + ): Promise { + const key = RATE_LIMIT_KEYS.USER(userId); + const config = getUserTierRateLimit(userTier); + + return this.checkRateLimit(key, config.max, config.windowMs); + } + + /** + * Check rate limit for an IP + */ + async checkIpRateLimit(ip: string): Promise { + const key = RATE_LIMIT_KEYS.IP(ip); + const limit = 1000; // Global IP limit + const windowMs = 15 * 60 * 1000; // 15 minutes + + return this.checkRateLimit(key, limit, windowMs); + } + + /** + * Check rate limit for an endpoint + */ + async checkEndpointRateLimit(endpoint: string): Promise { + const config = getEndpointRateLimit(endpoint); + if (!config) { + // No specific endpoint limit + return { + limit: 0, + remaining: 0, + reset: 0, + isExceeded: false, + }; + } + + const key = RATE_LIMIT_KEYS.ENDPOINT(endpoint); + return this.checkRateLimit(key, config.max, config.windowMs); + } + + /** + * Check rate limit for an API key + */ + async checkApiKeyRateLimit(apiKey: string): Promise { + const key = RATE_LIMIT_KEYS.API_KEY(apiKey); + const config = getUserTierRateLimit('apiKey'); + + return this.checkRateLimit(key, config.max, config.windowMs); + } + + /** + * Generic rate limit check + */ + private async checkRateLimit( + key: string, + limit: number, + windowMs: number, + ): Promise { + try { + const record = await this.cacheManager.get(key); + const now = Date.now(); + + let count = 1; + let resetAt = now + windowMs; + + if (record && record.resetAt > now) { + // Window still active + count = record.count + 1; + resetAt = record.resetAt; + } else { + // New window + resetAt = now + windowMs; + } + + const isExceeded = count > limit; + + // Store the updated record + if (!isExceeded) { + await this.cacheManager.set( + key, + { count, resetAt, windowMs }, + resetAt - now, + ); + } else { + // Still store the count for tracking + await this.cacheManager.set( + key, + { count, resetAt, windowMs }, + resetAt - now, + ); + } + + const remaining = Math.max(0, limit - count); + const retryAfter = isExceeded ? Math.ceil((resetAt - now) / 1000) : undefined; + + return { + limit, + remaining, + reset: Math.floor(resetAt / 1000), // Unix timestamp in seconds + retryAfter, + isExceeded, + }; + } catch (error) { + // If cache is unavailable, allow the request + console.error('Rate limit check failed:', error); + return { + limit, + remaining: limit - 1, + reset: Math.floor((Date.now() + windowMs) / 1000), + isExceeded: false, + }; + } + } + + /** + * Reset rate limit for a user + */ + async resetUserRateLimit(userId: string): Promise { + const key = RATE_LIMIT_KEYS.USER(userId); + await this.cacheManager.del(key); + } + + /** + * Reset rate limit for an IP + */ + async resetIpRateLimit(ip: string): Promise { + const key = RATE_LIMIT_KEYS.IP(ip); + await this.cacheManager.del(key); + } + + /** + * Reset rate limit for an endpoint + */ + async resetEndpointRateLimit(endpoint: string): Promise { + const key = RATE_LIMIT_KEYS.ENDPOINT(endpoint); + await this.cacheManager.del(key); + } + + /** + * Reset rate limit for an API key + */ + async resetApiKeyRateLimit(apiKey: string): Promise { + const key = RATE_LIMIT_KEYS.API_KEY(apiKey); + await this.cacheManager.del(key); + } + + /** + * Get rate limit status with headers + */ + getHeaders(status: RateLimitStatus): Record { + const headers: Record = { + [RATE_LIMIT_HEADERS.LIMIT]: status.limit.toString(), + [RATE_LIMIT_HEADERS.REMAINING]: status.remaining.toString(), + [RATE_LIMIT_HEADERS.RESET]: status.reset.toString(), + }; + + if (status.retryAfter) { + headers[RATE_LIMIT_HEADERS.RETRY_AFTER] = status.retryAfter.toString(); + } + + return headers; + } + + /** + * Get all rate limit stats for a user + */ + async getUserRateLimitStats(userId: string): Promise<{ + ip?: RateLimitStatus; + user?: RateLimitStatus; + reset?: Date; + }> { + const userLimit = await this.checkUserRateLimit(userId); + return { + user: userLimit, + reset: new Date(userLimit.reset * 1000), + }; + } +} diff --git a/src/main.ts b/src/main.ts index aaf9e0d7..a7cb98ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,14 @@ import { NestFactory } from '@nestjs/core'; import { Logger, ValidationPipe } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { VersionHeaderInterceptor } from './versioning/version-header.interceptor'; import { DeprecationWarningInterceptor } from './versioning/deprecation-warning.interceptor'; import { CacheMetricsInterceptor } from './cache/cache-metrics.interceptor'; import { CacheMonitoringService } from './cache/cache-monitoring.service'; +import { RateLimitGuard } from './auth/guards/rate-limit.guard'; +import { RateLimitService } from './auth/rate-limit.service'; +import { RateLimitHeadersInterceptor } from './auth/interceptors/rate-limit-headers.interceptor'; import { setupSwagger } from './config/swagger.config'; async function bootstrap() { @@ -26,11 +30,21 @@ async function bootstrap() { // Global prefix app.setGlobalPrefix('api'); + // Get services for guard initialization + const reflector = app.get(Reflector); + const rateLimitService = app.get(RateLimitService); + + // Apply global guards + app.useGlobalGuards(new RateLimitGuard(reflector, rateLimitService)); + // Apply version header interceptor globally app.useGlobalInterceptors(new VersionHeaderInterceptor()); // Apply deprecation warning interceptor - app.useGlobalInterceptors(new DeprecationWarningInterceptor(app.get('Reflector'))); + app.useGlobalInterceptors(new DeprecationWarningInterceptor(reflector)); + + // Apply rate limit headers interceptor + app.useGlobalInterceptors(new RateLimitHeadersInterceptor()); // Apply cache metrics interceptor const cacheMonitoringService = app.get(CacheMonitoringService); @@ -46,5 +60,7 @@ async function bootstrap() { logger.log(`📚 Swagger UI available at http://localhost:${port}/api/docs`); logger.log(`📋 OpenAPI spec available at http://localhost:${port}/api/openapi.json`); logger.log(`💾 Redis Caching enabled`); + logger.log(`🛡️ Rate Limiting enabled (per-user, per-endpoint, IP-based)`); } + bootstrap();