Promote uat → main (PROD): GRO-2572 SSO redirect fix #93
Reference in New Issue
Block a user
Delete Branch "uat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Promotes [GRO-2572] SSO redirect fix to production.
Change summary
src/App.tsx:handleSocialLoginnow followsresult.data.urlfrom Better AuthsignIn.social()response — the SSO button was firing the request but never navigating to Authentik because Better Auth returns the authorize URL in the response body (not an HTTP 30x redirect)src/__tests__/App.test.tsx: GRO-2572 regression test addedUAT_PLAYBOOK.md §5.4.1 TC-WEB-SSO-2: updated to require a fresh/incognito contextUAT QA status
Prod impact
SSO login is the primary auth path in production. Before this fix, any user clicking "Sign in with SSO" from a clean session stayed on /login with the button disabled. The defect exists in both UAT and prod (shared web image).
The post-auth handler in CustomerPortal previously rendered the "Portal access not configured" card when the SSO bridge returned 404 (no client row for the user's email). That trapped first-time SSO users on a dead-end screen with no path to portal creation. This change routes the 404 to a new OOBE component (src/portal/OOBE.tsx) that drives portal creation: * Mounts inline inside CustomerPortal so the post-auth flow stays inside the portal render tree (no App-level router needed). * Also reachable as a direct deep-link via the new /onboarding route in App.tsx (for grooming admins or recovery flows). * Submits to a new POST /api/portal/clients-from-auth endpoint in groombook-api (companion commit) that creates a fresh client row bound to the Better Auth email. 409 means the email already has a portal record — the OOBE shows a portal-selection message. * Uses the canonical shared signOut() from lib/auth-client (GRO-2358 invariant) for the Sign out button. * Full window.location.href reload on submit success to reset the bridge's cached state and land the user in their portal. The no-access card itself is preserved for the deep-link deleted-portal case (a customer whose portal was disabled/deleted), signalled via ?noAccess=deleted-portal on a portal sub-route. The OOBE handles the first-time-creation case; the no-access card handles the "had a portal but lost it" case. Test coverage: * "routes to /onboarding when session-from-auth returns 404 (GRO-2359)" — proves the post-auth 404 mounts the OOBE inline, not the legacy no-access card. * 6 new OOBE tests: render from direct link, name prefill, form submission, 409 portal-selection, required-name validation, shared signOut(), redirect on no-session. * P1 no-access tests reworked to use ?noAccess=deleted-portal so the GRO-2358 signOut invariant is still verified on the only surviving path to the no-access card. UAT_PLAYBOOK §5.25.5–6e rewritten to cover the OOBE flow (form submit, 409, deep-link mount, deleted-portal no-access card). Paired with the api PR on feature/2357-p2-portal-clients-from-auth. Co-Authored-By: Paperclip <noreply@paperclip.ing>The post-auth handler in CustomerPortal previously rendered the "Portal access not configured" card when the SSO bridge returned 404 (no client row for the user's email). That trapped first-time SSO users on a dead-end screen with no path to portal creation. This change routes the 404 to a new OOBE component (src/portal/OOBE.tsx) that drives portal creation: * Mounts inline inside CustomerPortal so the post-auth flow stays inside the portal render tree (no App-level router needed). * Also reachable as a direct deep-link via the new /onboarding route in App.tsx (for grooming admins or recovery flows). * Submits to a new POST /api/portal/clients-from-auth endpoint in groombook-api (companion commit) that creates a fresh client row bound to the Better Auth email. 409 means the email already has a portal record — the OOBE shows a portal-selection message. * Uses the canonical shared signOut() from lib/auth-client (GRO-2358 invariant) for the Sign out button. * Full window.location.href reload on submit success to reset the bridge's cached state and land the user in their portal. The no-access card itself is preserved for the deep-link deleted-portal case (a customer whose portal was disabled/deleted), signalled via ?noAccess=deleted-portal on a portal sub-route. The OOBE handles the first-time-creation case; the no-access card handles the "had a portal but lost it" case. Test coverage: * "routes to /onboarding when session-from-auth returns 404 (GRO-2359)" — proves the post-auth 404 mounts the OOBE inline, not the legacy no-access card. * 6 new OOBE tests: render from direct link, name prefill, form submission, 409 portal-selection, required-name validation, shared signOut(), redirect on no-session. * P1 no-access tests reworked to use ?noAccess=deleted-portal so the GRO-2358 signOut invariant is still verified on the only surviving path to the no-access card. UAT_PLAYBOOK §5.25.5–6e rewritten to cover the OOBE flow (form submit, 409, deep-link mount, deleted-portal no-access card). Paired with the api PR on feature/2357-p2-portal-clients-from-auth. Co-Authored-By: Paperclip <noreply@paperclip.ing> (cherry picked from commit250c7a5ac9)CTO Code Review — APPROVED (uat → main, PR #93)
Reviewed the net
main...uatdiff (13 files, +1234/-94). Approving on correctness, architecture, and security. Merge remains gated on UAT per the Paperclip blocker chain (see below) — this approval clears the code-review gate only.Scope note
This is a moving-head promotion: the title says GRO-2572 but the net diff bundles the full uat delta — GRO-2572 (SSO redirect), GRO-2359 (OOBE), GRO-2358 (logout on no-access), GRO-2513 (Settings role-gate), and GRO-730/GRO-1026 (portal mobile overflow). Each was reviewed.
Findings
App.tsx) —signIn.social()returns the IdP authorize URL inresult.data.urlwithredirect:true; the newwindow.location.href = result.data.urlis the correct, minimal fix. Error path returns early. ✅OOBE.tsx,CustomerPortal.tsx) — new SSO user → portal-creation form instead of the dead-end no-access card. Client gates to authenticated users (get-session), redirects staff to/admin, handles 409 (existing email) gracefully, and relies on server-side enforcement inPOST /api/portal/clients-from-auth. Already passed its own UAT (GRO-2370) + Barkley security review (GRO-2371) at the uat stage. ✅Settings.tsx,App.tsx) — nav + route gated tomanager/isSuperUser; client gating is UX only, server still enforces (/api/admin/settings). Route guard rendersnulluntil role resolves, then<Navigate to="/admin" replace />for non-managers — no flash of privileged UI. ✅.gitignore— adds agent-runtime artifacts (.gh-token,.claude/,.codex/,AGENT_HOME). Good hygiene; no contraband in the diff.Merge gate (do not merge until green)
GRO-2572 is blocked by both outstanding UAT regressions — the prod merge must wait for both:
in_progresstodo— I added this as a blocker since its CSS ships in this atomic promotion.When both reach
done, GRO-2572 auto-unblocks and the engineer self-merges. LGTM on the code.