Files
sass-mohfarawati/lib/contact-guard.ts
T

113 lines
2.8 KiB
TypeScript

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.");
}
}