Fix post-login redirect when the session cookie is off-origin
Signing in showed the "Welcome back!" toast but never left /login. The session cookie was host-only on the API subdomain, so the Next middleware guard on the site origin saw no cookie and bounced /dashboard straight back to /login?redirect=/dashboard. - Add AUTH_COOKIE_DOMAIN, wiring Better Auth's crossSubDomainCookies so the cookie also reaches the site origin. Unset in dev, where localhost is single-host and must stay host-only. - Navigate after authentication with a full page load, via a shared authRedirect helper: only a top-level request carries the httpOnly cookie. Used by the login, register, magic-link and Google flows. - Show "Redirecting..." on the login and register pages and keep the submit button disabled until the browser replaces the page, instead of re-enabling it mid-navigation. - Guard against a redirect loop with a sessionStorage marker. A React ref cannot do this: the full page load resets component state. If the destination bounces back, explain it rather than navigating again. - Middleware: accept any *.session_token cookie so a cookiePrefix change cannot lock everyone out, and preserve the destination's query string. - Trust any loopback port in dev, so reaching the dev server through a forwarded port does not fail Better Auth's CSRF origin check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
733d2459df
commit
dafa3711f8
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Post-authentication navigation.
|
||||
*
|
||||
* Navigation uses a full page load rather than `router.push`. The session is an httpOnly
|
||||
* cookie, and only a top-level request lets the middleware guard in `middleware.ts` see it
|
||||
* and re-render the header and the destination together.
|
||||
*
|
||||
* A marker in sessionStorage records where we last sent someone, and when. If the
|
||||
* middleware cannot see the session cookie it redirects straight back to `/login`, and
|
||||
* without the marker the login page would immediately navigate again — an endless reload
|
||||
* loop. A React ref cannot do this job: a full page load resets component state.
|
||||
*/
|
||||
const ATTEMPT_KEY = 'spanglish.auth-redirect-attempt';
|
||||
|
||||
// A real bounce comes back within a few hundred milliseconds (it is a server redirect).
|
||||
// An older marker means an unrelated later visit to /login, not a failed redirect.
|
||||
const ATTEMPT_TTL_MS = 30_000;
|
||||
|
||||
export function markRedirectAttempt(target: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify({ target, at: Date.now() }));
|
||||
} catch {
|
||||
/* storage disabled (private mode): the loop guard degrades, navigation still works */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearRedirectAttempt(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(ATTEMPT_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** True when we just sent the user to `target` and are back on the login page instead. */
|
||||
export function didRedirectBounce(target: string): boolean {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(ATTEMPT_KEY);
|
||||
if (!raw) return false;
|
||||
const { target: attempted, at } = JSON.parse(raw) as { target?: string; at?: number };
|
||||
return (
|
||||
attempted === target && typeof at === 'number' && Date.now() - at < ATTEMPT_TTL_MS
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the attempt, then navigates. Callers should keep their pending/loading UI on
|
||||
* screen: this never resolves, the page is replaced.
|
||||
*/
|
||||
export function redirectAfterAuth(target: string): void {
|
||||
markRedirectAttempt(target);
|
||||
window.location.assign(target);
|
||||
}
|
||||
Reference in New Issue
Block a user