From 9da0a0d0201c883cb5e621b8f75ffee01237e04c Mon Sep 17 00:00:00 2001 From: Anthony Stirling Date: Sat, 6 Jun 2026 19:20:07 +0100 Subject: [PATCH] fix: respect BASE_PATH in redirects, comparisons, and cookie consent paths --- .../core/components/filesPage/FileManagerView.tsx | 6 ++++-- .../src/core/components/shared/AppConfigModal.tsx | 3 ++- .../viewer/useViewerWorkbenchBarButtons.tsx | 11 ++--------- frontend/editor/src/core/constants/app.ts | 13 +++++++++++++ frontend/editor/src/core/hooks/useCookieConsent.ts | 6 +++--- .../editor/src/core/services/httpErrorHandler.ts | 3 ++- .../editor/src/core/utils/settingsNavigation.ts | 10 +++++----- frontend/editor/src/proprietary/auth/UseSession.tsx | 3 ++- .../src/proprietary/services/apiClientSetup.ts | 6 ++++-- frontend/editor/src/saas/services/apiClient.ts | 8 +++++--- 10 files changed, 42 insertions(+), 27 deletions(-) diff --git a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx index 8b54960e3..b13315cb4 100644 --- a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx +++ b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx @@ -34,6 +34,7 @@ import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { stripBasePath } from "@app/constants/app"; import { useSharingEnabled } from "@app/hooks/useSharingEnabled"; import { useFolders } from "@app/contexts/FolderContext"; import { useFileActions } from "@app/contexts/file/fileHooks"; @@ -190,10 +191,11 @@ export default function FileManagerView() { // Push folder selection into the URL while still on /files. useEffect(() => { - if (!window.location.pathname.startsWith("/files")) return; + const stripped = stripBasePath(window.location.pathname); + if (!stripped.startsWith("/files")) return; const target = currentFolderId === null ? "/files" : `/files/${currentFolderId}`; - if (window.location.pathname !== target) { + if (stripped !== target) { navigate(target, { replace: true }); } }, [currentFolderId, navigate]); diff --git a/frontend/editor/src/core/components/shared/AppConfigModal.tsx b/frontend/editor/src/core/components/shared/AppConfigModal.tsx index 3b1bd16d2..d27c74a90 100644 --- a/frontend/editor/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/editor/src/core/components/shared/AppConfigModal.tsx @@ -23,6 +23,7 @@ import { useUnsavedChanges, } from "@app/contexts/UnsavedChangesContext"; import { SettingsSearchBar } from "@app/components/shared/config/SettingsSearchBar"; +import { stripBasePath } from "@app/constants/app"; interface AppConfigModalProps { opened: boolean; @@ -82,7 +83,7 @@ const AppConfigModalInner: React.FC = ({ const detail = (ev as CustomEvent).detail as { key?: NavKey } | undefined; if (detail?.key) { const alreadyInSettings = - window.location.pathname.startsWith("/settings"); + stripBasePath(window.location.pathname).startsWith("/settings"); navigate(`/settings/${detail.key}`, { replace: alreadyInSettings, }); diff --git a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx index 25eacf3d9..4960672dd 100644 --- a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx +++ b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx @@ -18,7 +18,7 @@ import { useNavigationState, useNavigationGuard, } from "@app/contexts/NavigationContext"; -import { BASE_PATH, withBasePath } from "@app/constants/app"; +import { stripBasePath, withBasePath } from "@app/constants/app"; import { useRedaction, useRedactionMode } from "@app/contexts/RedactionContext"; import TextFieldsIcon from "@mui/icons-material/TextFields"; import StraightenIcon from "@mui/icons-material/Straighten"; @@ -73,17 +73,10 @@ export function useViewerWorkbenchBarButtons( }); }, [registerImmediatePanUpdate]); - const stripBasePath = useCallback((path: string) => { - if (BASE_PATH && path.startsWith(BASE_PATH)) { - return path.slice(BASE_PATH.length) || "/"; - } - return path; - }, []); - const isAnnotationsPath = useCallback(() => { const cleanPath = stripBasePath(window.location.pathname).toLowerCase(); return cleanPath === "/annotations" || cleanPath.endsWith("/annotations"); - }, [stripBasePath]); + }, []); const [isAnnotationsActive, setIsAnnotationsActive] = useState(() => isAnnotationsPath(), diff --git a/frontend/editor/src/core/constants/app.ts b/frontend/editor/src/core/constants/app.ts index e8daed89c..97346c9eb 100644 --- a/frontend/editor/src/core/constants/app.ts +++ b/frontend/editor/src/core/constants/app.ts @@ -38,3 +38,16 @@ export const absoluteWithBasePath = (path: string): string => { const clean = path.startsWith("/") ? path : `/${path}`; return `${window.location.origin}${BASE_PATH}${clean}`; }; + +/** + * Remove the BASE_PATH prefix from a pathname so comparisons against + * bare app routes ("/login", "/files", …) work under any subpath deploy. + */ +export const stripBasePath = (pathname: string): string => { + if (!BASE_PATH) return pathname; + if (pathname === BASE_PATH) return "/"; + if (pathname.startsWith(`${BASE_PATH}/`)) { + return pathname.slice(BASE_PATH.length); + } + return pathname; +}; diff --git a/frontend/editor/src/core/hooks/useCookieConsent.ts b/frontend/editor/src/core/hooks/useCookieConsent.ts index b92b180b4..0ab55f02e 100644 --- a/frontend/editor/src/core/hooks/useCookieConsent.ts +++ b/frontend/editor/src/core/hooks/useCookieConsent.ts @@ -36,14 +36,14 @@ export const useCookieConsent = ({ const mainCSS = document.createElement("link"); mainCSS.rel = "stylesheet"; - mainCSS.href = `${BASE_PATH}css/cookieconsent.css`; + mainCSS.href = `${BASE_PATH}/css/cookieconsent.css`; if (!document.querySelector(`link[href="${mainCSS.href}"]`)) { document.head.appendChild(mainCSS); } const customCSS = document.createElement("link"); customCSS.rel = "stylesheet"; - customCSS.href = `${BASE_PATH}css/cookieconsentCustomisation.css`; + customCSS.href = `${BASE_PATH}/css/cookieconsentCustomisation.css`; if (!document.querySelector(`link[href="${customCSS.href}"]`)) { document.head.appendChild(customCSS); } @@ -57,7 +57,7 @@ export const useCookieConsent = ({ } const script = document.createElement("script"); - script.src = `${BASE_PATH}js/thirdParty/cookieconsent.umd.js`; + script.src = `${BASE_PATH}/js/thirdParty/cookieconsent.umd.js`; script.onload = () => { setTimeout(() => { const detectTheme = () => { diff --git a/frontend/editor/src/core/services/httpErrorHandler.ts b/frontend/editor/src/core/services/httpErrorHandler.ts index 4783f99e2..968a44a99 100644 --- a/frontend/editor/src/core/services/httpErrorHandler.ts +++ b/frontend/editor/src/core/services/httpErrorHandler.ts @@ -11,6 +11,7 @@ import { clampText, extractAxiosErrorMessage, } from "@app/services/httpErrorUtils"; +import { withBasePath } from "@app/constants/app"; // Module-scoped state to reduce global variable usage const recentSpecialByEndpoint: Record = {}; @@ -82,7 +83,7 @@ export async function handleHttpError(error: any): Promise { // ignore storage access failures } const expiredPrefix = hadStoredJwt ? "expired=true&" : ""; - window.location.href = `/login?${expiredPrefix}from=${encodeURIComponent(currentLocation)}`; + window.location.href = `${withBasePath("/login")}?${expiredPrefix}from=${encodeURIComponent(currentLocation)}`; return true; // Suppress toast since we're redirecting } diff --git a/frontend/editor/src/core/utils/settingsNavigation.ts b/frontend/editor/src/core/utils/settingsNavigation.ts index b5b433d4d..443389e37 100644 --- a/frontend/editor/src/core/utils/settingsNavigation.ts +++ b/frontend/editor/src/core/utils/settingsNavigation.ts @@ -1,4 +1,5 @@ import { NavKey } from "@app/components/shared/config/types"; +import { stripBasePath, withBasePath } from "@app/constants/app"; /** * Navigate to a specific settings section @@ -13,8 +14,7 @@ import { NavKey } from "@app/components/shared/config/types"; * navigateToSettings('adminPremium'); */ export function navigateToSettings(section: NavKey) { - const basePath = window.location.pathname.split("/settings")[0] || ""; - const newPath = `${basePath}/settings/${section}`; + const newPath = withBasePath(`/settings/${section}`); window.history.pushState({}, "", newPath); // Trigger a popstate event to notify components @@ -30,10 +30,10 @@ export function navigateToSettings(section: NavKey) { * * @example * Go to People Settings - * // Returns: "/settings/people" + * // Returns: "/settings/people" (or "/bpp/settings/people" under subpath) */ export function getSettingsUrl(section: NavKey): string { - return `/settings/${section}`; + return withBasePath(`/settings/${section}`); } /** @@ -43,7 +43,7 @@ export function getSettingsUrl(section: NavKey): string { * @returns True if in settings (and matching specific section if provided) */ export function isInSettings(section?: NavKey): boolean { - const pathname = window.location.pathname; + const pathname = stripBasePath(window.location.pathname); if (!section) { return pathname.startsWith("/settings"); diff --git a/frontend/editor/src/proprietary/auth/UseSession.tsx b/frontend/editor/src/proprietary/auth/UseSession.tsx index efc846787..82ea8b63c 100644 --- a/frontend/editor/src/proprietary/auth/UseSession.tsx +++ b/frontend/editor/src/proprietary/auth/UseSession.tsx @@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { springAuth } from "@app/auth/springAuthClient"; import { clearPlatformAuthOnLoginInit } from "@app/extensions/authSessionCleanup"; +import { stripBasePath } from "@app/constants/app"; import type { Session, User, @@ -167,7 +168,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Clear any platform-specific cached auth on login page init. if ( typeof window !== "undefined" && - window.location.pathname.startsWith("/login") + stripBasePath(window.location.pathname).startsWith("/login") ) { await clearPlatformAuthOnLoginInit(); } diff --git a/frontend/editor/src/proprietary/services/apiClientSetup.ts b/frontend/editor/src/proprietary/services/apiClientSetup.ts index 66a8132e8..d11f02ba9 100644 --- a/frontend/editor/src/proprietary/services/apiClientSetup.ts +++ b/frontend/editor/src/proprietary/services/apiClientSetup.ts @@ -1,4 +1,5 @@ import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios"; +import { withBasePath } from "@app/constants/app"; let isRefreshing = false; let failedQueue: Array<{ @@ -89,9 +90,10 @@ async function refreshAuthToken(client: AxiosInstance): Promise { clearJwtTokenFromStorage(); // Redirect to login - if (window.location.pathname !== "/login") { + const loginPath = withBasePath("/login"); + if (window.location.pathname !== loginPath) { console.log("[API Client] Redirecting to login page..."); - window.location.href = "/login"; + window.location.href = loginPath; } throw error; diff --git a/frontend/editor/src/saas/services/apiClient.ts b/frontend/editor/src/saas/services/apiClient.ts index 4fc8407d0..75f140828 100644 --- a/frontend/editor/src/saas/services/apiClient.ts +++ b/frontend/editor/src/saas/services/apiClient.ts @@ -3,6 +3,7 @@ import { supabase } from "@app/auth/supabase"; import { handleHttpError } from "@app/services/httpErrorHandler"; import { alert } from "@app/components/toast"; import { openPlanSettings } from "@app/utils/appSettings"; +import { withBasePath } from "@app/constants/app"; // Global credit update callback - will be set by the AuthProvider let globalCreditUpdateCallback: ((credits: number) => void) | null = null; @@ -189,7 +190,7 @@ apiClient.interceptors.response.use( if (!isPublicEndpoint) { // Redirect to login only for protected endpoints - window.location.href = "/login"; + window.location.href = withBasePath("/login"); } return Promise.reject(error); @@ -209,8 +210,9 @@ apiClient.interceptors.response.use( console.debug( "[API Client] No session to refresh, 401 on protected endpoint", ); - if (window.location.pathname !== "/login") { - window.location.href = "/login"; + const loginPath = withBasePath("/login"); + if (window.location.pathname !== loginPath) { + window.location.href = loginPath; } } } catch (refreshError) {