Harden authentication, tenant isolation and security boundaries

This commit is contained in:
t.indorf
2026-10-09 22:49:55 +02:00
parent ec3f4f933d
commit 656ada80bd
71 changed files with 3107 additions and 1200 deletions
@@ -0,0 +1,45 @@
"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 {};
}