124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
"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 };
|
||
}
|