Add Turnstile protection to user actions

This commit is contained in:
2026-09-18 00:39:14 +02:00
parent 79cd7f7475
commit 1970be29e7
25 changed files with 406 additions and 39 deletions
@@ -5,6 +5,7 @@ import config from "@payload-config";
import { getClientIp, checkRateLimit } from "@/lib/auth/rateLimit";
import { newConversationAdminEmail } from "@/lib/email/conversationTemplates";
import { sendEmail } from "@/lib/email/sendBookingEmails";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
@@ -14,6 +15,7 @@ type Body = {
message?: unknown;
subject?: unknown;
type?: unknown;
token?: unknown;
};
function clean(value: unknown, max: number): string {
@@ -37,6 +39,9 @@ export async function POST(request: Request) {
if (!name || !email || !message || !/^\S+@\S+\.\S+$/.test(email)) {
return NextResponse.json({ error: "Bitte fülle alle Pflichtfelder korrekt aus." }, { status: 400 });
}
if (!(await verifyTurnstileToken(body.token, "contact", request))) {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
if (!checkRateLimit(`contact:ip:${getClientIp(request)}`, { max: 8, windowMs: 60 * 60 * 1000 })) {
return NextResponse.json({ error: "Zu viele Anfragen. Bitte versuche es später erneut." }, { status: 429 });
}
@@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import { REST_POST } from "@payloadcms/next/routes";
import config from "@payload-config";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
const payloadPost = REST_POST(config);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.clone().json();
} catch {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
const token = isRecord(body) ? body.token : undefined;
if (!(await verifyTurnstileToken(token, "login", request))) {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
const sanitizedBody = isRecord(body) ? { ...body } : body;
if (isRecord(sanitizedBody)) delete sanitizedBody.token;
const headers = new Headers(request.headers);
headers.delete("content-length");
headers.set("content-type", "application/json");
const downstreamRequest = new Request(request.url, {
method: request.method,
headers,
body: JSON.stringify(sanitizedBody),
});
return payloadPost(downstreamRequest, { params: Promise.resolve({ slug: ["customers", "login"] }) });
}
+38
View File
@@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import { REST_POST } from "@payloadcms/next/routes";
import config from "@payload-config";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
const payloadPost = REST_POST(config);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.clone().json();
} catch {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
const token = isRecord(body) ? body.token : undefined;
if (!(await verifyTurnstileToken(token, "registration", request))) {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
const sanitizedBody = isRecord(body) ? { ...body } : body;
if (isRecord(sanitizedBody)) delete sanitizedBody.token;
const headers = new Headers(request.headers);
headers.delete("content-length");
headers.set("content-type", "application/json");
const downstreamRequest = new Request(request.url, {
method: request.method,
headers,
body: JSON.stringify(sanitizedBody),
});
return payloadPost(downstreamRequest, { params: Promise.resolve({ slug: ["customers"] }) });
}
@@ -4,6 +4,7 @@ import config from "@payload-config";
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
import { groupRegistrationEmail } from "@/lib/email/conversationTemplates";
import { sendEmail } from "@/lib/email/sendBookingEmails";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
@@ -19,6 +20,7 @@ export async function POST(request: Request) {
const phone = value(body.phone, 80);
const message = value(body.message, 3000);
if (!offerSlug || !name || !email || !/^\S+@\S+\.\S+$/.test(email)) return NextResponse.json({ error: "Bitte fülle Name und E-Mail korrekt aus." }, { status: 400 });
if (!(await verifyTurnstileToken(body.token, "group-registration", request))) return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
try {
const payload = await getPayload({ config });
@@ -7,6 +7,7 @@ import { authorizeMeeting } from "@/lib/meeting/access";
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
import { createLiveKitToken, getLiveKitClientUrl } from "@/lib/meeting/livekit";
import { isMeetingParticipantKicked } from "@/lib/meeting/kicks";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
@@ -25,7 +26,7 @@ export async function POST(request: Request, { params }: Args) {
return NextResponse.json({ error: "Zu viele Beitrittsversuche." }, { status: 429 });
}
let body: { name?: unknown; password?: unknown; clientId?: unknown };
let body: { name?: unknown; password?: unknown; clientId?: unknown; token?: unknown };
try {
body = await request.json();
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Invalid body");
@@ -42,6 +43,9 @@ export async function POST(request: Request, { params }: Args) {
if (password.length > 200) {
return NextResponse.json({ error: "Das Meeting-Passwort ist ungültig." }, { status: 400 });
}
if (!(await verifyTurnstileToken(body.token, "meeting-join", request))) {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
try {
const payload = await getPayload({ config });
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getPayload } from "payload";
import config from "@payload-config";
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
import { verifyTurnstileToken } from "@/lib/turnstile/server";
export const dynamic = "force-dynamic";
@@ -35,7 +36,7 @@ export async function POST(request: Request, { params }: Args) {
return NextResponse.json({ error: "Zu viele Anmeldungen. Bitte versuche es später erneut." }, { status: 429 });
}
let body: { name?: unknown; email?: unknown };
let body: { name?: unknown; email?: unknown; token?: unknown };
try {
body = await request.json();
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Invalid body");
@@ -48,6 +49,9 @@ export async function POST(request: Request, { params }: Args) {
if (!name || name.length > 120 || !email || email.length > 254 || !email.includes("@")) {
return NextResponse.json({ error: "Bitte gib deinen Namen und eine gültige E-Mail-Adresse an." }, { status: 400 });
}
if (!(await verifyTurnstileToken(body.token, "meeting-registration", request))) {
return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 });
}
try {
const payload = await getPayload({ config });