import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { orgMembersAtom } from "../api/account-atoms"; import { useOrganizationId } from "../api/organization-context"; import { canCreateWorkspaceConnectionsForHost, isTenantAdminMember, type TenantMemberRow, } from "../lib/admin-access"; import type { ShellNavItem } from "./shell"; // --------------------------------------------------------------------------- // Admin-only nav items. // // The console has no role on the session (`/account/me` returns a user and an // organization, no role), so the current member's role is read off the member // list — the same derivation cloud's org page already uses for its danger zone. // Hosts pass their admin-only entries through this hook, which drops them for // anyone the list doesn't show as an active admin/owner. // // This hides a section; it does not protect one. The page behind each item // renders a denied state off the server's own 401/403, which is the gate that // matters. Failing closed here is deliberate: while the member list is loading // or unavailable, showing nothing is better than flashing a section that will // refuse. // // That fail-closed default is also what makes this hook SSR-safe. `orgMembersAtom` // carries an explicit server value (see `api/account-atoms`), so on the server it // reads as initial without evaluating — the role is simply UNKNOWN during SSR, // which lands on `false` here and resolves client-side once the list arrives. // The hook must never suspend or throw: it runs above the routed outlet, so // anything it raises takes down the whole server-rendered document. // --------------------------------------------------------------------------- /** Whether the current member reads as an admin/owner of this tenant. */ export const useIsTenantAdmin = (): boolean => { const members = useAtomValue(orgMembersAtom); return AsyncResult.match(members, { onInitial: () => false, onFailure: () => false, onSuccess: ({ value }) => isTenantAdminMember(value.members as readonly TenantMemberRow[]), }); }; /** Whether this host and active role allow adding Workspace credentials. * Personal connection creation is available to every active member. */ export const useCanCreateWorkspaceConnections = (): boolean => { const organizationId = useOrganizationId(); const isAdmin = useIsTenantAdmin(); return canCreateWorkspaceConnectionsForHost(organizationId, isAdmin); }; /** * Append admin-only nav items to a host's nav, for admins only. * * Takes both lists so hosts keep one nav definition and the ordering stays * theirs — the admin entries land wherever the host places the call. */ export const useAdminNavItems = ( base: ReadonlyArray, adminOnly: ReadonlyArray, ): ReadonlyArray => { const isAdmin = useIsTenantAdmin(); return isAdmin ? [...base, ...adminOnly] : base; };