diff --git a/web/src/api/auth-redirect.ts b/web/src/api/auth-redirect.ts new file mode 100644 index 000000000..f19e2b8a5 --- /dev/null +++ b/web/src/api/auth-redirect.ts @@ -0,0 +1,12 @@ +// Module-level flag to prevent multiple simultaneous redirects +// (eg, when multiple SWR queries fail with 401 at once, or when +// both ApiProvider and ProtectedRoute try to redirect) +let _isRedirectingToLogin = false; + +export function isRedirectingToLogin(): boolean { + return _isRedirectingToLogin; +} + +export function setRedirectingToLogin(value: boolean): void { + _isRedirectingToLogin = value; +} diff --git a/web/src/api/index.tsx b/web/src/api/index.tsx index 540a328eb..e5c5617ab 100644 --- a/web/src/api/index.tsx +++ b/web/src/api/index.tsx @@ -3,13 +3,10 @@ import { SWRConfig } from "swr"; import { WsProvider } from "./ws"; import axios from "axios"; import { ReactNode } from "react"; +import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect"; axios.defaults.baseURL = `${baseUrl}api/`; -// Module-level flag to prevent multiple simultaneous redirects -// (eg, when multiple SWR queries fail with 401 at once) -let isRedirectingToLogin = false; - type ApiProviderType = { children?: ReactNode; options?: Record; @@ -35,8 +32,8 @@ export function ApiProvider({ children, options }: ApiProviderType) { ) { // redirect to the login page if not already there const loginPage = error.response.headers.get("location") ?? "login"; - if (window.location.href !== loginPage && !isRedirectingToLogin) { - isRedirectingToLogin = true; + if (window.location.href !== loginPage && !isRedirectingToLogin()) { + setRedirectingToLogin(true); window.location.href = loginPage; } } diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx index 18dc50d53..55edc60bd 100644 --- a/web/src/components/auth/ProtectedRoute.tsx +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -1,7 +1,11 @@ -import { useContext } from "react"; +import { useContext, useEffect } from "react"; import { Navigate, Outlet } from "react-router-dom"; import { AuthContext } from "@/context/auth-context"; import ActivityIndicator from "../indicators/activity-indicator"; +import { + isRedirectingToLogin, + setRedirectingToLogin, +} from "@/api/auth-redirect"; export default function ProtectedRoute({ requiredRoles, @@ -10,6 +14,20 @@ export default function ProtectedRoute({ }) { const { auth } = useContext(AuthContext); + // Redirect to login page when not authenticated + // don't use because we need a full page load to reset state + useEffect(() => { + if ( + !auth.isLoading && + auth.isAuthenticated && + !auth.user && + !isRedirectingToLogin() + ) { + setRedirectingToLogin(true); + window.location.href = "/login"; + } + }, [auth.isLoading, auth.isAuthenticated, auth.user]); + if (auth.isLoading) { return ( @@ -23,7 +41,9 @@ export default function ProtectedRoute({ // Authenticated mode (8971): require login if (!auth.user) { - return ; + return ( + + ); } // If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback