"use server"; import { redirect } from "next/navigation"; import { prisma } from "@/lib/prisma"; import { createSession, getCurrentCustomer, hashPassword } from "@/lib/auth"; import { registerSchema } from "@/lib/validation"; import { rateLimit, getClientIp } from "@/lib/rate-limit"; import { createAndSendVerificationEmail } from "@/lib/email-verification"; import { verifyTurnstileToken } from "@/lib/turnstile"; import { isPasswordBreached } from "@/lib/password-breach-check"; import { fail } from "./_shared"; const REGISTER_ATTEMPTS_PER_WINDOW = 5; const REGISTER_WINDOW_MS = 60 * 60 * 1000; const VERIFY_RESEND_ATTEMPTS_PER_WINDOW = 5; const VERIFY_RESEND_WINDOW_MS = 60 * 60 * 1000; export async function registerAction(formData: FormData): Promise { const ip = await getClientIp(); if (!rateLimit(`register:${ip}`, REGISTER_ATTEMPTS_PER_WINDOW, REGISTER_WINDOW_MS)) { fail("/konto", "Zu viele Registrierungsversuche. Bitte versuche es später erneut."); } const turnstileToken = String(formData.get("turnstileToken") ?? ""); if (!(await verifyTurnstileToken(turnstileToken, ip))) { fail("/konto", "Die Sicherheitsprüfung ist fehlgeschlagen. Bitte versuche es erneut."); } const parsed = registerSchema.safeParse({ firstName: formData.get("firstName"), lastName: formData.get("lastName"), email: formData.get("email"), password: formData.get("password"), }); if (!parsed.success) { fail("/konto", parsed.error.issues[0]?.message ?? "Ungültige Eingabe."); } if (formData.get("password") !== formData.get("passwordConfirm")) { fail("/konto", "Die Passwörter stimmen nicht überein."); } if (await isPasswordBreached(parsed.data.password)) { fail( "/konto", "Dieses Passwort wurde in bekannten Datenlecks gefunden. Bitte wähle ein anderes Passwort." ); } const existing = await prisma.customer.findUnique({ where: { email: parsed.data.email } }); if (existing) { // Absichtlich keine eindeutige "Konto existiert bereits"-Meldung und // absichtlich derselbe bcrypt-Aufwand wie im Erfolgsfall (Zeile unten) — // beides wuerde sonst verraten, welche E-Mail-Adressen registriert sind // (Timing- bzw. Text-Oracle), analog zum Login-Dummy-Hash in lib/auth.ts. await hashPassword(parsed.data.password); fail( "/konto", "Falls für diese E-Mail-Adresse noch kein Konto besteht, erhältst du in Kürze eine Bestätigungsmail." ); } // Rolle wird bewusst nicht aus formData gelesen — neue Konten sind immer // KUNDE (Prisma-Default). Admin-Rechte werden ausschliesslich ueber die // Datenbank vergeben, nie ueber ein Client-Feld. const customer = await prisma.customer.create({ data: { firstName: parsed.data.firstName, lastName: parsed.data.lastName, email: parsed.data.email, passwordHash: await hashPassword(parsed.data.password), }, }); await createAndSendVerificationEmail(customer); await createSession(customer.id, customer.role, customer.sessionVersion); redirect("/konto"); } /** Rate-limitiert erneutes Zusenden des Bestaetigungslinks, falls die erste Mail verloren ging. */ export async function resendVerificationEmailAction(): Promise { const customer = await getCurrentCustomer(); if (!customer) fail("/konto", "Bitte melde dich an."); if (customer.emailVerifiedAt) redirect("/konto"); const ip = await getClientIp(); if (!rateLimit(`verify-resend:${ip}:${customer.id}`, VERIFY_RESEND_ATTEMPTS_PER_WINDOW, VERIFY_RESEND_WINDOW_MS)) { fail("/konto", "Zu viele Anfragen. Bitte versuche es später erneut."); } await createAndSendVerificationEmail(customer); redirect("/konto?message=" + encodeURIComponent("Bestätigungslink wurde erneut gesendet.")); }