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:
Michilis
2026-07-29 20:45:05 +00:00
co-authored by Claude Opus 4.6
parent 733d2459df
commit dafa3711f8
12 changed files with 243 additions and 26 deletions
+15 -6
View File
@@ -13,16 +13,25 @@ const SESSION_COOKIES = [
'spanglish.session_token', // development
];
// Matched as a fallback so a change to Better Auth's `advanced.cookiePrefix` cannot
// silently lock every user out of these routes.
const SESSION_COOKIE_SUFFIX = '.session_token';
function hasSessionCookie(request: NextRequest): boolean {
if (SESSION_COOKIES.some((name) => !!request.cookies.get(name)?.value)) return true;
return request.cookies
.getAll()
.some((cookie) => cookie.name.endsWith(SESSION_COOKIE_SUFFIX) && !!cookie.value);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const { pathname, search } = request.nextUrl;
if (pathname.startsWith('/admin') || pathname.startsWith('/dashboard')) {
const hasSessionCookie = SESSION_COOKIES.some(
(name) => !!request.cookies.get(name)?.value
);
if (!hasSessionCookie) {
if (!hasSessionCookie(request)) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
// Keep the query string, so a bounced /admin/photos?page=3 resumes where it was.
loginUrl.searchParams.set('redirect', `${pathname}${search}`);
return NextResponse.redirect(loginUrl);
}
}