fix: respect BASE_PATH in redirects, comparisons, and cookie consent paths

This commit is contained in:
Anthony Stirling
2026-06-06 19:20:07 +01:00
parent 0dff192281
commit 9da0a0d020
10 changed files with 42 additions and 27 deletions
@@ -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]);
@@ -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<AppConfigModalProps> = ({
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,
});
@@ -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<boolean>(() =>
isAnnotationsPath(),
+13
View File
@@ -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;
};
@@ -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 = () => {
@@ -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<string, number> = {};
@@ -82,7 +83,7 @@ export async function handleHttpError(error: any): Promise<boolean> {
// 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
}
@@ -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
* <a href={getSettingsUrl('people')}>Go to People Settings</a>
* // 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");
@@ -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();
}
@@ -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<string> {
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;
@@ -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) {