mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
fix: respect BASE_PATH in redirects, comparisons, and cookie consent paths
This commit is contained in:
@@ -34,6 +34,7 @@ import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
|||||||
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
|
||||||
|
import { stripBasePath } from "@app/constants/app";
|
||||||
import { useSharingEnabled } from "@app/hooks/useSharingEnabled";
|
import { useSharingEnabled } from "@app/hooks/useSharingEnabled";
|
||||||
import { useFolders } from "@app/contexts/FolderContext";
|
import { useFolders } from "@app/contexts/FolderContext";
|
||||||
import { useFileActions } from "@app/contexts/file/fileHooks";
|
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.
|
// Push folder selection into the URL while still on /files.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.location.pathname.startsWith("/files")) return;
|
const stripped = stripBasePath(window.location.pathname);
|
||||||
|
if (!stripped.startsWith("/files")) return;
|
||||||
const target =
|
const target =
|
||||||
currentFolderId === null ? "/files" : `/files/${currentFolderId}`;
|
currentFolderId === null ? "/files" : `/files/${currentFolderId}`;
|
||||||
if (window.location.pathname !== target) {
|
if (stripped !== target) {
|
||||||
navigate(target, { replace: true });
|
navigate(target, { replace: true });
|
||||||
}
|
}
|
||||||
}, [currentFolderId, navigate]);
|
}, [currentFolderId, navigate]);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
useUnsavedChanges,
|
useUnsavedChanges,
|
||||||
} from "@app/contexts/UnsavedChangesContext";
|
} from "@app/contexts/UnsavedChangesContext";
|
||||||
import { SettingsSearchBar } from "@app/components/shared/config/SettingsSearchBar";
|
import { SettingsSearchBar } from "@app/components/shared/config/SettingsSearchBar";
|
||||||
|
import { stripBasePath } from "@app/constants/app";
|
||||||
|
|
||||||
interface AppConfigModalProps {
|
interface AppConfigModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -82,7 +83,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
|
|||||||
const detail = (ev as CustomEvent).detail as { key?: NavKey } | undefined;
|
const detail = (ev as CustomEvent).detail as { key?: NavKey } | undefined;
|
||||||
if (detail?.key) {
|
if (detail?.key) {
|
||||||
const alreadyInSettings =
|
const alreadyInSettings =
|
||||||
window.location.pathname.startsWith("/settings");
|
stripBasePath(window.location.pathname).startsWith("/settings");
|
||||||
navigate(`/settings/${detail.key}`, {
|
navigate(`/settings/${detail.key}`, {
|
||||||
replace: alreadyInSettings,
|
replace: alreadyInSettings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
useNavigationState,
|
useNavigationState,
|
||||||
useNavigationGuard,
|
useNavigationGuard,
|
||||||
} from "@app/contexts/NavigationContext";
|
} 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 { useRedaction, useRedactionMode } from "@app/contexts/RedactionContext";
|
||||||
import TextFieldsIcon from "@mui/icons-material/TextFields";
|
import TextFieldsIcon from "@mui/icons-material/TextFields";
|
||||||
import StraightenIcon from "@mui/icons-material/Straighten";
|
import StraightenIcon from "@mui/icons-material/Straighten";
|
||||||
@@ -73,17 +73,10 @@ export function useViewerWorkbenchBarButtons(
|
|||||||
});
|
});
|
||||||
}, [registerImmediatePanUpdate]);
|
}, [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 isAnnotationsPath = useCallback(() => {
|
||||||
const cleanPath = stripBasePath(window.location.pathname).toLowerCase();
|
const cleanPath = stripBasePath(window.location.pathname).toLowerCase();
|
||||||
return cleanPath === "/annotations" || cleanPath.endsWith("/annotations");
|
return cleanPath === "/annotations" || cleanPath.endsWith("/annotations");
|
||||||
}, [stripBasePath]);
|
}, []);
|
||||||
|
|
||||||
const [isAnnotationsActive, setIsAnnotationsActive] = useState<boolean>(() =>
|
const [isAnnotationsActive, setIsAnnotationsActive] = useState<boolean>(() =>
|
||||||
isAnnotationsPath(),
|
isAnnotationsPath(),
|
||||||
|
|||||||
@@ -38,3 +38,16 @@ export const absoluteWithBasePath = (path: string): string => {
|
|||||||
const clean = path.startsWith("/") ? path : `/${path}`;
|
const clean = path.startsWith("/") ? path : `/${path}`;
|
||||||
return `${window.location.origin}${BASE_PATH}${clean}`;
|
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");
|
const mainCSS = document.createElement("link");
|
||||||
mainCSS.rel = "stylesheet";
|
mainCSS.rel = "stylesheet";
|
||||||
mainCSS.href = `${BASE_PATH}css/cookieconsent.css`;
|
mainCSS.href = `${BASE_PATH}/css/cookieconsent.css`;
|
||||||
if (!document.querySelector(`link[href="${mainCSS.href}"]`)) {
|
if (!document.querySelector(`link[href="${mainCSS.href}"]`)) {
|
||||||
document.head.appendChild(mainCSS);
|
document.head.appendChild(mainCSS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customCSS = document.createElement("link");
|
const customCSS = document.createElement("link");
|
||||||
customCSS.rel = "stylesheet";
|
customCSS.rel = "stylesheet";
|
||||||
customCSS.href = `${BASE_PATH}css/cookieconsentCustomisation.css`;
|
customCSS.href = `${BASE_PATH}/css/cookieconsentCustomisation.css`;
|
||||||
if (!document.querySelector(`link[href="${customCSS.href}"]`)) {
|
if (!document.querySelector(`link[href="${customCSS.href}"]`)) {
|
||||||
document.head.appendChild(customCSS);
|
document.head.appendChild(customCSS);
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ export const useCookieConsent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement("script");
|
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 = () => {
|
script.onload = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const detectTheme = () => {
|
const detectTheme = () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
clampText,
|
clampText,
|
||||||
extractAxiosErrorMessage,
|
extractAxiosErrorMessage,
|
||||||
} from "@app/services/httpErrorUtils";
|
} from "@app/services/httpErrorUtils";
|
||||||
|
import { withBasePath } from "@app/constants/app";
|
||||||
|
|
||||||
// Module-scoped state to reduce global variable usage
|
// Module-scoped state to reduce global variable usage
|
||||||
const recentSpecialByEndpoint: Record<string, number> = {};
|
const recentSpecialByEndpoint: Record<string, number> = {};
|
||||||
@@ -82,7 +83,7 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
|||||||
// ignore storage access failures
|
// ignore storage access failures
|
||||||
}
|
}
|
||||||
const expiredPrefix = hadStoredJwt ? "expired=true&" : "";
|
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
|
return true; // Suppress toast since we're redirecting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NavKey } from "@app/components/shared/config/types";
|
import { NavKey } from "@app/components/shared/config/types";
|
||||||
|
import { stripBasePath, withBasePath } from "@app/constants/app";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a specific settings section
|
* Navigate to a specific settings section
|
||||||
@@ -13,8 +14,7 @@ import { NavKey } from "@app/components/shared/config/types";
|
|||||||
* navigateToSettings('adminPremium');
|
* navigateToSettings('adminPremium');
|
||||||
*/
|
*/
|
||||||
export function navigateToSettings(section: NavKey) {
|
export function navigateToSettings(section: NavKey) {
|
||||||
const basePath = window.location.pathname.split("/settings")[0] || "";
|
const newPath = withBasePath(`/settings/${section}`);
|
||||||
const newPath = `${basePath}/settings/${section}`;
|
|
||||||
window.history.pushState({}, "", newPath);
|
window.history.pushState({}, "", newPath);
|
||||||
|
|
||||||
// Trigger a popstate event to notify components
|
// Trigger a popstate event to notify components
|
||||||
@@ -30,10 +30,10 @@ export function navigateToSettings(section: NavKey) {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <a href={getSettingsUrl('people')}>Go to People Settings</a>
|
* <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 {
|
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)
|
* @returns True if in settings (and matching specific section if provided)
|
||||||
*/
|
*/
|
||||||
export function isInSettings(section?: NavKey): boolean {
|
export function isInSettings(section?: NavKey): boolean {
|
||||||
const pathname = window.location.pathname;
|
const pathname = stripBasePath(window.location.pathname);
|
||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
return pathname.startsWith("/settings");
|
return pathname.startsWith("/settings");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { springAuth } from "@app/auth/springAuthClient";
|
import { springAuth } from "@app/auth/springAuthClient";
|
||||||
import { clearPlatformAuthOnLoginInit } from "@app/extensions/authSessionCleanup";
|
import { clearPlatformAuthOnLoginInit } from "@app/extensions/authSessionCleanup";
|
||||||
|
import { stripBasePath } from "@app/constants/app";
|
||||||
import type {
|
import type {
|
||||||
Session,
|
Session,
|
||||||
User,
|
User,
|
||||||
@@ -167,7 +168,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Clear any platform-specific cached auth on login page init.
|
// Clear any platform-specific cached auth on login page init.
|
||||||
if (
|
if (
|
||||||
typeof window !== "undefined" &&
|
typeof window !== "undefined" &&
|
||||||
window.location.pathname.startsWith("/login")
|
stripBasePath(window.location.pathname).startsWith("/login")
|
||||||
) {
|
) {
|
||||||
await clearPlatformAuthOnLoginInit();
|
await clearPlatformAuthOnLoginInit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios";
|
import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||||
|
import { withBasePath } from "@app/constants/app";
|
||||||
|
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
let failedQueue: Array<{
|
let failedQueue: Array<{
|
||||||
@@ -89,9 +90,10 @@ async function refreshAuthToken(client: AxiosInstance): Promise<string> {
|
|||||||
clearJwtTokenFromStorage();
|
clearJwtTokenFromStorage();
|
||||||
|
|
||||||
// Redirect to login
|
// 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...");
|
console.log("[API Client] Redirecting to login page...");
|
||||||
window.location.href = "/login";
|
window.location.href = loginPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { supabase } from "@app/auth/supabase";
|
|||||||
import { handleHttpError } from "@app/services/httpErrorHandler";
|
import { handleHttpError } from "@app/services/httpErrorHandler";
|
||||||
import { alert } from "@app/components/toast";
|
import { alert } from "@app/components/toast";
|
||||||
import { openPlanSettings } from "@app/utils/appSettings";
|
import { openPlanSettings } from "@app/utils/appSettings";
|
||||||
|
import { withBasePath } from "@app/constants/app";
|
||||||
|
|
||||||
// Global credit update callback - will be set by the AuthProvider
|
// Global credit update callback - will be set by the AuthProvider
|
||||||
let globalCreditUpdateCallback: ((credits: number) => void) | null = null;
|
let globalCreditUpdateCallback: ((credits: number) => void) | null = null;
|
||||||
@@ -189,7 +190,7 @@ apiClient.interceptors.response.use(
|
|||||||
|
|
||||||
if (!isPublicEndpoint) {
|
if (!isPublicEndpoint) {
|
||||||
// Redirect to login only for protected endpoints
|
// Redirect to login only for protected endpoints
|
||||||
window.location.href = "/login";
|
window.location.href = withBasePath("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@@ -209,8 +210,9 @@ apiClient.interceptors.response.use(
|
|||||||
console.debug(
|
console.debug(
|
||||||
"[API Client] No session to refresh, 401 on protected endpoint",
|
"[API Client] No session to refresh, 401 on protected endpoint",
|
||||||
);
|
);
|
||||||
if (window.location.pathname !== "/login") {
|
const loginPath = withBasePath("/login");
|
||||||
window.location.href = "/login";
|
if (window.location.pathname !== loginPath) {
|
||||||
|
window.location.href = loginPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user