From 267ba86b37dc50f973617265c107e867b8931340 Mon Sep 17 00:00:00 2001 From: Divine Ifediorah Date: Sat, 21 Feb 2026 10:19:52 +0100 Subject: [PATCH] Rate limit --- src/app.module.ts | 2 + .../dto/create-rate-limiting.dto.ts | 1 + .../dto/update-rate-limiting.dto.ts | 4 ++ .../entities/rate-limiting.entity.ts | 1 + .../rate-limiting.controller.spec.ts | 20 ++++++++++ src/rate-limiting/rate-limiting.controller.ts | 38 +++++++++++++++++++ src/rate-limiting/rate-limiting.service.ts | 37 ++++++++++++++++++ .../adaptive-rate-limiting.service.ts | 21 ++++++++++ .../services/distrubutes.service.ts | 34 +++++++++++++++++ .../services/limit-guard/guard.ts | 22 +++++++++++ src/rate-limiting/services/quota.service.ts | 23 +++++++++++ .../services/rate-limiting.module.ts | 18 +++++++++ .../services/throttling.service.ts | 30 +++++++++++++++ 13 files changed, 251 insertions(+) create mode 100644 src/rate-limiting/dto/create-rate-limiting.dto.ts create mode 100644 src/rate-limiting/dto/update-rate-limiting.dto.ts create mode 100644 src/rate-limiting/entities/rate-limiting.entity.ts create mode 100644 src/rate-limiting/rate-limiting.controller.spec.ts create mode 100644 src/rate-limiting/rate-limiting.controller.ts create mode 100644 src/rate-limiting/rate-limiting.service.ts create mode 100644 src/rate-limiting/services/adaptive-rate-limiting.service.ts create mode 100644 src/rate-limiting/services/distrubutes.service.ts create mode 100644 src/rate-limiting/services/limit-guard/guard.ts create mode 100644 src/rate-limiting/services/quota.service.ts create mode 100644 src/rate-limiting/services/rate-limiting.module.ts create mode 100644 src/rate-limiting/services/throttling.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 86c049e..7fa77a4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,6 +24,7 @@ import { ObservabilityModule } from './observability/observability.module'; import { BullModule } from '@nestjs/bull'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { CacheModule } from '@nestjs/cache-manager'; +import { RateLimitingModule } from './rate-limiting/services/rate-limiting.module'; import * as redisStore from 'cache-manager-redis-store'; @Module({ @@ -72,6 +73,7 @@ import * as redisStore from 'cache-manager-redis-store'; MigrationModule, ABTestingModule, ObservabilityModule, + RateLimitingModule, ], controllers: [AppController], providers: [ diff --git a/src/rate-limiting/dto/create-rate-limiting.dto.ts b/src/rate-limiting/dto/create-rate-limiting.dto.ts new file mode 100644 index 0000000..f201bda --- /dev/null +++ b/src/rate-limiting/dto/create-rate-limiting.dto.ts @@ -0,0 +1 @@ +export class CreateRateLimitingDto {} diff --git a/src/rate-limiting/dto/update-rate-limiting.dto.ts b/src/rate-limiting/dto/update-rate-limiting.dto.ts new file mode 100644 index 0000000..e9d61f0 --- /dev/null +++ b/src/rate-limiting/dto/update-rate-limiting.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRateLimitingDto } from './create-rate-limiting.dto'; + +export class UpdateRateLimitingDto extends PartialType(CreateRateLimitingDto) {} diff --git a/src/rate-limiting/entities/rate-limiting.entity.ts b/src/rate-limiting/entities/rate-limiting.entity.ts new file mode 100644 index 0000000..b5bd6b8 --- /dev/null +++ b/src/rate-limiting/entities/rate-limiting.entity.ts @@ -0,0 +1 @@ +export class RateLimiting {} diff --git a/src/rate-limiting/rate-limiting.controller.spec.ts b/src/rate-limiting/rate-limiting.controller.spec.ts new file mode 100644 index 0000000..77dc853 --- /dev/null +++ b/src/rate-limiting/rate-limiting.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RateLimitingController } from './rate-limiting.controller'; +import { RateLimitingService } from './rate-limiting.service'; + +describe('RateLimitingController', () => { + let controller: RateLimitingController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RateLimitingController], + providers: [RateLimitingService], + }).compile(); + + controller = module.get(RateLimitingController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/rate-limiting/rate-limiting.controller.ts b/src/rate-limiting/rate-limiting.controller.ts new file mode 100644 index 0000000..5ffe319 --- /dev/null +++ b/src/rate-limiting/rate-limiting.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { RateLimitingService } from './rate-limiting.service'; +import { RateLimitGuard } from './services/limit-guard/guard'; +import { CreateRateLimitingDto } from './dto/create-rate-limiting.dto'; +import { UpdateRateLimitingDto } from './dto/update-rate-limiting.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; + +@Controller('rate-limiting') +export class RateLimitingController { + constructor(private readonly rateLimitingService: RateLimitingService) {} + + + @Post() + create(@Body() createRateLimitingDto: CreateRateLimitingDto) { + return this.rateLimitingService.create(createRateLimitingDto); + } + + @UseGuards(JwtAuthGuard, RateLimitGuard) + @Get() + findAll() { + return this.rateLimitingService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.rateLimitingService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateRateLimitingDto: UpdateRateLimitingDto) { + return this.rateLimitingService.update(+id, updateRateLimitingDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.rateLimitingService.remove(+id); + } +} diff --git a/src/rate-limiting/rate-limiting.service.ts b/src/rate-limiting/rate-limiting.service.ts new file mode 100644 index 0000000..7e7e50e --- /dev/null +++ b/src/rate-limiting/rate-limiting.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { ThrottlingService } from './services/throttling.service'; +import { UserTier } from './services/quota.service'; +import { CreateRateLimitingDto } from './dto/create-rate-limiting.dto'; +import { UpdateRateLimitingDto } from './dto/update-rate-limiting.dto'; + +@Injectable() +export class RateLimitingService { + create(createRateLimitingDto: CreateRateLimitingDto) { + throw new Error('Method not implemented.'); + } + findAll() { + throw new Error('Method not implemented.'); + } + findOne(arg0: number) { + throw new Error('Method not implemented.'); + } + update(arg0: number, updateRateLimitingDto: UpdateRateLimitingDto) { + throw new Error('Method not implemented.'); + } + remove(arg0: number) { + throw new Error('Method not implemented.'); + } + constructor(private readonly throttlingService: ThrottlingService) {} + + async protect( + userId: string, + tier: UserTier, + endpoint: string, + ) { + await this.throttlingService.handleRequest( + userId, + tier, + endpoint, + ); + } +} \ No newline at end of file diff --git a/src/rate-limiting/services/adaptive-rate-limiting.service.ts b/src/rate-limiting/services/adaptive-rate-limiting.service.ts new file mode 100644 index 0000000..e3aa91a --- /dev/null +++ b/src/rate-limiting/services/adaptive-rate-limiting.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import * as os from 'os'; + +@Injectable() +export class AdaptiveRateLimitingService { + getSystemLoadFactor(): number { + const load = os.loadavg()[0]; // 1-minute average + const cpuCount = os.cpus().length; + + const loadPercentage = load / cpuCount; + + if (loadPercentage > 0.9) return 0.5; // reduce limits by 50% + if (loadPercentage > 0.7) return 0.7; + return 1; + } + + adjustLimit(baseLimit: number): number { + const factor = this.getSystemLoadFactor(); + return Math.floor(baseLimit * factor); + } +} \ No newline at end of file diff --git a/src/rate-limiting/services/distrubutes.service.ts b/src/rate-limiting/services/distrubutes.service.ts new file mode 100644 index 0000000..e6ced2f --- /dev/null +++ b/src/rate-limiting/services/distrubutes.service.ts @@ -0,0 +1,34 @@ +import { Injectable, ForbiddenException } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class DistributedLimiterService { + private redis: Redis; + + constructor() { + this.redis = new Redis(process.env.REDIS_URL); + } + + async slidingWindowCheck( + key: string, + limit: number, + windowInSeconds: number, + ): Promise { + const now = Date.now(); + const windowStart = now - windowInSeconds * 1000; + + const pipeline = this.redis.pipeline(); + + pipeline.zremrangebyscore(key, 0, windowStart); + pipeline.zadd(key, now, `${now}`); + pipeline.zcard(key); + pipeline.expire(key, windowInSeconds); + + const results = await pipeline.exec(); + const requestCount = results?.[2]?.[1] as number; + + if (requestCount > limit) { + throw new ForbiddenException('Rate limit exceeded'); + } + } +} \ No newline at end of file diff --git a/src/rate-limiting/services/limit-guard/guard.ts b/src/rate-limiting/services/limit-guard/guard.ts new file mode 100644 index 0000000..bda245f --- /dev/null +++ b/src/rate-limiting/services/limit-guard/guard.ts @@ -0,0 +1,22 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; +import { RateLimitingService } from "src/rate-limiting/rate-limiting.service"; + +@Injectable() +export class RateLimitGuard implements CanActivate { + constructor(private rateLimiting: RateLimitingService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + const user = req.user; + const endpoint = req.route.path; + + await this.rateLimiting.protect( + user.id, + user.tier, + endpoint, + ); + + return true; + } +} \ No newline at end of file diff --git a/src/rate-limiting/services/quota.service.ts b/src/rate-limiting/services/quota.service.ts new file mode 100644 index 0000000..1d54d06 --- /dev/null +++ b/src/rate-limiting/services/quota.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +export enum UserTier { + FREE = 'FREE', + PRO = 'PRO', + PREMIUM = 'PREMIUM', +} + +@Injectable() +export class QuotaManagementService { + getQuotaForTier(tier: UserTier) { + switch (tier) { + case UserTier.FREE: + return { limit: 100, window: 60 }; + case UserTier.PRO: + return { limit: 500, window: 60 }; + case UserTier.PREMIUM: + return { limit: Infinity, window: 60 }; + default: + return { limit: 50, window: 60 }; + } + } +} \ No newline at end of file diff --git a/src/rate-limiting/services/rate-limiting.module.ts b/src/rate-limiting/services/rate-limiting.module.ts new file mode 100644 index 0000000..87cc306 --- /dev/null +++ b/src/rate-limiting/services/rate-limiting.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { RateLimitingService } from '../rate-limiting.service'; +import { ThrottlingService } from './throttling.service'; +import { QuotaManagementService } from './quota.service'; +import { AdaptiveRateLimitingService } from './adaptive-rate-limiting.service'; +import { DistributedLimiterService } from './distrubutes.service'; + +@Module({ + providers: [ + RateLimitingService, + ThrottlingService, + QuotaManagementService, + AdaptiveRateLimitingService, + DistributedLimiterService, + ], + exports: [RateLimitingService], +}) +export class RateLimitingModule {} \ No newline at end of file diff --git a/src/rate-limiting/services/throttling.service.ts b/src/rate-limiting/services/throttling.service.ts new file mode 100644 index 0000000..7f2228a --- /dev/null +++ b/src/rate-limiting/services/throttling.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { DistributedLimiterService } from './distrubutes.service'; +import { QuotaManagementService, UserTier } from './quota.service'; +import { AdaptiveRateLimitingService } from './adaptive-rate-limiting.service'; + +@Injectable() +export class ThrottlingService { + constructor( + private readonly distributedLimiter: DistributedLimiterService, + private readonly quotaService: QuotaManagementService, + private readonly adaptiveService: AdaptiveRateLimitingService, + ) {} + + async handleRequest(userId: string, tier: UserTier, endpoint: string) { + if (tier === UserTier.PREMIUM) { + return; // bypass + } + + const { limit, window } = this.quotaService.getQuotaForTier(tier); + const adjustedLimit = this.adaptiveService.adjustLimit(limit); + + const key = `rate:${userId}:${endpoint}`; + + await this.distributedLimiter.slidingWindowCheck( + key, + adjustedLimit, + window, + ); + } +} \ No newline at end of file