"use server"; import { createElement } from "react"; import { z } from "zod"; import { hashToken, newToken } from "@/lib/tokens"; import { limitPublicAction } from "@/lib/rate-limit"; import { sendAppEmail } from "@/lib/mail"; import { prisma } from "@/lib/prisma"; // ===================================================================== // /register · Server Action // --------------------------------------------------------------------- // Speichert eine kurzlebige Registrierung und versendet einen Bestätigungslink. // Konto und Passwort entstehen erst nach dem Nachweis des Postfachbesitzes. // ===================================================================== const registerSchema = z.object({ email: z.string().trim().max(254).email("Bitte eine gültige E-Mail-Adresse angeben.").toLowerCase(), firstName: z.string().min(1, "Pflichtfeld.").max(100).trim(), lastName: z.string().min(1, "Pflichtfeld.").max(100).trim(), // HTML-Checkboxen senden "on" wenn aktiviert, sonst nichts acceptPrivacyPolicy: z.literal("on", { error: "Bitte Datenschutzerklärung akzeptieren.", }), }); export type RegisterState = { // Bumped after every action call so the client can key uncontrolled inputs // and force a remount with the echoed values from a failed submit. attempt: number; message?: string; // Echoed inputs (sans password) so failed submits don't wipe the form. values?: { firstName?: string; lastName?: string; email?: string; acceptPrivacyPolicy?: boolean; }; errors?: { email?: string[]; password?: string[]; firstName?: string[]; lastName?: string[]; acceptPrivacyPolicy?: string[]; _form?: string[]; }; }; function echoValues(formData: FormData): RegisterState["values"] { return { firstName: String(formData.get("firstName") ?? "").slice(0, 100), lastName: String(formData.get("lastName") ?? "").slice(0, 100), email: String(formData.get("email") ?? "").slice(0, 254), acceptPrivacyPolicy: formData.get("acceptPrivacyPolicy") === "on", }; } export async function registerAction( prevState: RegisterState, formData: FormData, ): Promise { const attempt = prevState.attempt + 1; const parsed = registerSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) { return { attempt, values: echoValues(formData), errors: parsed.error.flatten().fieldErrors, }; } const { email, firstName, lastName } = parsed.data; const message = "Falls die Adresse registriert werden kann, erhältst du eine E-Mail. Öffne den Link, um dein Passwort zu wählen und deine Kita einzurichten. Der Link ist 30 Minuten gültig."; if (!(await limitPublicAction("register", email))) { return { attempt, errors: { _form: ["Zu viele Anfragen. Bitte versuche es später erneut."] } }; } const existing = await prisma.user.findUnique({ where: { email }, select: { role: true, kitaId: true, emailVerifiedAt: true, disabledAt: true } }); if (existing && (existing.role !== "ADMIN" || existing.kitaId || existing.emailVerifiedAt || existing.disabledAt)) return { attempt, message }; const token = newToken(); await prisma.pendingRegistration.deleteMany({ where: { expiresAt: { lt: new Date() } } }); await prisma.pendingRegistration.upsert({ where: { email }, create: { email, firstName, lastName, tokenHash: hashToken(token), expiresAt: new Date(Date.now() + 30 * 60_000) }, update: { firstName, lastName, tokenHash: hashToken(token), expiresAt: new Date(Date.now() + 30 * 60_000) }, }); const base = process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "http://localhost:3000"; const result = await sendAppEmail({ to: email, subject: "E-Mail bestätigen · Kita-Planer", react: createElement("div", null, createElement("p", null, "Bestätige deine E-Mail-Adresse, um dein Konto einzurichten. Falls du dich nicht registriert hast, kannst du diese Nachricht ignorieren."), createElement("a", { href: `${base}/register/verify/${token}` }, "Registrierung abschließen")), }); if (!result.success) return { attempt, errors: { _form: ["Die Bestätigungs-E-Mail konnte nicht versendet werden. Bitte versuche es später erneut."] } }; return { attempt, message }; }