Skip to content
TALA
Esc
navigateopen⌘Jpreview
On this page

Routing

All 17 SPA routes plus the catch-all, the two guard components wrapping them, the three-state session machine, and the sanitiser that closes the open-redirect hole.

Routes are declared in src/App.tsx. The <BrowserRouter basename="/"> that wraps them lives one level up in src/main.tsx; Vite’s base is / to match.

The table below lists 17 paths plus a * catch-all — 18 <Route> elements in total. Counts elsewhere in these docs say “17 routes” and mean the 17 real paths.

The session state machine

AuthProvider calls GET /api/auth/user on mount and exposes exactly three statuses. Guards branch on that status and nothing else.

Status Meaning RequireAuth RedirectIfAuthenticated
loading the GET /api/auth/user call is in flight full-screen loader full-screen loader
anonymous no session redirect to /login?next=… render children
authenticated session confirmed render children redirect to next or /home

Rendering a loader in both guards while loading is what stops the login page flashing before a valid session resolves.

Routes

Route Guard Layout Component
/login RedirectIfAuthenticated LoginPage
/register RedirectIfAuthenticated SignUpPage
/forgot-password ForgotPasswordPage
/reset-password ResetPasswordPage
/verify-email VerifyEmailPage
/loading LoadingPage — OAuth code exchange
/home RequireAuth AuthenticatedLayout HomePage
/library RequireAuth AuthenticatedLayout LibraryPage
/timeline RequireAuth AuthenticatedLayout TimelinePage
/teams RequireAuth AuthenticatedLayout TeamsPage
/workspace RequireAuth AuthenticatedLayout WorkspacePage
/workspace/:workspaceId RequireAuth AuthenticatedLayout WorkspaceDetailPage
/profile RequireAuth AuthenticatedLayout ProfilePage
/settings RequireAuth AuthenticatedLayout SettingsPage
/billing RequireAuth AuthenticatedLayout BillingPage
/invitations/:token RequireAuth InvitationPage
/profile/:username redirects to /profile
* RedirectIfAuthenticated LoginPage

The next parameter

RequireAuth encodes the attempted destination so login can return the user to it:

if (status === "anonymous") {
  const next = encodeURIComponent(location.pathname + location.search);
  return <Navigate to={`/login?next=${next}`} replace />;
}

RedirectIfAuthenticated reads it back through a sanitiser before trusting it:

function sanitizeNextPath(raw: string | null): string | null {
  if (!raw) return null;
  if (!raw.startsWith("/") || raw.startsWith("//")) return null;
  return raw;
}

Rejecting // matters: //evil.example.com is a protocol-relative URL that browsers resolve to a different origin. Without that check, ?next=//evil.example.com would be an open redirect off the login page.

/timeline means two different things

The SPA route /timeline renders a Gantt view of tasks, fed by GET /api/tasks.

The backend’s /api/timeline/* routes serve the HTTP audit log and have no consumer at all.

They share a word and nothing else. Worth renaming one of them before someone wires the wrong pair together.

Route-level gaps

/invitations/:token is wrapped in RequireAuth, so an invited user who is not signed in is bounced to login with the invitation path in next — they land back on the invitation after authenticating. That works, but it means an invitee must already have an account; there is no register-then-accept path.

/settings renders placeholder copy and calls no endpoints. /profile and /workspace/:workspaceId are thin wrappers, 10 and 18 lines respectively.

Was this page helpful?