Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/App.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | frontend/src/App.tsx |
| Original SHA-256 | 7b971754078f609226c867d8bfc6f1c2784cf3b7f900e7a1a3f018bbe0c4d42b |
| Git blob SHA | ebbc2da02d96501049616566728eef341da64840 |
| Reading-copy SHA-256 | 7b971754078f609226c867d8bfc6f1c2784cf3b7f900e7a1a3f018bbe0c4d42b |
Recovered source transcript
Download complete source text ↓158 lines · 6.6 KB
import type { ReactNode } from 'react'
import { Loader2 } from 'lucide-react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { AppShell } from './components/AppShell'
import { ScreeningQueue } from './tools/screenmatch/ScreeningQueue'
import { CandidateEvaluation } from './tools/screenmatch/CandidateEvaluation'
import { ScreenCompare } from './tools/screenmatch/ScreenCompare'
import { AliasSettings } from './tools/screenmatch/AliasSettings'
import { Requirements } from './screens/Requirements'
import { Candidates } from './tools/missionmatch/Candidates'
import { RecruiterDashboard } from './tools/recruitmatch/RecruiterDashboard'
import { ClientDetail } from './tools/recruitmatch/ClientDetail'
import { Clients } from './tools/recruitmatch/Clients'
import { RequisitionDetail } from './tools/recruitmatch/RequisitionDetail'
import { Pipeline } from './tools/recruitmatch/Pipeline'
import { Assignment } from './tools/recruitmatch/Assignment'
import { Analytics } from './tools/recruitmatch/Analytics'
import { Reports } from './tools/recruitmatch/Reports'
import { Tasks } from './tools/recruitmatch/Tasks'
import { Requisitions } from './tools/recruitmatch/Requisitions'
import { Slate } from './tools/slatematch/Slate'
import { CommandCenter } from './tools/missionmatch/CommandCenter'
import { Ask } from './tools/missionmatch/Ask'
import { Compliance } from './screens/Compliance'
import { NotFound } from './screens/NotFound'
import { Admin } from './screens/Admin'
import { Settings } from './screens/Settings'
import { Login } from './screens/Login'
import { AcceptInvite } from './screens/AcceptInvite'
import { AuthProvider } from './lib/AuthContext'
import { useAuth } from './lib/useAuth'
import { homePath, toolForPath } from './lib/homeTool'
import { hasTool } from './lib/can'
import { useLocation } from 'react-router-dom'
export default function App() {
return (
<AuthProvider>
<Routes>
<Route path="/accept" element={<AcceptInvite />} />
<Route path="/*" element={<Gate />} />
</Routes>
</AuthProvider>
)
}
// Gate renders the app only for a validated session; otherwise the login. The session check runs once on
// load, so a reload never flashes the app before auth resolves.
function Gate() {
const { principal, loading } = useAuth()
if (loading) {
return (
<div className="grid min-h-screen place-items-center bg-navy-950 text-muted">
<Loader2 className="animate-spin" size={22} />
</div>
)
}
if (!principal) {
return <Login />
}
return (
<Routes>
<Route element={<ToolGuard><AppShell /></ToolGuard>}>
{/* The landing route follows ENTITLEMENT, not a hard coded tool. It was an unconditional redirect
to /command, which is missionMATCH, so a user without that grant was sent to a tool the
navigation hides from them and met a permanent error with a retry that could never succeed. */}
<Route index element={<Navigate to={homePath(principal)} replace />} />
{/* missionMATCH */}
<Route path="command" element={<CommandCenter />} />
<Route path="command/candidates" element={<Candidates />} />
<Route path="command/ask" element={<Ask />} />
<Route path="compliance" element={<Compliance />} />
{/* screenMATCH */}
<Route path="screen" element={<ScreeningQueue />} />
<Route path="screen/compare" element={<ScreenCompare />} />
<Route path="screen/aliases" element={<AliasSettings />} />
<Route path="screen/evaluate/:candidateId/:requirementId" element={<CandidateEvaluation />} />
<Route path="requirements" element={<Requirements />} />
{/* recruitMATCH */}
<Route path="recruit" element={<RecruiterDashboard />} />
<Route path="recruit/clients" element={<Clients />} />
<Route path="recruit/clients/:clientId" element={<ClientDetail />} />
<Route path="recruit/requisitions" element={<Requisitions />} />
<Route path="recruit/requisitions/:reqId" element={<RequisitionDetail />} />
<Route path="recruit/requisitions/:reqId/pipeline" element={<Pipeline />} />
<Route path="recruit/applications/:appId" element={<Assignment />} />
<Route path="recruit/tasks" element={<Tasks />} />
<Route path="recruit/analytics" element={<Analytics />} />
<Route path="recruit/reports" element={<Reports />} />
{/* slateMATCH */}
<Route path="slate" element={<Slate />} />
{/* shared */}
<Route path="settings" element={<Settings />} />
{/* One access surface. /people is kept as an alias so existing links and bookmarks keep working
after the rename rather than 404ing into the NotFound screen. */}
<Route path="admin" element={<Admin />} />
<Route path="people" element={<Admin />} />
{/* Last, so it matches only when nothing else did. Without it a mistyped address rendered the
shell with an empty outlet, which reads as a crash rather than a wrong URL. */}
<Route path="*" element={<NotFound />} />
{/* Legacy paths kept so existing links and deep links keep resolving after the tool split.
/pipeline (a placeholder) and /dashboard (the pre split dashboard) used to live here too. No
link in the app reached either one, and the real pipeline is recruitMATCH's
/recruit/requisitions/:reqId/pipeline, so both were removed rather than left as screens only a
typed URL could find. */}
<Route path="command-center" element={<Navigate to="/command" replace />} />
<Route path="candidates" element={<Navigate to="/command/candidates" replace />} />
<Route path="evaluate/:candidateId/:requirementId" element={<CandidateEvaluation />} />
</Route>
</Routes>
)
}
// ToolGuard redirects a route belonging to a tool the user was not granted back to their own home tool.
//
// The route table registers every tool for every authenticated user, and cross tool links exist (a
// candidate card links into screenMATCH to evaluate), so without this a user can be walked by a BUTTON
// into a tool whose every request the API refuses. The server side gate is the enforcement; this only
// stops the interface offering a journey that ends in an error panel.
function ToolGuard({ children }: { children: ReactNode }) {
const { principal } = useAuth()
const { pathname } = useLocation()
const needed = toolForPath(pathname)
if (needed && !hasTool(principal, needed)) {
return <Navigate to={homePath(principal)} replace />
}
return <>{children}</>
}