feat: add rate limiting to login and quote-request endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4889ead690
commit
05419e9457
3 changed files with 53 additions and 0 deletions
|
|
@ -1,7 +1,13 @@
|
||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { login } from "@/lib/auth"
|
import { login } from "@/lib/auth"
|
||||||
|
import { rateLimit } from "@/lib/rate-limit"
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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 {
|
try {
|
||||||
const { email, password } = await request.json()
|
const { email, password } = await request.json()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import nodemailer from "nodemailer"
|
||||||
import { formatPrice, type CalculationDetails } from "@/lib/calculations"
|
import { formatPrice, type CalculationDetails } from "@/lib/calculations"
|
||||||
import { FLOORING_TYPES } from "@/lib/constants"
|
import { FLOORING_TYPES } from "@/lib/constants"
|
||||||
import { saveQuote } from "@/lib/db"
|
import { saveQuote } from "@/lib/db"
|
||||||
|
import { rateLimit } from "@/lib/rate-limit"
|
||||||
|
|
||||||
interface QuoteRequestBody {
|
interface QuoteRequestBody {
|
||||||
customerInfo: {
|
customerInfo: {
|
||||||
|
|
@ -234,6 +235,11 @@ function formatFoamKingEmail(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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 {
|
try {
|
||||||
const body: QuoteRequestBody = await request.json()
|
const body: QuoteRequestBody = await request.json()
|
||||||
const { customerInfo, calculationDetails } = body
|
const { customerInfo, calculationDetails } = body
|
||||||
|
|
|
||||||
41
lib/rate-limit.ts
Normal file
41
lib/rate-limit.ts
Normal file
|
|
@ -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<string, RateLimitEntry>()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue