banatie-service/apps/api-service/src/middleware/auth/rateLimiter.ts

93 lines
2.5 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
/**
* Simple in-memory rate limiter
* Tracks requests per API key
*/
class RateLimiter {
private requests: Map<string, { count: number; resetAt: number }> = new Map();
private readonly limit: number;
private readonly windowMs: number;
constructor(limit: number = 100, windowMs: number = 60 * 60 * 1000) {
this.limit = limit;
this.windowMs = windowMs;
// Cleanup old entries every 5 minutes
setInterval(() => this.cleanup(), 5 * 60 * 1000);
}
check(keyId: string): { allowed: boolean; remaining: number; resetAt: number } {
const now = Date.now();
const record = this.requests.get(keyId);
if (!record || record.resetAt < now) {
// Create new window
const resetAt = now + this.windowMs;
this.requests.set(keyId, { count: 1, resetAt });
return { allowed: true, remaining: this.limit - 1, resetAt };
}
if (record.count >= this.limit) {
return { allowed: false, remaining: 0, resetAt: record.resetAt };
}
// Increment counter
record.count++;
return {
allowed: true,
remaining: this.limit - record.count,
resetAt: record.resetAt,
};
}
private cleanup() {
const now = Date.now();
for (const [keyId, record] of this.requests.entries()) {
if (record.resetAt < now) {
this.requests.delete(keyId);
}
}
}
}
const rateLimiter = new RateLimiter(100, 60 * 60 * 1000); // 100 requests per hour
/**
* Rate limiting middleware
* Must be used AFTER validateApiKey middleware
*/
export function rateLimitByApiKey(req: Request, res: Response, next: NextFunction): void {
if (!req.apiKey) {
next();
return;
}
const result = rateLimiter.check(req.apiKey.id);
// Add rate limit headers
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
res.setHeader('X-RateLimit-Reset', new Date(result.resetAt).toISOString());
if (!result.allowed) {
const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1000);
console.warn(
`[${new Date().toISOString()}] Rate limit exceeded: ${req.apiKey.id} (${req.apiKey.keyType}) - reset: ${new Date(result.resetAt).toISOString()}`,
);
res
.status(429)
.setHeader('Retry-After', retryAfter.toString())
.json({
error: 'Rate limit exceeded',
message: `Too many requests. Retry after ${retryAfter} seconds`,
retryAfter,
});
return;
}
next();
}