99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
"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<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, 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 };
|
|
}
|