import { createHash } from "crypto"; import { headers } from "next/headers"; import { prisma } from "@/lib/prisma"; import { CONTACT_RATE_LIMIT_KEY_PREFIX, type ContactProtectionSettings, } from "@/lib/contact-protection"; function getClientIpFromHeaders() { const requestHeaders = headers(); const forwardedFor = requestHeaders.get("x-forwarded-for"); if (forwardedFor) { return forwardedFor.split(",")[0]?.trim() || "unknown"; } return requestHeaders.get("x-real-ip")?.trim() || "unknown"; } function getRateLimitKey(ip: string, windowMinutes: number) { const windowMs = windowMinutes * 60 * 1000; const windowStart = Math.floor(Date.now() / windowMs) * windowMs; const ipHash = createHash("sha256").update(ip).digest("hex"); return `${CONTACT_RATE_LIMIT_KEY_PREFIX}:${ipHash}:${windowStart}`; } function parseCount(rawValue: string | null | undefined) { if (!rawValue) { return 0; } const parsed = Number.parseInt(rawValue, 10); return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0; } export async function enforceContactRateLimit(settings: ContactProtectionSettings) { if (!settings.rateLimit.enabled) { return; } const ip = getClientIpFromHeaders(); const key = getRateLimitKey(ip, settings.rateLimit.windowMinutes); await prisma.$transaction(async (tx) => { const current = await tx.appConfig.findUnique({ where: { key }, select: { value: true }, }); const nextCount = parseCount(current?.value) + 1; if (nextCount > settings.rateLimit.maxRequests) { throw new Error("Too many contact requests. Please try again later."); } await tx.appConfig.upsert({ where: { key }, update: { value: String(nextCount), }, create: { key, value: "1", }, }); }); } export async function verifyTurnstileToken( settings: ContactProtectionSettings, token: string, ) { if (!settings.turnstile.enabled) { return; } if (!settings.turnstile.siteKey || !settings.turnstile.secretKey) { throw new Error("Turnstile is enabled but not fully configured."); } if (!token.trim()) { throw new Error("Turnstile verification is required."); } const body = new URLSearchParams(); body.set("secret", settings.turnstile.secretKey); body.set("response", token); body.set("remoteip", getClientIpFromHeaders()); const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body, cache: "no-store", }); if (!response.ok) { throw new Error("Turnstile verification request failed."); } const result = await response.json() as { success?: boolean; }; if (!result.success) { throw new Error("Turnstile verification failed."); } }