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