Files
classroomio/apps/api/src/middlewares/rate-limiter.ts
T

97 lines
2.9 KiB
TypeScript

import {
DEFAULT_RATE_LIMITER_OPTIONS,
ERROR_MESSAGES,
HTTP_STATUS,
RATE_LIMIT_HEADERS
} from '@api/constants/rate-limiter';
import type { MiddlewareHandler } from 'hono';
import { RedisRateLimiter } from '@api/utils/redis/limiter';
import { userKeyGenerator } from '../utils/redis/key-generators';
import { env } from '@api/config/env';
import { redis } from '@api/utils/redis/redis';
export interface RateLimiterOptions {
windowMs?: number;
maxRequests?: number | ((c: any) => number);
keyGenerator?: (c: any) => string;
skip?: (c: any) => boolean;
skipSuccessfulRequests?: boolean;
skipFailedRequests?: boolean;
message?: string;
standardHeaders?: boolean;
legacyHeaders?: boolean;
}
const defaultOptions: Required<RateLimiterOptions> = {
...DEFAULT_RATE_LIMITER_OPTIONS,
keyGenerator: userKeyGenerator,
skip: () => false
};
export const createRateLimiter = (options: RateLimiterOptions = {}): MiddlewareHandler => {
const opts = { ...defaultOptions, ...options };
return async (c, next) => {
// Skip rate limiting if not in production
if (env.NODE_ENV !== 'production' || opts.skip(c)) {
return await next();
}
try {
const maxRequests = typeof opts.maxRequests === 'function' ? opts.maxRequests(c) : opts.maxRequests;
const limiter = new RedisRateLimiter(redis, opts.windowMs, maxRequests);
// Generate rate limit key
const key = opts.keyGenerator(c);
// Check rate limit
const result = await limiter.isAllowed(key);
// Set rate limit headers
if (opts.standardHeaders) {
c.header(RATE_LIMIT_HEADERS.LIMIT, maxRequests.toString());
c.header(RATE_LIMIT_HEADERS.REMAINING, result.remaining.toString());
c.header(RATE_LIMIT_HEADERS.RESET, new Date(result.resetTime).toISOString());
}
if (opts.legacyHeaders) {
c.header(RATE_LIMIT_HEADERS.LIMIT, maxRequests.toString());
c.header(RATE_LIMIT_HEADERS.REMAINING, result.remaining.toString());
c.header(RATE_LIMIT_HEADERS.RESET, Math.ceil(result.resetTime / 1000).toString());
}
if (!result.allowed) {
const retryAfter = Math.ceil((result.resetTime - Date.now()) / 1000);
if (opts.standardHeaders) {
c.header(RATE_LIMIT_HEADERS.RETRY_AFTER, retryAfter.toString());
}
return c.json(
{
error: ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
message: opts.message,
retryAfter
},
HTTP_STATUS.TOO_MANY_REQUESTS
);
}
await next();
} catch (error) {
console.error('Rate limiter error:', error);
// If Redis is down, allow the request but log the error
await next();
}
};
};
// Default rate limiter middleware with standard configuration
export default createRateLimiter({
maxRequests: (c) =>
c.req.path === '/api/auth/get-session'
? DEFAULT_RATE_LIMITER_OPTIONS.maxRequests * 3
: DEFAULT_RATE_LIMITER_OPTIONS.maxRequests
});