diff --git a/frontend/app/api/auth/signup/route.ts b/frontend/app/api/auth/signup/route.ts index 1c6a55c..3a52202 100644 --- a/frontend/app/api/auth/signup/route.ts +++ b/frontend/app/api/auth/signup/route.ts @@ -3,6 +3,7 @@ import { createClient } from "@supabase/supabase-js"; import { NextRequest, NextResponse } from "next/server"; import { randomUUID } from "crypto"; import { signupSchema } from "../../../../lib/validations/auth"; +import { signupRateLimiter } from "../../../../lib/rateLimiter"; import { handleApiError } from "../../../../lib/apiErrorHandler"; import { sanitizeInput } from "../../../../lib/sanitizeInput"; @@ -22,6 +23,21 @@ type ProjectRow = { export async function POST(req: NextRequest) { try { + const ip = + req.headers.get("x-forwarded-for") || + req.headers.get("x-real-ip") || + "unknown"; + + try { + await signupRateLimiter.consume(ip); + } catch { + return NextResponse.json( + { + error: "Too many signup attempts. Please try again later.", + }, + { status: 429 } + ); + } const payload = await req.json(); const validation = signupSchema.safeParse(payload); if (!validation.success) { diff --git a/frontend/lib/rateLimiter.ts b/frontend/lib/rateLimiter.ts new file mode 100644 index 0000000..6296444 --- /dev/null +++ b/frontend/lib/rateLimiter.ts @@ -0,0 +1,6 @@ +import { RateLimiterMemory } from "rate-limiter-flexible"; + +export const signupRateLimiter = new RateLimiterMemory({ + points: 5, + duration: 60, +}); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index bd38246..b4ad0b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "lucide-react": "^1.14.0", "next": "16.2.6", "postcss": "^8.5.10", + "rate-limiter-flexible": "^11.1.0", "react": "19.2.4", "react-dom": "19.2.4", "react-loading-skeleton": "^3.5.0", diff --git a/package-lock.json b/package-lock.json index 4c0baef..db742f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@supabase/supabase-js": "^2.103.0", "lucide": "^1.8.0", + "rate-limiter-flexible": "^11.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", "zod": "^4.4.3" @@ -31,6 +32,7 @@ "lucide-react": "^1.14.0", "next": "16.2.6", "postcss": "^8.5.10", + "rate-limiter-flexible": "^11.1.0", "react": "19.2.4", "react-dom": "19.2.4", "react-loading-skeleton": "^3.5.0", @@ -6299,6 +6301,12 @@ "node": ">=10" } }, + "node_modules/rate-limiter-flexible": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-11.1.0.tgz", + "integrity": "sha512-lyyC0SqKz+dE5JoHZ4JMqdrM3LSZKBxzuAFAyKCYAnmHnPz/Rb6iDquxoL4CMipDXoR0G+QRhOzYWL3JKihbNw==", + "license": "ISC" + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", diff --git a/package.json b/package.json index 860b2bd..d67a199 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@supabase/supabase-js": "^2.103.0", "lucide": "^1.8.0", + "rate-limiter-flexible": "^11.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", "zod": "^4.4.3"