diff --git a/src/plugins/rateLimit.ts b/src/plugins/rateLimit.ts new file mode 100644 index 0000000..38869f4 --- /dev/null +++ b/src/plugins/rateLimit.ts @@ -0,0 +1,59 @@ +import { FastifyInstance, FastifyPluginAsync } from 'fastify'; +import rateLimit from '@fastify/rate-limit'; +import fp from 'fastify-plugin'; +import { env } from '../config/env.js'; + +declare module 'fastify' { + interface FastifyInstance { + rateLimitOptions: { + login: { max: number; timeWindow: string }; + register: { max: number; timeWindow: string }; + forgotPassword: { max: number; timeWindow: string }; + verifyEmail: { max: number; timeWindow: string }; + apiAuthed: { max: number; timeWindow: string }; + apiGuest: { max: number; timeWindow: string }; + }; + } +} + +const rateLimitPlugin: FastifyPluginAsync = async (app: FastifyInstance) => { + const options = { + login: { max: env.RATE_LIMIT_LOGIN, timeWindow: '15 minutes' }, + register: { max: env.RATE_LIMIT_REGISTER, timeWindow: '1 hour' }, + forgotPassword: { max: env.RATE_LIMIT_FORGOT_PASSWORD, timeWindow: '1 hour' }, + verifyEmail: { max: env.RATE_LIMIT_VERIFY_EMAIL, timeWindow: '15 minutes' }, + apiAuthed: { max: env.RATE_LIMIT_API_AUTHED, timeWindow: '1 minute' }, + apiGuest: { max: env.RATE_LIMIT_API_GUEST, timeWindow: '1 minute' }, + }; + + app.decorate('rateLimitOptions', options); + + await app.register(rateLimit, { + max: options.apiGuest.max, + timeWindow: options.apiGuest.timeWindow, + keyGenerator: (req) => { + return (req.ip ?? 'unknown') as string; + }, + redis: app.redis, + addHeadersOnExceeding: { + 'x-ratelimit-limit': true, + 'x-ratelimit-remaining': true, + 'x-ratelimit-reset': true, + }, + addHeaders: { + 'x-ratelimit-limit': true, + 'x-ratelimit-remaining': true, + 'x-ratelimit-reset': true, + 'retry-after': true, + }, + errorResponseBuilder: (_req, context) => ({ + error: { + code: 'RATE_LIMIT_EXCEEDED', + message: 'Too many requests, please try again later', + retryAfter: context.ttl, + }, + }), + }); +}; + +export default fp(rateLimitPlugin, { name: 'rateLimit', dependencies: ['redis'] });