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
+15 -3
View File
@@ -1,5 +1,7 @@
"use server";
import { removeAttachmentFiles } from "@/lib/attachments";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { AuditTargetType, UserRole } from "@prisma/client";
import { z } from "zod";
@@ -26,6 +28,7 @@ type ActionResult = {
async function requireSuperadminActor() {
const session = await requireRole([UserRole.SUPERADMIN]);
if (Date.now() - session.user.authenticatedAt > 15 * 60_000) redirect("/login?reauth=1&callbackUrl=/admin");
return {
id: session.user.id,
email: session.user.email ?? session.user.name ?? "Unbekannter Superadmin",
@@ -92,7 +95,8 @@ export async function deleteKita(
const counts = await getKitaDeleteCounts(kita.id);
await prisma.$transaction(async (tx) => {
const attachmentIds = await prisma.$transaction(async (tx) => {
const attachments = await tx.attachment.findMany({ where: { announcement: { kitaId: kita.id } }, select: { id: true } });
await tx.auditLog.create({
data: {
actorUserId: actor.id,
@@ -107,8 +111,10 @@ export async function deleteKita(
});
await tx.kita.delete({ where: { id: kita.id } });
return attachments.map(item => item.id);
});
await removeAttachmentFiles(attachmentIds);
revalidatePath("/admin");
revalidatePath("/admin/audit");
return { ok: true, redirectTo: "/admin" };
@@ -182,7 +188,8 @@ export async function deleteUser(rawPayload: unknown): Promise<ActionResult> {
};
}
await prisma.$transaction(async (tx) => {
const attachmentIds = await prisma.$transaction(async (tx) => {
const attachments = await tx.attachment.findMany({ where: { announcement: { authorId: user.id } }, select: { id: true } });
await tx.verificationToken.deleteMany({
where: { identifier: user.id },
});
@@ -206,8 +213,10 @@ export async function deleteUser(rawPayload: unknown): Promise<ActionResult> {
},
});
await tx.user.delete({ where: { id: user.id } });
return attachments.map(item => item.id);
});
await removeAttachmentFiles(attachmentIds);
revalidatePath("/admin");
revalidatePath(`/admin/kitas/${user.kitaId}`);
revalidatePath("/admin/audit");
@@ -219,6 +228,7 @@ async function updateUserSupportState(
action: "USER_LOCK" | "USER_UNLOCK" | "USER_FAILED_LOGIN_RESET",
data: {
lockedUntil?: Date | null;
disabledAt?: Date | null;
failedLoginAttempts?: number;
},
): Promise<ActionResult> {
@@ -238,7 +248,7 @@ async function updateUserSupportState(
await prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: user.id },
data,
data: { ...data, ...(action === "USER_LOCK" ? { sessionVersion: { increment: 1 } } : {}) },
});
await tx.auditLog.create({
data: {
@@ -266,12 +276,14 @@ async function updateUserSupportState(
export async function lockUser(userId: string): Promise<ActionResult> {
return updateUserSupportState(userId, "USER_LOCK", {
disabledAt: new Date(),
lockedUntil: new Date("2099-12-31T23:59:59.000Z"),
});
}
export async function unlockUser(userId: string): Promise<ActionResult> {
return updateUserSupportState(userId, "USER_UNLOCK", {
disabledAt: null,
lockedUntil: null,
failedLoginAttempts: 0,
});