75 lines
No EOL
2.2 KiB
TypeScript
75 lines
No EOL
2.2 KiB
TypeScript
import { Context, Next } from 'hono';
|
|
import Redis from 'ioredis';
|
|
import { config } from '../config';
|
|
import { logger } from '../utils/logger';
|
|
import { getWorkerConfig } from '@wcag-ada/config';
|
|
|
|
const workerConfig = getWorkerConfig();
|
|
const redis = new Redis({
|
|
host: workerConfig.redis.host,
|
|
port: workerConfig.redis.port,
|
|
password: workerConfig.redis.password,
|
|
db: workerConfig.redis.db,
|
|
});
|
|
|
|
interface RateLimitOptions {
|
|
limit?: number;
|
|
window?: number; // in milliseconds
|
|
keyGenerator?: (c: Context) => string;
|
|
}
|
|
|
|
export const rateLimiter = (options: RateLimitOptions = {}) => {
|
|
const {
|
|
limit = config.API_RATE_LIMIT,
|
|
window = config.API_RATE_WINDOW,
|
|
keyGenerator = (c) => {
|
|
const userId = c.get('userId');
|
|
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown';
|
|
return userId ? `rate:user:${userId}` : `rate:ip:${ip}`;
|
|
},
|
|
} = options;
|
|
|
|
return async (c: Context, next: Next) => {
|
|
const key = keyGenerator(c);
|
|
|
|
try {
|
|
// Get current count
|
|
const current = await redis.get(key);
|
|
const count = current ? parseInt(current) : 0;
|
|
|
|
// Check if limit exceeded
|
|
if (count >= limit) {
|
|
const ttl = await redis.pttl(key);
|
|
|
|
c.header('X-RateLimit-Limit', limit.toString());
|
|
c.header('X-RateLimit-Remaining', '0');
|
|
c.header('X-RateLimit-Reset', new Date(Date.now() + ttl).toISOString());
|
|
|
|
return c.json(
|
|
{
|
|
error: 'Rate limit exceeded',
|
|
retryAfter: Math.ceil(ttl / 1000),
|
|
},
|
|
429
|
|
);
|
|
}
|
|
|
|
// Increment counter
|
|
const pipeline = redis.pipeline();
|
|
pipeline.incr(key);
|
|
pipeline.pexpire(key, window);
|
|
await pipeline.exec();
|
|
|
|
// Set headers
|
|
c.header('X-RateLimit-Limit', limit.toString());
|
|
c.header('X-RateLimit-Remaining', (limit - count - 1).toString());
|
|
c.header('X-RateLimit-Reset', new Date(Date.now() + window).toISOString());
|
|
|
|
return next();
|
|
} catch (error) {
|
|
logger.error('Rate limiter error:', error);
|
|
// Continue on error - don't block requests due to rate limiter failure
|
|
return next();
|
|
}
|
|
};
|
|
}; |