From 6e9c7bf448b7df398a09694f3d32bb0ac7a8adb1 Mon Sep 17 00:00:00 2001 From: Emirhan Date: Fri, 26 Jun 2026 11:36:49 +0700 Subject: [PATCH] fix(auth): avoid stale redirects after auth changes Use full-page navigation after login/logout so the dashboard reloads with the fresh auth cookie, and mark login/logout responses no-store. Fixes #2100 Co-authored-by: Cursor --- src/app/(dashboard)/dashboard/profile/page.js | 5 +---- src/app/api/auth/login/route.js | 3 ++- src/app/api/auth/logout/route.js | 2 +- src/app/login/page.js | 13 ++++--------- src/shared/components/Header.js | 6 ++---- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 2851bc0c..4df8cbef 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -1,7 +1,6 @@ "use client"; import { useState, useEffect, useRef } from "react"; -import { useRouter } from "next/navigation"; import { Card, Button, Toggle, Input } from "@/shared/components"; import Modal, { ConfirmModal } from "@/shared/components/Modal"; import LanguageSwitcher from "@/shared/components/LanguageSwitcher"; @@ -21,7 +20,6 @@ function getLocaleFromCookie() { } export default function ProfilePage() { - const router = useRouter(); const { theme, setTheme, isDark } = useTheme(); const [locale, setLocale] = useState("en"); const [langOpen, setLangOpen] = useState(false); @@ -569,8 +567,7 @@ export default function ProfilePage() { try { const res = await fetch("/api/auth/logout", { method: "POST" }); if (res.ok) { - router.push("/login"); - router.refresh(); + window.location.assign("/login"); } } catch (err) { console.error("Failed to logout:", err); diff --git a/src/app/api/auth/login/route.js b/src/app/api/auth/login/route.js index e6b264dc..cbd3c6ab 100644 --- a/src/app/api/auth/login/route.js +++ b/src/app/api/auth/login/route.js @@ -8,6 +8,7 @@ import { checkLock, recordFail, recordSuccess, getClientIp } from "@/lib/auth/lo import { isLocalRequest } from "@/dashboardGuard"; const RESET_HINT = "Forgot password? Reset to default via 9Router CLI → Settings → Reset Password to Default."; +const NO_STORE_HEADERS = { "Cache-Control": "no-store" }; function isTunnelRequest(request, settings) { const host = (request.headers.get("host") || "").split(":")[0].toLowerCase(); @@ -61,7 +62,7 @@ export async function POST(request) { const mustChangePassword = !storedHash && !process.env.INITIAL_PASSWORD && !isLocalRequest(request); - return NextResponse.json({ success: true, mustChangePassword }); + return NextResponse.json({ success: true, mustChangePassword }, { headers: NO_STORE_HEADERS }); } const { remainingBeforeLock } = recordFail(ip); diff --git a/src/app/api/auth/logout/route.js b/src/app/api/auth/logout/route.js index d6a58142..2b0fa5ca 100644 --- a/src/app/api/auth/logout/route.js +++ b/src/app/api/auth/logout/route.js @@ -8,5 +8,5 @@ export async function POST() { cookieStore.delete("oidc_state"); cookieStore.delete("oidc_nonce"); cookieStore.delete("oidc_code_verifier"); - return NextResponse.json({ success: true }); + return NextResponse.json({ success: true }, { headers: { "Cache-Control": "no-store" } }); } diff --git a/src/app/login/page.js b/src/app/login/page.js index 38cf033d..8e50a191 100644 --- a/src/app/login/page.js +++ b/src/app/login/page.js @@ -2,7 +2,6 @@ import { useState, useEffect } from "react"; import { Card, Button, Input } from "@/shared/components"; -import { useRouter } from "next/navigation"; export default function LoginPage() { const [password, setPassword] = useState(""); @@ -16,7 +15,6 @@ export default function LoginPage() { const [oidcLoginLabel, setOidcLoginLabel] = useState("Sign in with OIDC"); const [mustChange, setMustChange] = useState(false); const [newPassword, setNewPassword] = useState(""); - const router = useRouter(); // Countdown for rate-limit useEffect(() => { @@ -40,8 +38,7 @@ export default function LoginPage() { if (res.ok) { const data = await res.json(); if (data.requireLogin === false) { - router.push("/dashboard"); - router.refresh(); + window.location.assign("/dashboard"); return; } setHasPassword(!!data.hasPassword); @@ -58,7 +55,7 @@ export default function LoginPage() { } } checkAuth(); - }, [router]); + }, []); const handleLogin = async (e) => { e.preventDefault(); @@ -79,8 +76,7 @@ export default function LoginPage() { setMustChange(true); return; } - router.push("/dashboard"); - router.refresh(); + window.location.assign("/dashboard"); } else { const data = await res.json(); setError(data.error || "Invalid password"); @@ -106,8 +102,7 @@ export default function LoginPage() { body: JSON.stringify({ currentPassword: password, newPassword }), }); if (res.ok) { - router.push("/dashboard"); - router.refresh(); + window.location.assign("/dashboard"); } else { const data = await res.json(); setError(data.error || "Failed to set password"); diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js index ad3c0a03..f70a0128 100644 --- a/src/shared/components/Header.js +++ b/src/shared/components/Header.js @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { usePathname, useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; import Link from "next/link"; import PropTypes from "prop-types"; import ProviderIcon from "@/shared/components/ProviderIcon"; @@ -180,7 +180,6 @@ const getPageInfo = (pathname) => { export default function Header({ onMenuClick, showMenuButton = true }) { const pathname = usePathname(); - const router = useRouter(); const [displayName, setDisplayName] = useState(""); const [loginMethod, setLoginMethod] = useState(""); const [donateOpen, setDonateOpen] = useState(false); @@ -219,8 +218,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) { try { const res = await fetch("/api/auth/logout", { method: "POST" }); if (res.ok) { - router.push("/login"); - router.refresh(); + window.location.assign("/login"); } } catch (err) { console.error("Failed to logout:", err);