import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { Compass, LayoutDashboard, Boxes, GitBranch, ShieldAlert, ClipboardCheck, ShieldCheck, KanbanSquare, ListChecks, Siren, BookOpenText, MessagesSquare, Network, FolderCheck, Truck, LineChart, Search, Settings, } from "lucide-react"; import { auth, signOut } from "@/server/auth"; import { dbForTenant, prisma } from "@/server/db"; import { hasPermission } from "@/server/rbac"; import { isTokenStillValid } from "@/server/sessions"; import { resolveMfaRequired } from "@/lib/mfa-policy"; import { HREF_TO_MODULE } from "@/lib/modules"; import { AUDIT_READINESS_MODULE_KEY } from "@/lib/audit-readiness/activation"; import { resolveTenantBranding } from "@/lib/brand"; import { Button } from "@/components/ui/button"; import { NavLink } from "@/components/nav-link"; import { TenantBrand } from "@/components/brand/tenant-brand"; import { TenantSwitcher } from "@/components/tenant-switcher"; import { UiLocaleSwitcher } from "@/components/ui-locale-switcher"; import { CertviaLogo } from "@/components/brand/certvia-logo"; export default async function AppLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const session = await auth(); if (!session?.user) redirect("/login"); // WS2: angemeldet, aber (noch) kein aktiver Mandant (mehrere Mitgliedschaften ohne // Slug) → Auswahlseite. MUSS vor jeder dbForTenant-Nutzung stehen (leerer tenantId). if (!session.user.tenantId) redirect("/select-tenant"); // Kontostatus (autoritativ aus DB, nicht aus dem JWT): deaktivierte Nutzer werden // abgemeldet, ein offener Passwortwechsel wird erzwungen (Force-Change). // Option C (WS4): Membership-Status kommt vom User, die globalen Auth-Zustände // (Passwortzwang, Kill-Switch, MFA, globaler Sperrstatus) von der Identity. const account = await dbForTenant(session.user.tenantId).user.findUnique({ where: { id: session.user.id }, select: { status: true }, }); const identity = session.user.identityId ? await prisma.identity.findUnique({ where: { id: session.user.identityId }, select: { status: true, mustChangePassword: true, sessionsValidAfter: true, mfaEnrolledAt: true, uiLocale: true }, }) : null; // SEC2: serverseitig entwertete Sessions (Passwort-Reset/-Wechsel) sofort abmelden. if (identity && !isTokenStillValid(session.user.tokenIssuedAt, identity.sessionsValidAfter)) { await signOut({ redirectTo: "/login" }); } if (!account || account.status !== "ACTIVE" || !identity || identity.status !== "ACTIVE") { return (

Konto deaktiviert

Ihr Zugang wurde deaktiviert. Bitte wenden Sie sich an Ihre Administration.

{ "use server"; await signOut({ redirectTo: "/login" }); }} className="mt-5">
); } if (identity.mustChangePassword) redirect("/change-password"); // SEC3-a/-b: MFA-Enrollment-Gate. Verlangt der Mandant MFA und hat die Identity weder TOTP // noch einen Passkey, wird die Einrichtung erzwungen (Pflichtseite außerhalb dieses Layouts). if (!identity.mfaEnrolledAt) { const settings = await dbForTenant(session.user.tenantId).tenantSettings.findFirst({ select: { securityPolicy: true } }); if (resolveMfaRequired(settings?.securityPolicy)) { const passkeys = session.user.identityId ? await prisma.webAuthnCredential.count({ where: { identityId: session.user.identityId } }) : 0; if (passkeys === 0) redirect("/enroll-mfa"); } } const t = await getTranslations("nav"); const tc = await getTranslations("common"); const nav = [ { href: "/dashboard", label: t("dashboard"), icon: LayoutDashboard, enabled: true }, { href: "/onboarding", label: t("onboarding"), icon: Compass, enabled: true }, // M4: nur sichtbar, wenn der Audit-Wizard scharfgeschaltet ist (Opt-in-Flag unten). { href: "/audit-readiness", label: t("auditReadiness"), icon: ShieldCheck, enabled: true, requiresAudit: true }, { href: "/assets", label: t("assets"), icon: Boxes, enabled: true }, { href: "/processes", label: t("bia"), icon: GitBranch, enabled: true }, { href: "/risks", label: t("risks"), icon: ShieldAlert, enabled: true }, { href: "/soa", label: t("soa"), icon: ClipboardCheck, enabled: true }, { href: "/measures", label: t("measures"), icon: KanbanSquare, enabled: true }, { href: "/tasks", label: t("tasks"), icon: ListChecks, enabled: true }, { href: "/incidents", label: t("incidents"), icon: Siren, enabled: true }, { href: "/policies", label: t("policies"), icon: BookOpenText, enabled: true }, { href: "/chat", label: t("chat"), icon: MessagesSquare, enabled: false }, { href: "/dependencies", label: t("dependencies"), icon: Network, enabled: true }, { href: "/evidence", label: t("evidence"), icon: FolderCheck, enabled: false }, { href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: true }, { href: "/review", label: t("review"), icon: LineChart, enabled: true }, ]; // Modul-Gating: deaktivierte Module werden ausgeblendet (§3.4) const [moduleRows, brandingSettings] = await Promise.all([ dbForTenant(session.user.tenantId).tenantModule.findMany(), dbForTenant(session.user.tenantId).tenantSettings.findUnique({ where: { tenantId: session.user.tenantId }, select: { accent: true }, }), ]); // Certvia ist der Default; ein Mandanten-Logo überschreibt später nur, wenn gesetzt (S8). const branding = resolveTenantBranding(brandingSettings); const disabledModules = new Set(moduleRows.filter((m) => !m.enabled).map((m) => m.moduleKey)); const moduleEnabled = (href: string) => { const key = HREF_TO_MODULE[href]; return !key || !disabledModules.has(key); }; // M4: Audit-Wizard ist opt-in (Default aus) — nur bei ausdrücklicher Aktivierung sichtbar. const auditEnabled = moduleRows.some((m) => m.moduleKey === AUDIT_READINESS_MODULE_KEY && m.enabled); const visibleNav = nav.filter( (item) => (!("requiresAudit" in item) || !item.requiresAudit || auditEnabled) && (!item.enabled || moduleEnabled(item.href)), ); const canManageTenant = hasPermission(session, "tenant:manage"); const initials = (session.user.name ?? "?") .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase(); async function logout() { "use server"; await signOut({ redirectTo: "/login" }); } return (

{session.user.name}

{session.user.tenantSlug}

{initials}
{children}
); }