From 7f7c86588813e3da758a3d0ae0e16f3ee44e3715 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+frooodle@users.noreply.github.com>
Date: Wed, 10 Jun 2026 11:56:20 +0100
Subject: [PATCH] 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 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
---
.../src/core/contexts/FolderContext.test.tsx | 17 +++++++++++------
.../editor/src/core/contexts/FolderContext.tsx | 13 +++++++++++--
frontend/editor/vite.config.ts | 7 ++++++-
3 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/frontend/editor/src/core/contexts/FolderContext.test.tsx b/frontend/editor/src/core/contexts/FolderContext.test.tsx
index 46de8f6d7..1734bf350 100644
--- a/frontend/editor/src/core/contexts/FolderContext.test.tsx
+++ b/frontend/editor/src/core/contexts/FolderContext.test.tsx
@@ -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 {
render(
-
-
- ,
+
+
+
+
+ ,
);
// 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(
-
- (apiRef.current = api)} />
- ,
+
+
+ (apiRef.current = api)} />
+
+ ,
);
await waitFor(() =>
expect(screen.getByTestId("count").textContent).toBe(
diff --git a/frontend/editor/src/core/contexts/FolderContext.tsx b/frontend/editor/src/core/contexts/FolderContext.tsx
index e218072ec..65b746bd6 100644
--- a/frontend/editor/src/core/contexts/FolderContext.tsx
+++ b/frontend/editor/src/core/contexts/FolderContext.tsx
@@ -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();
diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts
index 3f51f7ba3..0df5fbee6 100644
--- a/frontend/editor/vite.config.ts
+++ b/frontend/editor/vite.config.ts
@@ -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 `` 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"
? "/"
: "./",