Files
kita-planer/src/app/register/actions.ts
T

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 };
}