Skip to content

Auth: sign-in and sign-out load a new document - #77

Merged
heliocosta-dev merged 2 commits into
mainfrom
login-redirect-fix
Oct 1, 2026
Merged

heliocosta-dev merged 2 commits into
mainfrom
login-redirect-fix

Conversation

@heliocosta-dev

@heliocosta-dev heliocosta-dev commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Signing in with email and password sometimes left the user on the login form, and a reload got them in. Signing in a little later instead looped on a blank page. Both came from the client router cache. Sign-in and sign-out now load a new document.

Cause

  • Sign-out ran router.push("/login") and then router.refresh(). Next 16 runs the queued refresh before React commits /login, and the refresh re-prefetches every <Link> that is still mounted. The sidebar's prefetches went out with the session cookie already cleared. So (dashboard)/layout.tsx rendered signed out, and its redirect("/login") was cached as a complete layout segment. That one entry is shared by every dashboard route and lives for staleTimes.static (180 s).
  • Sign-in then called router.push(next). That reused the cached layout, and RedirectBoundary replayed the redirect:
    • within 30 s of signing out: /login came back from the cache, so the form reappeared and nothing seemed to happen;
    • 30–180 s after: /login was fetched again with the new cookie. It redirected to /, / redirected to /emails, and /emails replayed the cached redirect. That made an endless loop on a blank screen until the entry expired.
  • A session that expired on /onboarding cached the same entry, because that page's links into the dashboard prefetch the layout.
  • Only production builds are affected; next dev never prefetches on viewport or hover.

Change

  • auth-form.tsx: after a successful sign-in, or a sign-up that returns a session, window.location.replace(next). The target loads as a new document with empty client caches, the root layout renders in the account's language (NEXT_LOCALE is set by the sign-in), and Back skips the spent form.
  • signOutToLogin() in lib/auth-client.ts: used by the dashboard and console menus. It signs out, then loads /login as a new document, so the account's router payloads and react-query data do not outlive its session in the tab.
    • It asks about unsaved editor changes first, with confirmUnsavedNavigation(), so Cancel keeps the session.
    • The new leaveDocument() stops the editor's beforeunload prompt from asking a second time.
  • next.config.ts: the staleTimes comment no longer claims cached payloads hold no session data.

?next= dot segments (second commit)

safeNextPath compared the parsed origin and then returned the normalized path. That let /.//host come back as //host, which a browser resolves to another origin. The redirect happens after email sign-in, and in /verify-email's redirect for a visitor who is already signed in. A normalized path that starts with // now falls back like any other off-site target, and nav.test.ts lists the forms.

Verification

Local production build (next build + next start, Postgres 17), driven by headless Chromium, with every RSC request and response body logged. The new scenarios were also run against a pre-fix build of the same tree as a control.

Scenario Before After
Sign out, sign in again 5 s later back on the login form (5/5) dashboard (3/3)
Sign out, sign in again 60 s later endless loop until 180 s after sign-out (4/4) dashboard (3/3)
Session expired on /onboarding, then sign in back on the login form (1/1) dashboard (1/1)
Cold sign-in; wrong then right password; ?next=/domains pass pass
Sign out as one account, sign in as another in the same tab the first account's team showed in the switcher never shows the first account's team or email (7/7)
pt-BR account signing in from an English /login English UI until a reload Portuguese right away (2/2)
Operator signs out of the console, signs in, opens the console 404 console (2/2)
Back after sign-in login form while signed in the page before /login (8/8)
Back after sign-out (bfcache on) – /login, no dashboard content (4/4)
Sign out from an editor with unsaved changes no warning one confirm before the session ends; Cancel keeps the session, OK leaves without a second prompt

tsc, Biome and the web suite (839 tests) pass. The full browser suite above was rerun on the committed tree (19/19).

Not in this change

Server-side getSession calls drop the cookie that better-auth re-issues when it extends a session. So the browser's session cookie expires 7 days after sign-in regardless of activity, while the database row keeps sliding. That is a separate fix.

🤖 Generated with Claude Code

heliocosta-dev and others added 2 commits October 1, 2026 18:32
Signing out ran router.push("/login") then router.refresh(). Next runs
the queued refresh before React commits /login, and the refresh
re-prefetches every link still mounted: the sidebar's prefetches went
out with the session cookie already cleared, so the (dashboard) layout
rendered signed out and its redirect("/login") was cached as a complete
layout segment, shared by every dashboard route, for the 180 s static
stale time. The next sign-in's router.push(next) reused it and bounced
back to /login; past the 30 s dynamic stale time it looped
/emails -> /login -> / -> /emails on a blank screen until the entry
expired. A session that expired on /onboarding cached the same entry.

Sign-in now replaces the location with next, and sign-out (dashboard
and console menus, through signOutToLogin) loads /login, so no router
payload, react-query data or language provider from before the change
survives it. Sign-out asks about unsaved editor changes before the
session ends, and leaveDocument keeps the editor's beforeunload prompt
from asking again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
safeNextPath resolved dot segments before returning the path, so
"/.//host" came back as "//host", which a browser reads as another
origin: the sign-in form and verify-email's redirect for a signed-in
visitor could leave the app. A normalized path that starts with "//"
now falls back like any other off-site target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@heliocosta-dev
heliocosta-dev merged commit 5a38804 into main Oct 1, 2026
4 checks 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.

1 participant