fix(GRO-2099): show loading state during CustomerPortal SSO bridge bootstrap
CI / Test (pull_request) Successful in 24s
CI / Lint & Typecheck (pull_request) Successful in 29s
CI / Build & Push Docker Image (pull_request) Successful in 45s

Root cause: `Dashboard.tsx:194` runs its own `!sessionId && !isImpersonating &&
!getDevUser()` auth guard, redirecting to `/login` if `sessionId` is null. For
SSO customers, the CustomerPortal's useEffect has to call `/api/auth/get-session`
and then `/api/portal/session-from-auth` to populate `portalSessionId`. During
that bootstrap window (typically 100-300ms), `sessionId` is null and the guard
fires — redirecting the user to `/login` and breaking the post-sign-in flow.
App.tsx additionally returned `null` at `/login` for authenticated users
(`showCustomerPortal` is false at `/login`), leaving a blank React root even
if the redirect target was /login itself.

Fix:
- `CustomerPortal.tsx`: show a 'Loading…' state (`role=status`) while
  `!initComplete`. The portal chrome and its child sections only mount once
  the bootstrap has resolved, so child auth guards don't fire prematurely.
- `App.tsx`: at `/login` with a valid session, redirect to `/` so the
  customer lands on the portal instead of seeing a blank page.
- `App.tsx`: only return `LoginPage` when at `/login` — other portal
  routes defer the auth check to `CustomerPortal` (the customer SSO bridge
  resolves `portalSessionId` on mount).
- `UAT_PLAYBOOK.md`: add §5.27 with 8 cases covering the bug, the loading
  state, the /login auto-redirect, the unauth fallback, and the groomer /
  impersonation non-regressions.
- `src/__tests__/portal.test.tsx`: add a regression test that asserts the
  loading state is shown during the bridge and the portal nav is NOT in the
  DOM mid-bootstrap.

Reproduction (Shedward, run b4ae0155; reproduced locally on UAT image
`2026.06.01-ec29f71`):
1. From `about:blank`, complete customer SSO as `uat-customer`.
2. `browser_navigate` to `/portal`.
3. Pre-fix: redirected to `/login` with blank React root.
4. Post-fix: URL stays at `/portal`, dashboard renders with customer name.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-06-02 16:35:16 +00:00
parent 903ce2d675
commit f1cf58dc56
4 changed files with 107 additions and 3 deletions
+20
View File
@@ -377,6 +377,26 @@ These cases cover the `CustomerPortal` initialisation path that bridges an Authe
| TC-WEB-5.25.10 | Unauthenticated user is sent to login (no infinite loop) | Without signing in, navigate directly to `/`. | `App.tsx` renders the LoginPage. `CustomerPortal` does not render. No `session-from-auth` request is made. |
| TC-WEB-5.25.11 | Session persists across reload via Better Auth cookie | After TC-WEB-5.25.1 succeeds, reload the page. | Portal dashboard re-renders. A fresh `GET /api/auth/get-session` + `POST /api/portal/session-from-auth` pair runs and yields 200/201. Greeting still reads "Hi, &lt;FirstName&gt;". |
### 5.27 Customer Portal — Authenticated HTML-route cold mount (GRO-2099)
These cases guard against the regression where a customer who had just completed SSO sign-in was bounced back to `/login` (with a blank React root) when navigating directly to `/portal`, `/book`, `/schedule`, or even `/login` itself. Root cause: `Dashboard.tsx`'s `!sessionId && !isImpersonating && !getDevUser()` guard fired during the CustomerPortal's bootstrap — before the SSO bridge resolved `portalSessionId` — and redirected to `/login`. The fix: `CustomerPortal` now shows a loading state while the bootstrap is in flight, so the portal chrome and its `!sessionId` child guards do not mount prematurely. App.tsx additionally redirects an authenticated user at `/login` to `/` instead of rendering `null`.
**Pre-conditions:**
- TC-WEB-5.25.1 — TC-WEB-5.25.3 must pass on the build under test.
- Clear cookies and localStorage between cases.
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.27.1 | Authenticated customer lands on `/portal` after direct nav | 1. From clean state, complete TC-WEB-5.25.1 (SSO sign-in as a customer). 2. Land on `/`. 3. `browser_navigate` (full page load) directly to `/portal`. | Final URL stays at `/portal`. The React root is non-empty. The portal dashboard renders with the customer's name. No `Navigate to /login` fires. |
| TC-WEB-5.27.2 | Authenticated customer lands on `/book` and `/schedule` after direct nav | From TC-WEB-5.27.1, `browser_navigate` to `/book` then `/schedule` (one fresh navigation each). | Each final URL stays at the navigated path. The portal chrome is visible. The page does not redirect to `/login`. |
| TC-WEB-5.27.3 | Authenticated customer at `/login` is auto-redirected to `/` | From TC-WEB-5.27.1, `browser_navigate` to `/login`. | The browser ends at `/` (not at a blank `/login`). The portal dashboard renders. No blank React root at `/login`. |
| TC-WEB-5.27.4 | Loading state is visible during the bootstrap, no portal chrome flash | 1. With the UAT build under test, open DevTools → Network and throttle to Slow 3G. 2. Sign in via SSO. 3. Land on `/`. | A "Loading…" element (`role="status"`) is briefly visible. The portal nav (Home / Appointments / etc.) is NOT visible during the loading window. No `Navigate to /login` fires during the bootstrap. |
| TC-WEB-5.27.5 | SSO bridge still runs and yields 201 | From TC-WEB-5.27.4 (or TC-WEB-5.27.1), inspect Network. | The same `GET /api/auth/get-session` (200) → `POST /api/portal/session-from-auth` (201) sequence from TC-WEB-5.25.2 still runs. The customer name appears in the greeting. |
| TC-WEB-5.27.6 | Unauthenticated direct nav to `/portal` still ends at `/login` (no regression) | Clear cookies. `browser_navigate` to `/portal`. | The portal briefly shows the loading state, then `CustomerPortal`'s `!session && !portalSessionId` guard redirects to `/login`. The login form renders. No infinite loop. |
| TC-WEB-5.27.7 | Groomer SSO still works (no regression) | 1. From clean state, sign in via SSO as the groomer identity (uat-groomer). 2. Land on `/`. | `App.tsx`'s staff check redirects to `/admin`. The groomer nav renders. No `CustomerPortal` flash. No `/portal` redirect loop. |
| TC-WEB-5.27.8 | Impersonation session still works (no regression) | 1. With an active impersonation session, open `/?sessionId=<id>`. | The amber "STAFF VIEW" chrome renders. The portal loads. No `/login` redirect. |
### 5.26 Customer Portal — RescheduleFlow under SSO Bridge (GRO-2012)
These cases guard against the regression where an SSO-bridge customer (no `?sessionId=` URL param, no impersonation session) could trigger the RescheduleFlow and have `RescheduleFlow` receive `sessionId={null}`, which caused the internal `/api/book/availability` call to send `X-Impersonation-Session-Id: ` (empty) and return 401. The fix: `CustomerPortal` now passes `sessionId={session?.id ?? portalSessionId}` to `<RescheduleFlow>` (matching the fallback `renderSection()` already used).