Skip to content

Commit 4ad4176

Browse files
fix(webapp): stop the public landing page looping to /login under the /api base path (#1334)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1293c33 commit 4ad4176

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

webapp/src/integrations/auth/sessionExpiry.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { refreshAccessToken } from "./sessionRefresh";
77
vi.mock("./sessionRefresh", () => ({ refreshAccessToken: vi.fn() }));
88
const refreshMock = vi.mocked(refreshAccessToken);
99

10+
// Prod serves the API under /api (Traefik strips it); pin a base path so the exemptions are exercised
11+
// the way they run in prod — the GET /api/user probe must be exempt exactly like /user is locally.
12+
vi.mock("@/environment", () => ({ default: { serverUrl: "http://localhost/api" } }));
13+
1014
// jsdom's window.location is not directly assignable; replace it with a stub exposing `assign` plus
1115
// the pathname/search/origin the handler reads.
1216
function stubLocation(pathname: string, search = ""): { assigned: string[] } {
@@ -130,6 +134,30 @@ describe("handlePossibleSessionExpiry", () => {
130134
expect(assigned).toHaveLength(0);
131135
});
132136

137+
it("does NOT handle a 401 from the GET /api/user probe (prod /api base path)", () => {
138+
// A logged-out visitor on the public landing (pathname "/") probes the session; the /api-prefixed
139+
// probe must be exempt, not drive the login redirect.
140+
const { assigned } = stubLocation("/");
141+
const handled = handlePossibleSessionExpiry(
142+
res(401, "http://localhost/api/user"),
143+
makeQueryClient(),
144+
);
145+
expect(handled).toBe(false);
146+
expect(refreshMock).not.toHaveBeenCalled();
147+
expect(assigned).toHaveLength(0);
148+
});
149+
150+
it("does NOT handle a 401 from /api/auth/* endpoints (prod /api base path)", () => {
151+
const { assigned } = stubLocation("/");
152+
const handled = handlePossibleSessionExpiry(
153+
res(401, "http://localhost/api/auth/refresh"),
154+
makeQueryClient(),
155+
);
156+
expect(handled).toBe(false);
157+
expect(refreshMock).not.toHaveBeenCalled();
158+
expect(assigned).toHaveLength(0);
159+
});
160+
133161
it("does NOT handle a 401 from /auth/* endpoints (refresh must not recurse)", () => {
134162
const { assigned } = stubLocation("/");
135163
const handled = handlePossibleSessionExpiry(

webapp/src/integrations/auth/sessionExpiry.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import type { QueryClient } from "@tanstack/react-query";
22
import { getCurrentUserQueryKey } from "@/api/@tanstack/react-query.gen";
3+
import environment from "@/environment";
34
import { safeReturnTo } from "@/integrations/auth/guard";
45
import { refreshAccessToken } from "@/integrations/auth/sessionRefresh";
56

7+
/**
8+
* The API client prefixes every request with `environment.serverUrl` (`/api` in prod where Traefik
9+
* strips it, empty in local dev). Returns that base path so the endpoint exemptions below match across
10+
* deploys, not just the local unprefixed shape.
11+
*/
12+
function apiBasePath(): string {
13+
try {
14+
return new URL(environment.serverUrl, window.location.origin).pathname.replace(/\/$/, "");
15+
} catch {
16+
return "";
17+
}
18+
}
19+
620
/**
721
* Paths whose own 401 must NOT trigger a redirect-to-login. The `GET /user` identity probe
822
* legitimately 401s while logged out (and is the query the login page itself reads), so reacting
@@ -18,6 +32,11 @@ function isExemptFromSessionExpiry(pathname: string, url: string): boolean {
1832
} catch {
1933
// Leave requestPath as-is for an unparseable URL.
2034
}
35+
// Strip the API base path so `/api/user` (prod) matches the same exemptions as `/user` (local dev).
36+
const base = apiBasePath();
37+
if (base && requestPath.startsWith(`${base}/`)) {
38+
requestPath = requestPath.slice(base.length);
39+
}
2140
if (requestPath === "/user") return true;
2241
if (requestPath.startsWith("/auth/")) return true;
2342
// Don't redirect when we're already on (or heading to) the login page.

0 commit comments

Comments
 (0)