fix: stop unauthenticated storage calls on /login + fix subpath manifest 404

Two issues seen on the hosted /bpp login screen:

1. GET /api/v1/storage/folders fired (and 401'd) on the login page. The
   global FolderProvider pulls from the server whenever
   appConfig.storageEnabled is true, with no auth gate, so it hits the
   authenticated storage API before the user has signed in. Skip the pull
   on auth routes (/login, /signup, /auth/*, /invite, /reset-password),
   mirroring the existing LicenseContext / AppConfigContext guards. Tests
   wrap FolderProvider in MemoryRouter (now uses useLocation).

2. manifest.json and modern-logo/favicon.ico 404'd from the domain root
   instead of /bpp/. vite base for RUN_SUBPATH deploys was "/bpp" with no
   trailing slash, so <base href="/bpp"> made the browser resolve relative
   links against the parent (root). Use "/bpp/"; getBasePath() strips the
   trailing slash, so BASE_PATH, routing and asset URLs are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Anthony Stirling
2026-06-10 11:56:48 +01:00
co-authored by Claude Opus 4.8
parent 2101b4028c
commit 7f7c865888
3 changed files with 28 additions and 9 deletions
@@ -1,6 +1,7 @@
import React from "react";
import { describe, expect, test, vi, beforeEach } from "vitest";
import { render, screen, waitFor, act } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { FolderProvider, useFolders } from "@app/contexts/FolderContext";
import { createFolderId, FolderId, FolderRecord } from "@app/types/folder";
@@ -110,9 +111,11 @@ function axiosError(status: number, message = "rejected"): Error {
async function renderAndWaitForPull(): Promise<void> {
render(
<FolderProvider>
<Probe />
</FolderProvider>,
<MemoryRouter>
<FolderProvider>
<Probe />
</FolderProvider>
</MemoryRouter>,
);
// The pull is fired from a mount effect; wait until it has resolved by
// observing that `mockList` was called at least once and a tick has
@@ -241,9 +244,11 @@ describe("FolderContext stale-folder 404 cleanup", () => {
mockList.mockResolvedValueOnce(initial);
const apiRef: { current: ProbeApi | null } = { current: null };
render(
<FolderProvider>
<ApiProbe onReady={(api) => (apiRef.current = api)} />
</FolderProvider>,
<MemoryRouter>
<FolderProvider>
<ApiProbe onReady={(api) => (apiRef.current = api)} />
</FolderProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(screen.getByTestId("count").textContent).toBe(
@@ -38,6 +38,8 @@ import {
} from "@app/types/folder";
import { useIndexedDB } from "@app/contexts/IndexedDBContext";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useLocation } from "react-router-dom";
import { isAuthRoute } from "@app/constants/routes";
interface FolderContextValue {
folders: FolderRecord[];
@@ -363,10 +365,17 @@ export function FolderProvider({ children }: FolderProviderProps) {
// guaranteed-to-403 round-trip per session.
const { config: appConfig } = useAppConfig();
const storageBackedByServer = appConfig?.storageEnabled === true;
// Don't hit the authenticated storage API on auth routes (/login, /signup,
// /auth/*, ...). The global FolderProvider is mounted everywhere, including
// the login screen where the user has no session yet, so an unguarded pull
// fires a guaranteed-401 GET /api/v1/storage/folders before sign-in. Mirror
// the auth-route skip used by LicenseContext / AppConfigContext.
const location = useLocation();
const onAuthRoute = isAuthRoute(location.pathname);
useEffect(() => {
if (!storageBackedByServer) return;
if (!storageBackedByServer || onAuthRoute) return;
void pullFromServer();
}, [pullFromServer, storageBackedByServer]);
}, [pullFromServer, storageBackedByServer, onAuthRoute]);
const foldersById = useMemo(() => {
const map = new Map<FolderId, FolderRecord>();
+6 -1
View File
@@ -243,8 +243,13 @@ export default defineConfig(async ({ mode }) => {
// SPA fallback returns index.html as text/html and React never mounts.
// VITE_BUILD_FOR_PREVIEW=1 (set by the CI playwright steps) overrides to
// an absolute base so deep-route asset paths resolve to /assets/...
// Trailing slash is required: it becomes `<base href>` in index.html, and
// the browser resolves relative links (manifest.json, modern-logo/favicon.ico)
// against the base's *directory*. Without it, `/bpp` resolves relatives to
// the domain root → `/manifest.json` 404 instead of `/bpp/manifest.json`.
// getBasePath() strips the trailing slash, so BASE_PATH/routing are unchanged.
base: env.RUN_SUBPATH
? `/${env.RUN_SUBPATH}`
? `/${env.RUN_SUBPATH}/`
: process.env.VITE_BUILD_FOR_PREVIEW === "1"
? "/"
: "./",