Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Sonar / synapse-sonar / app / (app) / layout.tsx 2753 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import Link from "next/link";
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/authOptions";
import SignOutButton from "@/components/SignOutButton";
import Logo from "@/components/Logo";
import { prisma } from "@/lib/prisma";

// Touches the DB + session per request — never prerender at build time.
export const dynamic = "force-dynamic";

/**
 * Authenticated command-center shell. The login/setup routes live outside this
 * group so they render without the navigation rail.
 */
export default async function AppShell({ children }: { children: React.ReactNode }) {
  // First-run redirect: if nobody has been provisioned yet, send to setup.
  // (Auth itself is enforced by middleware.ts.)
  const userCount = await prisma.user.count();
  if (userCount === 0) redirect("/setup");

  // Org-wide MFA policy gate: if the tenant requires MFA and this user hasn't
  // enrolled yet, force them through mandatory enrollment before anything else.
  const session = await getServerSession(authOptions);
  if (session?.user?.id) {
    const me = await prisma.user.findUnique({
      where: { id: session.user.id },
      select: { mfaEnabled: true, tenant: { select: { mfaRequired: true } } },
    });
    if (me?.tenant?.mfaRequired && !me.mfaEnabled) {
      redirect("/mfa-required");
    }
  }

  const isAdmin = session?.user?.role === "TENANT_ADMIN";

  return (
    <div className="flex h-screen">
      <aside className="flex w-56 flex-col border-r border-slate-800 bg-[#0f172a]">
        <div className="flex items-center gap-2.5 border-b border-slate-800 px-5 py-4">
          <Logo variant="icon" className="h-8 w-8" />
          <span className="font-semibold tracking-tight text-slate-100">
            Synapse<span className="text-synapse-cyan">Sonar</span>
          </span>
        </div>
        <nav className="flex flex-1 flex-col gap-1 p-3 text-sm">
          <NavLink href="/" label="Topology Map" />
          <NavLink href="/integrations" label="Integrations" />
          {isAdmin && <NavLink href="/agents" label="Agents & Collectors" />}
          {isAdmin && <NavLink href="/admin/users" label="User Administration" />}
          <NavLink href="/profile" label="Profile & Security" />
        </nav>
        <div className="border-t border-slate-800 p-3">
          <SignOutButton />
        </div>
      </aside>
      <main className="flex-1 overflow-hidden">{children}</main>
    </div>
  );
}

function NavLink({ href, label }: { href: string; label: string }) {
  return (
    <Link
      href={href}
      className="rounded-lg px-3 py-2 text-slate-300 transition hover:bg-slate-800/60 hover:text-slate-100"
    >
      {label}
    </Link>
  );
}