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
+21 -11
View File
@@ -1,11 +1,14 @@
"use server";
import { UserFacingError, publicActionError } from "@/lib/action-error";
import { createElement } from "react";
import { revalidatePath } from "next/cache";
import { NotdienstPlanStatus, UserRole } from "@prisma/client";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { dateOnlySchema, validPlanMonth } from "@/lib/date-schema";
import { rateLimit } from "@/lib/rate-limit";
import { requireRole } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import { getTargetMonthData, getWorkingDaysOfMonth } from "@/lib/date-utils";
@@ -32,6 +35,8 @@ export async function generatePlanAction(year?: number, month?: number) {
targetMonth = data.targetMonth;
}
if (!validPlanMonth(targetYear, targetMonth)) return { error: "Ungültiger Planungsmonat." };
// Prüfen, ob schon ein Plan existiert
const existingPlan = await prisma.notdienstPlan.findUnique({
where: {
@@ -156,6 +161,7 @@ export async function updateAssignmentAction(
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
const kitaId = session.user.kitaId!;
if (!dateOnlySchema.safeParse(date).success) return { error: "Ungültiges Datum." };
const [yr, mo, dy] = date.slice(0, 10).split("-").map(Number);
const parsedDate = new Date(Date.UTC(yr, mo - 1, dy));
@@ -166,21 +172,21 @@ export async function updateAssignmentAction(
});
if (!plan) {
throw new Error("Plan nicht gefunden.");
throw new UserFacingError("Plan nicht gefunden.");
}
const isDraft = plan.status === NotdienstPlanStatus.DRAFT;
const isPublished = plan.status === NotdienstPlanStatus.PUBLISHED;
if (!isDraft && !isPublished) {
throw new Error("Plan kann nicht bearbeitet werden.");
throw new UserFacingError("Plan kann nicht bearbeitet werden.");
}
if (isPublished) {
const today = new Date();
const todayUtc = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()));
if (parsedDate < todayUtc) {
throw new Error("Vergangene Notdienste können nicht geändert werden.");
throw new UserFacingError("Vergangene Notdienste können nicht geändert werden.");
}
}
@@ -192,7 +198,7 @@ export async function updateAssignmentAction(
family: {
include: {
users: {
where: { role: UserRole.ELTERN },
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -222,7 +228,7 @@ export async function updateAssignmentAction(
family: {
include: {
users: {
where: { role: UserRole.ELTERN },
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -231,7 +237,7 @@ export async function updateAssignmentAction(
});
if (!newChild) {
throw new Error("Kind gehört nicht zu dieser Kita.");
throw new UserFacingError("Kind gehört nicht zu dieser Kita.");
}
}
@@ -364,7 +370,7 @@ export async function updateAssignmentAction(
return { success: true };
} catch (error) {
return {
error: error instanceof Error ? error.message : "Update fehlgeschlagen.",
error: publicActionError(error, "Update fehlgeschlagen."),
};
}
}
@@ -379,6 +385,8 @@ export async function updateKitaNotdienstSettingsAction(
return { error: "Kein Mandant zugeordnet." };
}
if (!Number.isInteger(minPerChild) || minPerChild < 1 || minPerChild > 31 ||
!Number.isInteger(reminderDays) || reminderDays < 0 || reminderDays > 31) return { error: "Ungültige Einstellungen." };
try {
await prisma.kita.update({
where: { id: kitaId },
@@ -398,6 +406,8 @@ export async function updateKitaNotdienstSettingsAction(
export async function sendNotdienstRemindersAction(year: number, month: number) {
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
if (!validPlanMonth(year, month)) return { error: "Ungültiger Planungsmonat." };
if (!(await rateLimit(`notdienst-reminders:${session.user.kitaId}`, 3, 3600)).allowed) return { error: "Bitte versuche es später erneut." };
const kitaId = session.user.kitaId;
if (!kitaId) {
return { error: "Kein Mandant zugeordnet." };
@@ -426,7 +436,7 @@ export async function sendNotdienstRemindersAction(year: number, month: number)
},
include: {
users: {
where: { role: UserRole.ELTERN },
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
},
},
});
@@ -513,11 +523,11 @@ export async function resetPlanAction(planId: string) {
});
if (!plan) {
throw new Error("Plan nicht gefunden.");
throw new UserFacingError("Plan nicht gefunden.");
}
if (plan.status !== NotdienstPlanStatus.DRAFT) {
throw new Error("Nur Pläne im Entwurfsstatus können zurückgesetzt werden.");
throw new UserFacingError("Nur Pläne im Entwurfsstatus können zurückgesetzt werden.");
}
// Delete all assignments
@@ -535,6 +545,6 @@ export async function resetPlanAction(planId: string) {
return { success: true };
} catch (error) {
console.error("Fehler beim Zurücksetzen des Plans:", error);
return { error: error instanceof Error ? error.message : "Fehler beim Zurücksetzen des Plans." };
return { error: publicActionError(error, "Fehler beim Zurücksetzen des Plans.") };
}
}