diff --git a/apps/backend/package.json b/apps/backend/package.json index b8d1141..b7aa6ff 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -22,6 +22,7 @@ "@fastify/helmet": "^12.0.0", "@fastify/jwt": "^9.0.0", "@fastify/multipart": "^9.0.0", + "@fastify/rate-limit": "^10.3.0", "@fastify/static": "^8.0.0", "@prisma/client": "^6.0.0", "dotenv": "^16.4.0", diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index dc023a2..aebc294 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -5,6 +5,7 @@ import jwt from '@fastify/jwt'; import cookie from '@fastify/cookie'; import multipart from '@fastify/multipart'; import fastifyStatic from '@fastify/static'; +import rateLimit from '@fastify/rate-limit'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -45,6 +46,10 @@ export async function buildApp() { await app.register(cookie); await app.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB + await app.register(rateLimit, { + max: 100, + timeWindow: '1 minute', + }); // Static file serving for uploads await app.register(fastifyStatic, { diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index 0d9c91c..ecbe344 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -4,7 +4,14 @@ import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; export async function publicRoutes(app: FastifyInstance) { // ─── Public Profile ─── - app.get('/:username', async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { + app.get('/:username', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute' + } + } + }, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { const { username } = request.params; const user = await app.prisma.user.findUnique({ @@ -71,7 +78,14 @@ export async function publicRoutes(app: FastifyInstance) { // ─── Shared Card View (Direct) ─── - app.get('/card/:cardId', async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => { + app.get('/card/:cardId', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute' + } + } + }, async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => { const { cardId } = request.params; const card = await app.prisma.card.findUnique({ @@ -110,7 +124,14 @@ export async function publicRoutes(app: FastifyInstance) { // ─── Public Card View ─── - app.get('/:username/card/:cardId', async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => { + app.get('/:username/card/:cardId', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute' + } + } + }, async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => { const { username, cardId } = request.params; const user = await app.prisma.user.findUnique({ @@ -182,7 +203,14 @@ export async function publicRoutes(app: FastifyInstance) { // ─── QR Code Generation ─── - app.get('/:username/qr', async (request: FastifyRequest<{ + app.get('/:username/qr', { + config: { + rateLimit: { + max: 50, // Lower limit for QR generation as it's more resource intensive + timeWindow: '1 minute' + } + } + }, async (request: FastifyRequest<{ Params: { username: string }; Querystring: { format?: string; size?: string }; }>, reply: FastifyReply) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2637abe..d97b4c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@fastify/multipart': specifier: ^9.0.0 version: 9.4.0 + '@fastify/rate-limit': + specifier: ^10.3.0 + version: 10.3.0 '@fastify/static': specifier: ^8.0.0 version: 8.3.0 @@ -1233,6 +1236,9 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/rate-limit@10.3.0': + resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==} + '@fastify/send@4.1.0': resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} @@ -1925,6 +1931,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -6257,6 +6264,12 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.3.0 + '@fastify/rate-limit@10.3.0': + dependencies: + '@lukeed/ms': 2.0.2 + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 + '@fastify/send@4.1.0': dependencies: '@lukeed/ms': 2.0.2