93 lines
2.5 KiB
TypeScript
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();
|
|
}
|