feat: implement rate limiting system
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
import { RateLimiter, RateLimitResult } from './rate-limiter';
|
||||||
|
|
||||||
|
describe('Rate Limiting System', () => {
|
||||||
|
let rateLimiter: RateLimiter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rateLimiter = new RateLimiter();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow request within limit', async () => {
|
||||||
|
const result = await rateLimiter.checkIP('192.168.1.1');
|
||||||
|
expect(result.allowed).toBe(true);
|
||||||
|
expect(result.remaining).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should block request when limit exceeded', async () => {
|
||||||
|
const ip = '192.168.1.2';
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await rateLimiter.checkIP(ip);
|
||||||
|
}
|
||||||
|
const result = await rateLimiter.checkIP(ip);
|
||||||
|
expect(result.allowed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reset limit after window expires', async () => {
|
||||||
|
const ip = '192.168.1.3';
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await rateLimiter.checkIP(ip);
|
||||||
|
}
|
||||||
|
await rateLimiter.checkIP(ip);
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.advanceTimersByTime(61 * 60 * 1000);
|
||||||
|
|
||||||
|
const result = await rateLimiter.checkIP(ip);
|
||||||
|
expect(result.allowed).toBe(true);
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should track email rate limits separately', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await rateLimiter.checkEmail(email);
|
||||||
|
}
|
||||||
|
const result = await rateLimiter.checkEmail(email);
|
||||||
|
expect(result.allowed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should enforce global rate limit', async () => {
|
||||||
|
const promises = [];
|
||||||
|
for (let i = 0; i < 51; i++) {
|
||||||
|
promises.push(rateLimiter.checkGlobal());
|
||||||
|
}
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
const blockedCount = results.filter(r => !r.allowed).length;
|
||||||
|
expect(blockedCount).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { getSecurityConfig } from './config';
|
||||||
|
|
||||||
|
export interface RateLimitResult {
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
resetTime: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestRecord {
|
||||||
|
count: number;
|
||||||
|
windowStart: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InMemoryRateLimiter {
|
||||||
|
private ipRequests = new Map<string, RequestRecord>();
|
||||||
|
private emailRequests = new Map<string, RequestRecord>();
|
||||||
|
private globalRequests: RequestRecord = { count: 0, windowStart: Date.now() };
|
||||||
|
|
||||||
|
async checkIP(ip: string): Promise<RateLimitResult> {
|
||||||
|
const config = getSecurityConfig();
|
||||||
|
const now = Date.now();
|
||||||
|
const windowMs = config.rateLimit.ip.windowMinutes * 60 * 1000;
|
||||||
|
|
||||||
|
let record = this.ipRequests.get(ip);
|
||||||
|
if (!record || now - record.windowStart > windowMs) {
|
||||||
|
record = { count: 0, windowStart: now };
|
||||||
|
this.ipRequests.set(ip, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = record.count < config.rateLimit.ip.maxRequests;
|
||||||
|
if (allowed) {
|
||||||
|
record.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed,
|
||||||
|
remaining: Math.max(0, config.rateLimit.ip.maxRequests - record.count),
|
||||||
|
resetTime: record.windowStart + windowMs,
|
||||||
|
limit: config.rateLimit.ip.maxRequests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkEmail(email: string): Promise<RateLimitResult> {
|
||||||
|
const config = getSecurityConfig();
|
||||||
|
const now = Date.now();
|
||||||
|
const windowMs = config.rateLimit.email.windowHours * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
let record = this.emailRequests.get(email);
|
||||||
|
if (!record || now - record.windowStart > windowMs) {
|
||||||
|
record = { count: 0, windowStart: now };
|
||||||
|
this.emailRequests.set(email, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = record.count < config.rateLimit.email.maxRequests;
|
||||||
|
if (allowed) {
|
||||||
|
record.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed,
|
||||||
|
remaining: Math.max(0, config.rateLimit.email.maxRequests - record.count),
|
||||||
|
resetTime: record.windowStart + windowMs,
|
||||||
|
limit: config.rateLimit.email.maxRequests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkGlobal(): Promise<RateLimitResult> {
|
||||||
|
const config = getSecurityConfig();
|
||||||
|
const now = Date.now();
|
||||||
|
const windowMs = config.rateLimit.global.windowMinutes * 60 * 1000;
|
||||||
|
|
||||||
|
if (now - this.globalRequests.windowStart > windowMs) {
|
||||||
|
this.globalRequests = { count: 0, windowStart: now };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = this.globalRequests.count < config.rateLimit.global.maxRequests;
|
||||||
|
if (allowed) {
|
||||||
|
this.globalRequests.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed,
|
||||||
|
remaining: Math.max(0, config.rateLimit.global.maxRequests - this.globalRequests.count),
|
||||||
|
resetTime: this.globalRequests.windowStart + windowMs,
|
||||||
|
limit: config.rateLimit.global.maxRequests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.ipRequests.clear();
|
||||||
|
this.emailRequests.clear();
|
||||||
|
this.globalRequests = { count: 0, windowStart: Date.now() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RateLimiter {
|
||||||
|
private limiter: InMemoryRateLimiter;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.limiter = new InMemoryRateLimiter();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIP(ip: string): Promise<RateLimitResult> {
|
||||||
|
return this.limiter.checkIP(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkEmail(email: string): Promise<RateLimitResult> {
|
||||||
|
return this.limiter.checkEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkGlobal(): Promise<RateLimitResult> {
|
||||||
|
return this.limiter.checkGlobal();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.limiter.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user