Harden authentication, tenant isolation and security boundaries
This commit is contained in:
@@ -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.") };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user