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: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -72,6 +73,7 @@ import * as redisStore from 'cache-manager-redis-store';
MigrationModule,
ABTestingModule,
ObservabilityModule,
RateLimitingModule,
],
controllers: [AppController],
providers: [
Expand Down
1 change: 1 addition & 0 deletions src/rate-limiting/dto/create-rate-limiting.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class CreateRateLimitingDto {}
4 changes: 4 additions & 0 deletions src/rate-limiting/dto/update-rate-limiting.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateRateLimitingDto } from './create-rate-limiting.dto';

export class UpdateRateLimitingDto extends PartialType(CreateRateLimitingDto) {}
1 change: 1 addition & 0 deletions src/rate-limiting/entities/rate-limiting.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class RateLimiting {}
20 changes: 20 additions & 0 deletions src/rate-limiting/rate-limiting.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(RateLimitingController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
38 changes: 38 additions & 0 deletions src/rate-limiting/rate-limiting.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
37 changes: 37 additions & 0 deletions src/rate-limiting/rate-limiting.service.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
21 changes: 21 additions & 0 deletions src/rate-limiting/services/adaptive-rate-limiting.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
34 changes: 34 additions & 0 deletions src/rate-limiting/services/distrubutes.service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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');
}
}
}
22 changes: 22 additions & 0 deletions src/rate-limiting/services/limit-guard/guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}
}
23 changes: 23 additions & 0 deletions src/rate-limiting/services/quota.service.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
}
18 changes: 18 additions & 0 deletions src/rate-limiting/services/rate-limiting.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
30 changes: 30 additions & 0 deletions src/rate-limiting/services/throttling.service.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}