feat(front): guard the account, sign-in and sign-up pages on the Session #542

Merged
root merged 10 commits from claude/spec-409-implementation-862587 into main 2026-10-15 22:35:36 +00:00
root commented 2026-10-15 22:11:53 +00:00 (Migrated from gitlab.core-project.space)

Summary

Each route reads the Account through Query in its beforeLoad (ADR 0019), so the page never renders before the redirect.

/account  beforeLoad requireSession
  signed-out → redirect /sign-in?redirect=<location.href>   (focusHeading)
/sign-in, /sign-up  beforeLoad leaveWhenSignedIn
  signed-in, on opening → redirect /                        (focusHeading)
  signed-in by this page ("stay") → stay for the claim offer (ADR 0035)
sign-in / sign-up success
  claim offer, if any → navigate to redirect (same-site path only) ?? "/"

The guards re-run when a Session changes. There is one place for it, not a call at every place a Session changes:

app/router.tsx  runGuardsAgainOnSessionChange
  Account query result lands, signed-in ↔ signed-out flipped → router.invalidate()

This covers sign-in, sign-up, sign-out, reset, deletion and a refused refresh. It fires after the new Account is read, so no sleep(1) is needed.

 apps/front/src/
 ├── auth/
+│   ├── services/route-guards.ts   # requireSession, leaveWhenSignedIn, validateDestinationSearch
+│   └── route-guards.test.tsx      # S1
-│   account.page.tsx: signed-out panel and its copy (en, fr)
+├── design-system/landing-heading.tsx  # the heading a redirect focuses, shared with home
+└── lib/focus-heading.ts               # the focusHeading history state

Decisions the spec left open:

  • Deleting an Account still lands home. deleteAccountMutationOptions takes onDeleted and goes home before the reset. Otherwise the account guard would send the Player to sign-in.
  • The remembered destination is the redirect search parameter, as in the TanStack example. Only this site's origin is accepted, so https://…, //… and /\… are refused.
  • If the Account read fails on sign-in or sign-up, the router's error component shows, as it does on the account page.
  • The e2e password-reset test now tries the old password from a second device. The first device is still signed in, so it is now sent home from /sign-in.

Evidence

  • Before: with no Session, /account shows the signed-out panel; after signing in, the Player lands on /. A signed-in Player can open /sign-in.
    After: route-guards.test.tsx (S1) passes:

    no Session → /account?section=data → lands on /sign-in?redirect=… → signs in → /account?section=data
    signs up instead → same destination
    claim offer open on /sign-in → "Not now" → destination
    sign-in refused → stays on sign-in, destination kept
    redirect=https://elsewhere, //elsewhere, /\elsewhere → home
    Account read fails → router error, account.me called once
    signed in → /sign-in or /sign-up → home, sign-in heading never in the DOM
    signs in on /sign-in → guard re-run → still on /sign-in with the offer
    signed out / refresh refused on /account → sign-in, tab kept, heading focused
    

    S2: accounts.spec.ts opens the account page with no Session, signs in and lands on the account page. It then opens sign-in while signed in and lands home. The full e2e suite passed 43/43 locally.

  • Gate on node 24: lint, typecheck, build, format:check and test --force (1397 tests) are green. Drag tests timed out a few times when the machine was under heavy load, then passed on rerun.

Merge Danger

Door: two-way

Blast Radius: auth navigation

The account, sign-in and sign-up routes and every Session change now re-run the guards. A wrong guard would send a Player to the wrong page; it causes no data loss. The heading-focus refactor also touches home and the Room entry.

Closes #409

## Summary Each route reads the Account through Query in its `beforeLoad` (ADR 0019), so the page never renders before the redirect. ```text /account beforeLoad requireSession signed-out → redirect /sign-in?redirect=<location.href> (focusHeading) /sign-in, /sign-up beforeLoad leaveWhenSignedIn signed-in, on opening → redirect / (focusHeading) signed-in by this page ("stay") → stay for the claim offer (ADR 0035) sign-in / sign-up success claim offer, if any → navigate to redirect (same-site path only) ?? "/" ``` The guards re-run when a Session changes. There is one place for it, not a call at every place a Session changes: ```text app/router.tsx runGuardsAgainOnSessionChange Account query result lands, signed-in ↔ signed-out flipped → router.invalidate() ``` This covers sign-in, sign-up, sign-out, reset, deletion and a refused refresh. It fires after the new Account is read, so no `sleep(1)` is needed. ```diff apps/front/src/ ├── auth/ +│ ├── services/route-guards.ts # requireSession, leaveWhenSignedIn, validateDestinationSearch +│ └── route-guards.test.tsx # S1 -│ account.page.tsx: signed-out panel and its copy (en, fr) +├── design-system/landing-heading.tsx # the heading a redirect focuses, shared with home +└── lib/focus-heading.ts # the focusHeading history state ``` Decisions the spec left open: - Deleting an Account still lands home. `deleteAccountMutationOptions` takes `onDeleted` and goes home before the reset. Otherwise the account guard would send the Player to sign-in. - The remembered destination is the `redirect` search parameter, as in the TanStack example. Only this site's origin is accepted, so `https://…`, `//…` and `/\…` are refused. - If the Account read fails on sign-in or sign-up, the router's error component shows, as it does on the account page. - The e2e password-reset test now tries the old password from a second device. The first device is still signed in, so it is now sent home from `/sign-in`. ## Evidence - **Before:** with no Session, `/account` shows the signed-out panel; after signing in, the Player lands on `/`. A signed-in Player can open `/sign-in`. **After:** `route-guards.test.tsx` (S1) passes: ```text no Session → /account?section=data → lands on /sign-in?redirect=… → signs in → /account?section=data signs up instead → same destination claim offer open on /sign-in → "Not now" → destination sign-in refused → stays on sign-in, destination kept redirect=https://elsewhere, //elsewhere, /\elsewhere → home Account read fails → router error, account.me called once signed in → /sign-in or /sign-up → home, sign-in heading never in the DOM signs in on /sign-in → guard re-run → still on /sign-in with the offer signed out / refresh refused on /account → sign-in, tab kept, heading focused ``` S2: `accounts.spec.ts` opens the account page with no Session, signs in and lands on the account page. It then opens sign-in while signed in and lands home. The full e2e suite passed 43/43 locally. - Gate on node 24: lint, typecheck, build, format:check and `test --force` (1397 tests) are green. Drag tests timed out a few times when the machine was under heavy load, then passed on rerun. ## Merge Danger **Door:** two-way **Blast Radius:** auth navigation The account, sign-in and sign-up routes and every Session change now re-run the guards. A wrong guard would send a Player to the wrong page; it causes no data loss. The heading-focus refactor also touches home and the Room entry. Closes #409
root commented 2026-10-15 22:12:25 +00:00 (Migrated from gitlab.core-project.space)

added 14 commits

  • e745a04b...7cbb0647 - 4 commits from branch main
  • 348a3e7f - feat(front): guard the account, sign-in and sign-up pages on the Session
  • d76c8f38 - refactor(front): drop the account page's signed-out copy, which nothing renders
  • 04f2d271 - test(e2e): send the account page to sign in and back, and a signed-in Player from sign-in home
  • 6b9450e6 - test(front): share the sign-in, sign-up and navigation helpers through the auth harness
  • f85fbc32 - test(front): keep a Player who signs in on the sign-in page there for the claim offer
  • 10e9ca77 - refactor(front): keep the heading a redirect lands on, and the state asking for it, in one place
  • bdda7769 - refactor(front): match on the Account in the route guards, and name their search check
  • a67587bc - refactor(front): say an auth link keeps the destination instead of passing a bare search
  • f3da16ce - refactor(front): track the Session's kind without an unread sentinel in the router
  • d31d0cea - test(e2e): sign in on the page a redirect led to through the shared sign-in helper

Compare with previous version

added 14 commits <ul><li>e745a04b...7cbb0647 - 4 commits from branch <code>main</code></li><li>348a3e7f - feat(front): guard the account, sign-in and sign-up pages on the Session</li><li>d76c8f38 - refactor(front): drop the account page&#39;s signed-out copy, which nothing renders</li><li>04f2d271 - test(e2e): send the account page to sign in and back, and a signed-in Player from sign-in home</li><li>6b9450e6 - test(front): share the sign-in, sign-up and navigation helpers through the auth harness</li><li>f85fbc32 - test(front): keep a Player who signs in on the sign-in page there for the claim offer</li><li>10e9ca77 - refactor(front): keep the heading a redirect lands on, and the state asking for it, in one place</li><li>bdda7769 - refactor(front): match on the Account in the route guards, and name their search check</li><li>a67587bc - refactor(front): say an auth link keeps the destination instead of passing a bare search</li><li>f3da16ce - refactor(front): track the Session&#39;s kind without an unread sentinel in the router</li><li>d31d0cea - test(e2e): sign in on the page a redirect led to through the shared sign-in helper</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/42/diffs?diff_id=260&start_sha=e745a04bae1774c20f527c111e84029ec3ca2e5a)
root commented 2026-10-15 22:17:10 +00:00 (Migrated from gitlab.core-project.space)

added 18 commits

  • d31d0cea...eb0d0b8e - 8 commits from branch main
  • 4386078b - feat(front): guard the account, sign-in and sign-up pages on the Session
  • f42c5dca - refactor(front): drop the account page's signed-out copy, which nothing renders
  • 54bd79bc - test(e2e): send the account page to sign in and back, and a signed-in Player from sign-in home
  • 71b511d6 - test(front): share the sign-in, sign-up and navigation helpers through the auth harness
  • 8fc8aa62 - test(front): keep a Player who signs in on the sign-in page there for the claim offer
  • 06afab51 - refactor(front): keep the heading a redirect lands on, and the state asking for it, in one place
  • f7d0eddc - refactor(front): match on the Account in the route guards, and name their search check
  • 1e95eab2 - refactor(front): say an auth link keeps the destination instead of passing a bare search
  • 86f8a1fd - refactor(front): track the Session's kind without an unread sentinel in the router
  • ee4cc4e0 - test(e2e): sign in on the page a redirect led to through the shared sign-in helper

Compare with previous version

added 18 commits <ul><li>d31d0cea...eb0d0b8e - 8 commits from branch <code>main</code></li><li>4386078b - feat(front): guard the account, sign-in and sign-up pages on the Session</li><li>f42c5dca - refactor(front): drop the account page&#39;s signed-out copy, which nothing renders</li><li>54bd79bc - test(e2e): send the account page to sign in and back, and a signed-in Player from sign-in home</li><li>71b511d6 - test(front): share the sign-in, sign-up and navigation helpers through the auth harness</li><li>8fc8aa62 - test(front): keep a Player who signs in on the sign-in page there for the claim offer</li><li>06afab51 - refactor(front): keep the heading a redirect lands on, and the state asking for it, in one place</li><li>f7d0eddc - refactor(front): match on the Account in the route guards, and name their search check</li><li>1e95eab2 - refactor(front): say an auth link keeps the destination instead of passing a bare search</li><li>86f8a1fd - refactor(front): track the Session&#39;s kind without an unread sentinel in the router</li><li>ee4cc4e0 - test(e2e): sign in on the page a redirect led to through the shared sign-in helper</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/42/diffs?diff_id=266&start_sha=d31d0cea0ed59177d02e8b37bbd1db12381c8ae5)
project_1_bot_a8dd9262857d850eef8dca5206e61de5 commented 2026-10-15 22:29:47 +00:00 (Migrated from gitlab.core-project.space)

Screenshots

Before is production, after is this merge request's review app at ee4cc4e.

Desktop

Page Before After
Home
Local game
Room

Mobile

Page Before After
Home
Local game
Room
<!-- chess-screenshots --> ### Screenshots Before is production, after is this merge request's review app at ee4cc4e. #### Desktop | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/home-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/home-desktop.png" width="400"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/local-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/local-desktop.png" width="400"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/room-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/room-desktop.png" width="400"> | #### Mobile | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/home-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/home-mobile.png" width="200"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/local-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/local-mobile.png" width="200"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/before/room-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1580/artifacts/raw/review-screenshots/after/room-mobile.png" width="200"> |
root (Migrated from gitlab.core-project.space) merged commit into main 2026-10-15 22:35:36 +00:00
Sign in to join this conversation.
No description provided.