diff --git a/src/App.tsx b/src/App.tsx index a60ad72..4a861ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,21 @@ +import { useAuth } from '@clerk/react' +import type { ReactNode } from 'react' import { Navigate, Route, Routes } from 'react-router-dom' import { AuthGate } from './auth/AuthGate' -import { AppLayout } from './components/AppLayout' +import { AppLayout, ORG_ADMIN } from './components/AppLayout' +import { BillingPage } from './pages/Billing' import { BrainPage } from './pages/Brain' import { ProductionPage } from './pages/Production' import { InstructionPage } from './pages/Instruction' import { SchedulePage } from './pages/Schedule' import { ResultsPage } from './pages/Results' +// For a member who opens the link directly; the engine refuses them the data either way. +function AdminOnly({ children }: { children: ReactNode }) { + const { orgRole } = useAuth() + return orgRole === ORG_ADMIN ? children : +} + export default function App() { return ( @@ -17,6 +26,7 @@ export default function App() { } /> } /> } /> + } /> {/* Paths from before the routes were renamed, so old links keep working. */} } /> diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 63e02a6..59fc3f9 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react' -import { useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react' +import { useAuth, useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react' import { useQuery } from '@tanstack/react-query' import { NavLink, Outlet, useLocation } from 'react-router-dom' import { LANGS, locale, setLang, t, useLang, type Key } from '../i18n' @@ -11,9 +11,12 @@ import { ErrorBoundary } from './ErrorBoundary' // Learning Studio (learning-lab/frontend); both apps share one Clerk instance, so the user stays signed in. const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL +/** The Clerk role the engine treats as the organization's owner (`require_org_owner`). */ +export const ORG_ADMIN = 'org:admin' + // The Production dashboard shows what Express is making; the four settings doors run it: // feed the Brain, give an instruction, watch the schedule, collect results. -export const NAV_GROUPS: { label: Key; items: { to: string; label: Key; icon: IconName }[] }[] = [ +export const NAV_GROUPS: { label: Key; adminOnly?: boolean; items: { to: string; label: Key; icon: IconName }[] }[] = [ { label: 'nav.dashboard', items: [{ to: '/production', label: 'nav.production', icon: 'chart' }] }, { label: 'nav.settings', @@ -24,6 +27,7 @@ export const NAV_GROUPS: { label: Key; items: { to: string; label: Key; icon: Ic { to: '/results', label: 'nav.results', icon: 'layers' }, ], }, + { label: 'nav.admin', adminOnly: true, items: [{ to: '/admin/billing', label: 'nav.billing', icon: 'coin' }] }, ] export const NAV = NAV_GROUPS.flatMap(g => g.items) @@ -150,6 +154,7 @@ export function AppLayout() { const [help, setHelp] = useState(false) const { user } = useUser() const { signOut } = useClerk() + const { orgRole } = useAuth() const { pathname } = useLocation() const current = NAV.find(n => pathname.startsWith(n.to)) // Nav groups stay closed unless they hold the current page; a click on the label overrides that until the next route. @@ -178,7 +183,7 @@ export function AppLayout() {