Files
kita-planer/src/app/register/actions.ts
T
2026-05-19 11:09:53 +02:00

124 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<RegisterState> {
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 };
}