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 <cursoragent@cursor.com>
This commit is contained in:
Emirhan
2026-06-26 11:39:49 +07:00
committed by decolua
co-authored by Cursor
parent ab5ec52f28
commit 6e9c7bf448
5 changed files with 10 additions and 19 deletions
@@ -1,7 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { Card, Button, Toggle, Input } from "@/shared/components"; import { Card, Button, Toggle, Input } from "@/shared/components";
import Modal, { ConfirmModal } from "@/shared/components/Modal"; import Modal, { ConfirmModal } from "@/shared/components/Modal";
import LanguageSwitcher from "@/shared/components/LanguageSwitcher"; import LanguageSwitcher from "@/shared/components/LanguageSwitcher";
@@ -21,7 +20,6 @@ function getLocaleFromCookie() {
} }
export default function ProfilePage() { export default function ProfilePage() {
const router = useRouter();
const { theme, setTheme, isDark } = useTheme(); const { theme, setTheme, isDark } = useTheme();
const [locale, setLocale] = useState("en"); const [locale, setLocale] = useState("en");
const [langOpen, setLangOpen] = useState(false); const [langOpen, setLangOpen] = useState(false);
@@ -569,8 +567,7 @@ export default function ProfilePage() {
try { try {
const res = await fetch("/api/auth/logout", { method: "POST" }); const res = await fetch("/api/auth/logout", { method: "POST" });
if (res.ok) { if (res.ok) {
router.push("/login"); window.location.assign("/login");
router.refresh();
} }
} catch (err) { } catch (err) {
console.error("Failed to logout:", err); console.error("Failed to logout:", err);
+2 -1
View File
@@ -8,6 +8,7 @@ import { checkLock, recordFail, recordSuccess, getClientIp } from "@/lib/auth/lo
import { isLocalRequest } from "@/dashboardGuard"; import { isLocalRequest } from "@/dashboardGuard";
const RESET_HINT = "Forgot password? Reset to default via 9Router CLI → Settings → Reset Password to Default."; 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) { function isTunnelRequest(request, settings) {
const host = (request.headers.get("host") || "").split(":")[0].toLowerCase(); const host = (request.headers.get("host") || "").split(":")[0].toLowerCase();
@@ -61,7 +62,7 @@ export async function POST(request) {
const mustChangePassword = const mustChangePassword =
!storedHash && !process.env.INITIAL_PASSWORD && !isLocalRequest(request); !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); const { remainingBeforeLock } = recordFail(ip);
+1 -1
View File
@@ -8,5 +8,5 @@ export async function POST() {
cookieStore.delete("oidc_state"); cookieStore.delete("oidc_state");
cookieStore.delete("oidc_nonce"); cookieStore.delete("oidc_nonce");
cookieStore.delete("oidc_code_verifier"); cookieStore.delete("oidc_code_verifier");
return NextResponse.json({ success: true }); return NextResponse.json({ success: true }, { headers: { "Cache-Control": "no-store" } });
} }
+4 -9
View File
@@ -2,7 +2,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Card, Button, Input } from "@/shared/components"; import { Card, Button, Input } from "@/shared/components";
import { useRouter } from "next/navigation";
export default function LoginPage() { export default function LoginPage() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -16,7 +15,6 @@ export default function LoginPage() {
const [oidcLoginLabel, setOidcLoginLabel] = useState("Sign in with OIDC"); const [oidcLoginLabel, setOidcLoginLabel] = useState("Sign in with OIDC");
const [mustChange, setMustChange] = useState(false); const [mustChange, setMustChange] = useState(false);
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const router = useRouter();
// Countdown for rate-limit // Countdown for rate-limit
useEffect(() => { useEffect(() => {
@@ -40,8 +38,7 @@ export default function LoginPage() {
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
if (data.requireLogin === false) { if (data.requireLogin === false) {
router.push("/dashboard"); window.location.assign("/dashboard");
router.refresh();
return; return;
} }
setHasPassword(!!data.hasPassword); setHasPassword(!!data.hasPassword);
@@ -58,7 +55,7 @@ export default function LoginPage() {
} }
} }
checkAuth(); checkAuth();
}, [router]); }, []);
const handleLogin = async (e) => { const handleLogin = async (e) => {
e.preventDefault(); e.preventDefault();
@@ -79,8 +76,7 @@ export default function LoginPage() {
setMustChange(true); setMustChange(true);
return; return;
} }
router.push("/dashboard"); window.location.assign("/dashboard");
router.refresh();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || "Invalid password"); setError(data.error || "Invalid password");
@@ -106,8 +102,7 @@ export default function LoginPage() {
body: JSON.stringify({ currentPassword: password, newPassword }), body: JSON.stringify({ currentPassword: password, newPassword }),
}); });
if (res.ok) { if (res.ok) {
router.push("/dashboard"); window.location.assign("/dashboard");
router.refresh();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || "Failed to set password"); setError(data.error || "Failed to set password");
+2 -4
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import ProviderIcon from "@/shared/components/ProviderIcon"; import ProviderIcon from "@/shared/components/ProviderIcon";
@@ -180,7 +180,6 @@ const getPageInfo = (pathname) => {
export default function Header({ onMenuClick, showMenuButton = true }) { export default function Header({ onMenuClick, showMenuButton = true }) {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter();
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [loginMethod, setLoginMethod] = useState(""); const [loginMethod, setLoginMethod] = useState("");
const [donateOpen, setDonateOpen] = useState(false); const [donateOpen, setDonateOpen] = useState(false);
@@ -219,8 +218,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
try { try {
const res = await fetch("/api/auth/logout", { method: "POST" }); const res = await fetch("/api/auth/logout", { method: "POST" });
if (res.ok) { if (res.ok) {
router.push("/login"); window.location.assign("/login");
router.refresh();
} }
} catch (err) { } catch (err) {
console.error("Failed to logout:", err); console.error("Failed to logout:", err);