Skip to content

fix(auth): #154 make auth redirects basePath-aware (email confirm bounced to domain root)#156

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/154-basepath-auth-redirects
Jul 3, 2026
Merged

fix(auth): #154 make auth redirects basePath-aware (email confirm bounced to domain root)#156
TortoiseWolfe merged 1 commit into
mainfrom
fix/154-basepath-auth-redirects

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #154

What was happening

On a GitHub Pages project-site deploy (any fork without a CNAME, e.g. RescueDogs at tortoisewolfe.github.io/RescueDogs), a new user who clicked the Supabase confirmation email briefly landed in a signed-in state, then got bounced to the bare domain root where no app exists. Sign-up conversion was fully broken in production.

Two defects stacked:

  1. Every hand-built auth URL ignored the basePath. emailRedirectTo was ${window.location.origin}/auth/callback (AuthContext.tsx), and the cross-tab sign-out handler plus signOut() did window.location.href = '/' — the domain root, not the app root. Same class of bug in the OAuth redirectTo, the password-reset redirectTo, and the verify-email / sign-in / error-boundary hard navigations.
  2. The resolved basePath never reached client code in production. The deploy workflows intentionally omit NEXT_PUBLIC_BASE_PATH and rely on scripts/detect-project.jsproject-detected.jsonnext.config.ts. That value configured the router, but getProjectConfig() in the browser saw ''. So even a "basePath-aware" URL builder would have produced the wrong URL in the deployed bundle.

The visible bounce was the cross-tab SIGNED_OUT branch: during email confirmation no auth token exists yet, so the token-validity guard classifies the transient SIGNED_OUT as real and the handler hard-navigated to '/' — off the app.

The fix

  • src/config/project.config.ts: new getInternalUrl(path) (basePath-prefixed, trailing-slash canonical — trailingSlash: true exports emit route/index.html, and Supabase's redirect allow-list is exact-match, so the slashless form costs a Pages 301 that can break allow-list matching) and getRedirectUrl(path) (runtime origin + basePath, for Supabase redirect params). With an empty basePath both degrade to exactly the old strings.
  • next.config.ts: inject the resolved basePath into the client bundle via the env block. This is the load-bearing line for forks — verified by building with a basePath and finding basePath:"/ScriptHammer" inlined in the emitted client chunk.
  • Routed all nine call sites through the helpers (AuthContext emailRedirectTo/signOut/cross-tab, OAuthButtons, ForgotPasswordForm, SignInForm, sign-up page, reset-password page, ErrorBoundary).
  • Guarded the cross-tab SIGNED_OUT redirect so it never fires on /auth/callback — the callback page owns its own error handling, and a transient SIGNED_OUT there is expected during confirmation.

Tests

  • src/config/__tests__/project.config.test.ts: helper behavior with/without basePath, trailing-slash canonicalization, query/hash passthrough, absolute redirect composition.
  • tests/unit/auth/use-auth.test.tsx: with NEXT_PUBLIC_BASE_PATH=/RescueDogs — prefixed emailRedirectTo at sign-up, prefixed signOut() redirect, prefixed cross-tab SIGNED_OUT redirect, and no redirect when SIGNED_OUT fires on the callback page (the exact pre-fix bounce).

Suite: unit tests, type-check, lint, gitleaks all green in the container. Existing E2E can't see this class of bug (it builds without a basePath) — follow-up issue coming for a basePath-enabled E2E project.

What this does NOT fix by itself

The Supabase project for each deployed fork needs its dashboard config to match (Site URL inside the app, prefixed callback URLs in the redirect allow-list). For RescueDogs (cmdhajshektesctrappl): Site URL https://tortoisewolfe.github.io/RescueDogs, allow-list additions https://tortoisewolfe.github.io/RescueDogs/auth/callback/, https://tortoisewolfe.github.io/RescueDogs/reset-password/, https://tortoisewolfe.github.io/RescueDogs/**, plus the two localhost equivalents for dev. That project lives in a Supabase account the local tooling has no token for, so it's a dashboard task.

Full-loop verification (real confirmation email → land under /RescueDogs/auth/callback/ → profile) only closes in production after the RescueDogs cherry-pick deploys; not claiming it here.

🤖 Generated with Claude Code

…nced to domain root)

Auth flow built URLs from window.location.origin and hardcoded '/',
ignoring the GitHub Pages basePath. On a project-site deploy the
confirmation flow therefore stranded new users at the domain root.

- add getInternalUrl/getRedirectUrl to project.config (trailing-slash
  canonical, degrade to old strings when basePath is empty)
- inject the resolved basePath into the client bundle via next.config
  env (deploy workflows omit NEXT_PUBLIC_BASE_PATH on purpose, so the
  detected value never reached client code in production)
- route emailRedirectTo, OAuth redirectTo, password-reset redirectTo and
  all hard navigations through the helpers
- guard the cross-tab SIGNED_OUT redirect so it can never hijack
  /auth/callback (transient SIGNED_OUT during confirmation passes the
  token-validity check because no token exists yet)
- pin the seam with unit tests: prefixed emailRedirectTo, prefixed
  signOut/cross-tab redirects, and no redirect on the callback page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@TortoiseWolfe TortoiseWolfe merged commit e6861e6 into main Jul 3, 2026
17 of 18 checks passed
@TortoiseWolfe TortoiseWolfe deleted the fix/154-basepath-auth-redirects branch July 3, 2026 14:52
TortoiseWolfe added a commit that referenced this pull request Jul 3, 2026
…copes on rendered rows (#164)

The payment-isolation "shows only own payments" E2E asserts
`hasNoPayments || hasPayments`, counting `[data-payment-item]` for the
list branch. PaymentHistory never emitted that attribute, so the test
only passed via the empty-state — it went red the moment the shared
fixture user acquired a seeded payment row (verified via the failure
trace: query returned 200 with one row, component rendered the table,
neither test branch matched). Not flake, not a regression from #156 —
a latent test-selector gap. Confirmed pre-existing by re-running E2E on
the pre-#156 commit (also red).

Adds data-payment-item to the desktop table rows and mobile cards so
the list branch matches actual rendered payments.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
TortoiseWolfe added a commit that referenced this pull request Jul 4, 2026
payment-isolation.spec.ts:113 went red on all 3 browsers on main after
#156 shifted session/redirect timing. Root cause (reproduced): under
concurrent CI backend load, getPaymentHistory()'s first attempt can hit a
transient Realtime/RLS 401/406 during session hydration and throw. The
component then rendered a THIRD terminal state — the 'Error loading
payment history' alert — which is neither the empty-state heading nor a
[data-payment-item], so the test's hasNoPayments||hasPayments assertion
was false. Locally (uncontended backend) the test passes 9/9, confirming
it's load-sensitivity, not broken logic.

Fix (root cause, helps prod too): PaymentHistory.refetch now retries a
failed fetch up to 3x with linear backoff (300/600ms) before surfacing
the error alert — a momentary blip no longer strands the user on an error
screen. The E2E assertion is also made resilient: expect.poll for a stable
terminal state instead of asserting on the first post-loading frame, so it
never races the retry window.

Co-authored-by: TurtleWolfe <TurtleWolfe@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant