46 lines
2.8 KiB
TypeScript
46 lines
2.8 KiB
TypeScript
"use server";
|
|
import { hash } from "bcryptjs";
|
|
import { z } from "zod";
|
|
import { Prisma, UserRole } from "@prisma/client";
|
|
import { signIn } from "@/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { passwordSchema } from "@/lib/password";
|
|
import { hashToken, isPublicToken } from "@/lib/tokens";
|
|
import { limitPublicAction } from "@/lib/rate-limit";
|
|
|
|
const schema = z.object({ token: z.string().refine(isPublicToken), password: passwordSchema,
|
|
confirmPassword: z.string().max(72), acceptPrivacyPolicy: z.literal("on") })
|
|
.refine(d => d.password === d.confirmPassword, { message: "Passwörter stimmen nicht überein.", path: ["confirmPassword"] });
|
|
export type VerificationState = { error?: string };
|
|
export async function verifyRegistrationAction(_state: VerificationState, form: FormData): Promise<VerificationState> {
|
|
const parsed = schema.safeParse(Object.fromEntries(form));
|
|
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Bitte Eingaben prüfen." };
|
|
const { token, password } = parsed.data;
|
|
if (!(await limitPublicAction("verify-registration", hashToken(token), 10, 900))) return { error: "Bitte versuche es später erneut." };
|
|
const draft = await prisma.pendingRegistration.findUnique({ where: { tokenHash: hashToken(token) } });
|
|
if (!draft || draft.expiresAt <= new Date()) return { error: "Der Link ist ungültig oder abgelaufen. Bitte fordere eine neue E-Mail an." };
|
|
const passwordHash = await hash(password, 12);
|
|
try {
|
|
await prisma.$transaction(async tx => {
|
|
const used = await tx.pendingRegistration.deleteMany({ where: { tokenHash: hashToken(token), expiresAt: { gt: new Date() } } });
|
|
if (used.count !== 1) throw new Error("TOKEN_USED");
|
|
const existing = await tx.user.findUnique({ where: { email: draft.email }, select: { id: true } });
|
|
const data = { firstName: draft.firstName, lastName: draft.lastName, passwordHash,
|
|
emailVerifiedAt: new Date(), privacyPolicyAcceptedAt: new Date(), privacyPolicyVersion: "2026-05-01" };
|
|
if (existing) {
|
|
const changed = await tx.user.updateMany({ where: { id: existing.id, role: UserRole.ADMIN, kitaId: null,
|
|
emailVerifiedAt: null, disabledAt: null }, data: { ...data, sessionVersion: { increment: 1 } } });
|
|
if (changed.count !== 1) throw new Error("TOKEN_USED");
|
|
} else {
|
|
await tx.user.create({ data: { ...data, email: draft.email, role: UserRole.ADMIN } });
|
|
}
|
|
});
|
|
} catch (error) {
|
|
if ((error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") ||
|
|
(error instanceof Error && error.message === "TOKEN_USED")) return { error: "Der Link wurde bereits verwendet. Bitte melde dich an." };
|
|
throw error;
|
|
}
|
|
await signIn("credentials", { email: draft.email, password, redirectTo: "/onboarding" });
|
|
return {};
|
|
}
|