// The admin shell — and the AUTH GATE. This nested layout wraps every page // under `/admin`, and its `loader` runs server-side BEFORE the page renders, so // it's the right seam to validate the session. Throwing RedirectError // short-circuits the subtree (303 + Location on SSR; replace-nav on the client). // Pages under here are `renderMode: 'ssr'` so the gate runs per request. // // The sidebar is DRIVEN BY THE API'S CAPABILITY MAP: useCapabilityManifest + // deriveEntityAdmins discover every entity at runtime, so point the admin at any // api and the nav adapts — no per-entity code. The undo bar wraps the // __voltro.undo.* built-ins (requires undo capture on; the dev default). import type { ReactNode } from 'react' import { RedirectError, useLoaderData, useLocation, type LoaderFn } from '@voltro/web' import { useCapabilityManifest, deriveEntityAdmins, useUndoLog, PermissionProvider, } from '@voltro/client' import { T, useLocale, useT } from '@voltro/i18n' import { LocaleSwitcher } from '@voltro/ui-shadcn' import { APP_NAME } from '../../config' import { isSignedIn } from '../../lib/auth' import { DEMO_SCOPES } from '../../lib/admin' const LOCALES = [ { code: 'en', label: 'English' }, { code: 'de', label: 'Deutsch' }, ] export const renderMode = 'ssr' as const interface AdminData { readonly user: { readonly name: string } } export const loader: LoaderFn = async ({ headers }) => { if (!isSignedIn(headers)) { throw new RedirectError('/login?from=/admin') } return { user: { name: 'Demo Admin' } } } // The undo bar — split out so a deployment running with VOLTRO_UNDO=off (no // __voltro.undo.* built-ins) can delete just these lines. const UndoBar = (): ReactNode => { const undo = useUndoLog('app') return ( <> ) } export default function AdminLayout({ children }: { readonly children: ReactNode }): ReactNode { const { user } = useLoaderData() const pathname = useLocation() const locale = useLocale() const langLabel = useT('lang.label') const { manifest, loading } = useCapabilityManifest('app') const entities = manifest ? deriveEntityAdmins(manifest) : [] const navItem = (href: string, label: ReactNode): ReactNode => ( {label} ) return ( // PermissionProvider feeds the current subject's scopes to useAccessDecision // across the admin, which judges them against each procedure's OWN declared // guards. DEMO scopes here — swap for your session's (see lib/admin.ts).
{user.name}
{children}
) }