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
+17 -23
View File
@@ -1,3 +1,4 @@
import { cache } from "react";
import { redirect } from "next/navigation";
import type { Session } from "next-auth";
import { UserRole } from "@prisma/client";
@@ -28,12 +29,14 @@ export type KitaSession = AuthenticatedSession & {
user: AuthenticatedSession["user"] & { kitaId: string };
};
const readRequestSession = cache(() => auth());
export async function getSession(): Promise<Session | null> {
return auth();
return readRequestSession();
}
export async function requireSession(): Promise<AuthenticatedSession> {
const session = await auth();
const session = await getSession();
if (!session?.user?.id) {
redirect("/login");
}
@@ -46,17 +49,10 @@ export async function requireSession(): Promise<AuthenticatedSession> {
* für alle Tenant-gebundenen Server Actions / Page Components.
*/
export async function requireKitaSession(): Promise<KitaSession> {
if (process.env.TEST_MODE === "true") {
return {
user: {
id: process.env.TEST_USER_ID || "test-user-id",
role: (process.env.TEST_USER_ROLE as UserRole) || UserRole.ELTERN,
kitaId: process.env.TEST_KITA_ID || "test-kita-id",
familyId: process.env.TEST_FAMILY_ID || "test-family-id",
},
} as any;
}
const session = await requireSession();
return enforceKitaSession(await requireSession());
}
async function enforceKitaSession(session: AuthenticatedSession): Promise<KitaSession> {
if (!session.user.kitaId) {
// Superadmins haben keine Kita → eigene Oberfläche.
@@ -80,20 +76,11 @@ export async function requireKitaSession(): Promise<KitaSession> {
export async function requireRole(
allowed: UserRole[],
): Promise<AuthenticatedSession> {
if (process.env.TEST_MODE === "true") {
return {
user: {
id: process.env.TEST_USER_ID || "test-user-id",
role: (process.env.TEST_USER_ROLE as UserRole) || UserRole.ADMIN,
kitaId: process.env.TEST_KITA_ID || "test-kita-id",
familyId: process.env.TEST_FAMILY_ID || "test-family-id",
},
} as any;
}
const session = await requireSession();
if (!allowed.includes(session.user.role)) {
redirect("/forbidden");
}
if (session.user.role !== UserRole.SUPERADMIN) return enforceKitaSession(session);
return session;
}
@@ -113,3 +100,10 @@ async function consentCheck(userId: string | undefined): Promise<boolean> {
});
return !!user?.privacyPolicyAcceptedAt;
}
// Route handlers use this non-redirecting counterpart of the same consent gate.
export async function getKitaSession(): Promise<KitaSession | null> {
const session = await getSession();
if (!session?.user?.id || !session.user.kitaId || !(await consentCheck(session.user.id))) return null;
return session as KitaSession;
}