Files
kita-planer/src/lib/auth-utils.ts
T
2026-05-06 22:31:07 +02:00

94 lines
3.2 KiB
TypeScript

import { redirect } from "next/navigation";
import type { Session } from "next-auth";
import { UserRole } from "@prisma/client";
import { auth } from "@/auth";
// =====================================================================
// Auth-Utils für Server Components, Server Actions, Route Handlers
// ---------------------------------------------------------------------
// Wir kapseln die Session-Auflösung an einer Stelle, damit jede
// geschützte Route denselben Sicherheits-Pfad geht:
//
// 1. `getSession()` → optional, gibt `null` zurück
// 2. `requireSession()` → eingeloggt, sonst Redirect /login
// 3. `requireKitaSession()` → eingeloggt + kitaId vorhanden + Privacy-Consent
// 4. `requireRole()` → zusätzlich Rollen-Whitelist
//
// Mandanten-Isolation: NIEMALS `session.user.kitaId` ungeprüft an Prisma
// reichen. Über `requireKitaSession` ist garantiert, dass der Wert
// nicht-null ist und der Consent-Zeitstempel gesetzt wurde.
// =====================================================================
export type AuthenticatedSession = Session & {
user: NonNullable<Session["user"]>;
};
export type KitaSession = AuthenticatedSession & {
user: AuthenticatedSession["user"] & { kitaId: string };
};
export async function getSession(): Promise<Session | null> {
return auth();
}
export async function requireSession(): Promise<AuthenticatedSession> {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return session as AuthenticatedSession;
}
/**
* Garantiert: eingeloggter User MIT Mandantenzuordnung UND akzeptierter
* Datenschutzerklärung. Genau diese Funktion ist der Single-Point-of-Truth
* für alle Tenant-gebundenen Server Actions / Page Components.
*/
export async function requireKitaSession(): Promise<KitaSession> {
const session = await requireSession();
if (!session.user.kitaId) {
// Superadmins haben keine Kita → eigene Oberfläche.
if (session.user.role === UserRole.SUPERADMIN) {
redirect("/admin");
}
// Frisch registrierte Gründer → in den Onboarding-Wizard.
redirect("/onboarding");
}
// DSGVO-Gate: Ohne Privacy-Consent → erst Consent einholen.
// Wir prüfen das hier zentral statt in jeder Route einzeln.
const consent = await consentCheck(session.user.id);
if (!consent) {
redirect("/onboarding/consent");
}
return session as KitaSession;
}
export async function requireRole(
allowed: UserRole[],
): Promise<AuthenticatedSession> {
const session = await requireSession();
if (!allowed.includes(session.user.role)) {
redirect("/forbidden");
}
return session;
}
// ---------------------------------------------------------------------
// interne Helfer
// ---------------------------------------------------------------------
async function consentCheck(userId: string): Promise<boolean> {
// Lazy-Import, damit `auth-utils` selbst Edge-kompatibel bleibt
// (Prisma läuft nur in Node-Runtime).
const { prisma } = await import("@/lib/prisma");
const user = await prisma.user.findUnique({
where: { id: userId },
select: { privacyPolicyAcceptedAt: true },
});
return !!user?.privacyPolicyAcceptedAt;
}