* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
80 lines
2.3 KiB
TypeScript
80 lines
2.3 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 { rateLimiter } from '@api/utils/redis/limiter';
|
|
import { userKeyGenerator } from '../utils/redis/key-generators';
|
|
|
|
export interface RateLimiterOptions {
|
|
windowMs?: number;
|
|
maxRequests?: number;
|
|
keyGenerator?: (c: any) => string;
|
|
skipSuccessfulRequests?: boolean;
|
|
skipFailedRequests?: boolean;
|
|
message?: string;
|
|
standardHeaders?: boolean;
|
|
legacyHeaders?: boolean;
|
|
}
|
|
|
|
const defaultOptions: Required<RateLimiterOptions> = {
|
|
...DEFAULT_RATE_LIMITER_OPTIONS,
|
|
keyGenerator: userKeyGenerator
|
|
};
|
|
|
|
export const createRateLimiter = (options: RateLimiterOptions = {}): MiddlewareHandler => {
|
|
const opts = { ...defaultOptions, ...options };
|
|
|
|
return async (c, next) => {
|
|
try {
|
|
// Generate rate limit key
|
|
const key = opts.keyGenerator(c);
|
|
|
|
// Check rate limit
|
|
const result = await rateLimiter.isAllowed(key);
|
|
|
|
// Set rate limit headers
|
|
if (opts.standardHeaders) {
|
|
c.header(RATE_LIMIT_HEADERS.LIMIT, opts.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, opts.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 const rateLimiterMiddleware = createRateLimiter();
|