"use server"; import { hash } from "bcryptjs"; import { Prisma, UserRole } from "@prisma/client"; import { z } from "zod"; import { signIn } from "@/auth"; import { prisma } from "@/lib/prisma"; // ===================================================================== // /register · Server Action // --------------------------------------------------------------------- // Legt einen "heimatlosen" Admin-Account an (kitaId === null) und loggt // ihn direkt ein. Die Tenant-Erstellung folgt im Onboarding-Wizard. // ===================================================================== const registerSchema = z.object({ email: z.string().email("Bitte eine gültige E-Mail-Adresse angeben.").toLowerCase().trim(), // bcrypt akzeptiert max. 72 Bytes — wir capen davor zur Sicherheit password: z .string() .min(8, "Mindestens 8 Zeichen.") .max(72, "Maximal 72 Zeichen."), 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.", }), }); const PRIVACY_POLICY_VERSION = "2026-05-01"; 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; // 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") ?? ""), lastName: String(formData.get("lastName") ?? ""), email: String(formData.get("email") ?? ""), 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, password, firstName, lastName } = parsed.data; const passwordHash = await hash(password, 12); const now = new Date(); try { await prisma.user.create({ data: { email, passwordHash, firstName, lastName, // Gründer wird "heimatloser" Admin — beim Onboarding-Abschluss // wird die Rolle bestätigt und der User mit einer Kita verknüpft. role: UserRole.ADMIN, kitaId: null, privacyPolicyAcceptedAt: now, privacyPolicyVersion: PRIVACY_POLICY_VERSION, emailVerifiedAt: now, }, }); } catch (err) { if ( err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002" ) { return { attempt, values: echoValues(formData), errors: { email: ["Mit dieser E-Mail-Adresse existiert bereits ein Account."], }, }; } throw err; } // Direkt einloggen → wirft NEXT_REDIRECT, das wir nicht abfangen. await signIn("credentials", { email, password, redirectTo: "/onboarding", }); // Unreachable – signIn redirected. return { attempt }; }