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