Skip to content

Protect application routes with shared layout - #47

Merged
rd-code-23 merged 1 commit into
mainfrom
feature/dashboard
Aug 23, 2026
Merged

rd-code-23 merged 1 commit into
mainfrom
feature/dashboard

Conversation

@rd-code-23

@rd-code-23 rd-code-23 commented Aug 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • protect the application routes for authenticated users and active guests
  • redirect anonymous visitors to the welcome page
  • provide explicit session-loading and recoverable error states
  • move the shared access check and application shell into one TanStack Router pathless parent layout

Closes #39

TLDR explanation of the problem

When multiple pages need the same functionality—such as redirecting anonymous users to the welcome page—we can place them under a TanStack Router pathless layout route. The parent route wraps its child routes with shared access control and layout behavior. This is called component composition. Any new route added as a child automatically receives the same protection and application layout without changing its URL.

The problem

GoForLift currently has four routes in its main application area:

  • /dashboard
  • /routines
  • /history
  • /settings

Every one of these pages needs the same two things:

  1. Access must be limited to either a registered user with a valid server session or a visitor who deliberately entered guest mode.
  2. The page must appear inside the same responsive application shell, including the desktop sidebar and mobile navigation.

Putting those responsibilities inside each page would create repetition and make it possible for a future application page to accidentally omit its access check or shared layout.

Route structure

This PR introduces a TanStack Router pathless layout route:

_app
├── dashboard
├── routines
├── history
└── settings

The route files are:

_app.tsx
_app.dashboard.tsx
_app.routines.tsx
_app.history.tsx
_app.settings.tsx

In TanStack Router file-based routing, the underscore prefix means _app organizes and wraps its child routes without adding app to the URL. The dot establishes the parent-child relationship.

Therefore, _app.dashboard.tsx still resolves to /dashboard, not /_app/dashboard or /app/dashboard.

How the parent layout works

_app.tsx owns the behavior shared by every application page:

<DashboardAccessBoundary>
  <ApplicationShell>
    <Outlet />
  </ApplicationShell>
</DashboardAccessBoundary>

<Outlet /> is TanStack Router's placeholder for the currently matched child page. At /dashboard, it renders the dashboard content. At /routines, it renders the routines content. The access boundary and application shell remain the common parent around either child.

This is component composition: the parent route wraps each child route with shared behavior.

Access behavior

The boundary resolves access in this order:

  1. Active guest: the tab-scoped guest marker allows immediate entry without waiting for the current-user request.
  2. Authenticated user: a valid current-user result from the server allows entry.
  3. Anonymous visitor: no guest marker and no registered user redirects to /.
  4. Loading: while the server session is unresolved, the page displays Checking your session… rather than briefly showing protected content.
  5. Error: a failed session check displays a safe error and a Try again action rather than incorrectly treating a server failure as logout.

TanStack Query continues to cache the current-user response, so navigating among child routes does not inherently require a new API request every time.

Why this design

  • one source of truth for application access
  • one shared shell for desktop and mobile navigation
  • new child routes automatically receive the same protection and layout
  • page components contain only page-specific content
  • public URLs remain unchanged
  • registered-user authorization remains based on the server session; the guest marker only represents the intentionally temporary guest flow

Testing

  • authenticated visitor can enter
  • active guest can enter without waiting for the authentication API
  • anonymous visitor is redirected
  • unresolved authentication displays a loading state
  • authentication failure displays a retryable error
  • existing welcome-page and responsive-navigation tests continue to pass

Local verification:

  • TypeScript passed
  • ESLint passed
  • 19 web tests passed
  • all changed files passed Prettier

Shared test fixtures

A follow-up scan found stable test data repeated across multiple files. This PR now centralizes:

  • typed public-user factories within both the web and API test boundaries
  • the secure API session configuration previously repeated across four tests

Factories accept Partial<T> overrides so individual tests show only the variation relevant to their scenario. One-off values and behavior-defining data remain local so tests stay understandable.

The web and API public-user factories intentionally remain separate. They share the PublicUser contract through @goforlift/contracts, but their scenario defaults belong to different application boundaries and may evolve independently. A shared cross-package fixture would add coupling and package configuration without enough current reuse to justify it.

The convention is recorded once in AGENTS.md: extract stable data repeated across test files, keep scenario-specific values local, and avoid sharing fixtures across unrelated boundaries.

@rd-code-23
rd-code-23 merged commit 9005bff into main Aug 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Dashboard][Phase 1] Implement authenticated, guest, and anonymous states

1 participant