From 05419e9457a0514dad96d065c039b03a03b71ea2 Mon Sep 17 00:00:00 2001 From: mikl0s Date: Sun, 22 Feb 2026 21:55:40 +0000 Subject: [PATCH] feat: add rate limiting to login and quote-request endpoints Co-Authored-By: Claude Opus 4.6 --- app/api/auth/login/route.ts | 6 +++++ app/api/quote-request/route.ts | 6 +++++ lib/rate-limit.ts | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 lib/rate-limit.ts diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 0ca4253..a4838fc 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,7 +1,13 @@ import { NextRequest, NextResponse } from "next/server" import { login } from "@/lib/auth" +import { rateLimit } from "@/lib/rate-limit" export async function POST(request: NextRequest) { + const ip = request.headers.get("x-forwarded-for") || "unknown" + if (!rateLimit(ip, 5, 60_000)) { + return NextResponse.json({ error: "For mange forsøg. Prøv igen om lidt." }, { status: 429 }) + } + try { const { email, password } = await request.json() diff --git a/app/api/quote-request/route.ts b/app/api/quote-request/route.ts index 3d30fdf..068c859 100644 --- a/app/api/quote-request/route.ts +++ b/app/api/quote-request/route.ts @@ -3,6 +3,7 @@ import nodemailer from "nodemailer" import { formatPrice, type CalculationDetails } from "@/lib/calculations" import { FLOORING_TYPES } from "@/lib/constants" import { saveQuote } from "@/lib/db" +import { rateLimit } from "@/lib/rate-limit" interface QuoteRequestBody { customerInfo: { @@ -234,6 +235,11 @@ function formatFoamKingEmail( } export async function POST(request: NextRequest) { + const ip = request.headers.get("x-forwarded-for") || "unknown" + if (!rateLimit(ip, 10, 60_000)) { + return NextResponse.json({ error: "For mange anmodninger. Prøv igen om lidt." }, { status: 429 }) + } + try { const body: QuoteRequestBody = await request.json() const { customerInfo, calculationDetails } = body diff --git a/lib/rate-limit.ts b/lib/rate-limit.ts new file mode 100644 index 0000000..a28ff65 --- /dev/null +++ b/lib/rate-limit.ts @@ -0,0 +1,41 @@ +// Simple in-memory rate limiter. No external dependencies. +// Tracks requests per IP with a sliding window. + +interface RateLimitEntry { + count: number + resetAt: number +} + +const store = new Map() + +// Clean up expired entries periodically +setInterval(() => { + const now = Date.now() + for (const [key, entry] of store) { + if (entry.resetAt < now) store.delete(key) + } +}, 60_000) + +/** + * Check if a request should be rate-limited. + * @param key - Unique identifier (e.g. IP address) + * @param limit - Max requests per window + * @param windowMs - Time window in milliseconds + * @returns true if the request is allowed, false if rate-limited + */ +export function rateLimit(key: string, limit: number, windowMs: number): boolean { + const now = Date.now() + const entry = store.get(key) + + if (!entry || entry.resetAt < now) { + store.set(key, { count: 1, resetAt: now + windowMs }) + return true + } + + if (entry.count >= limit) { + return false + } + + entry.count++ + return true +}