From 9c2442097766cc5e071c1534680d63d072f45b0f Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 17 Sep 2026 20:41:15 +0000 Subject: [PATCH] feat: redirect to AUTH login URL on 401 from manager When built with the AUTH env var set to an OSC login URL, redirect the browser there whenever the manager returns HTTP 401 so the user can re-authenticate. Wired into the central handleFetchRequest choke point so all endpoints behave uniformly; no-op when AUTH is unset. Co-Authored-By: Claude Opus 4.7 --- .env.local.sample | 4 ++ README.md | 2 + scripts/entrypoint.sh | 2 +- src/api/handle-fetch-request.ts | 5 ++ src/api/redirect-on-auth-failure.test.ts | 59 ++++++++++++++++++++++++ src/api/redirect-on-auth-failure.ts | 22 +++++++++ src/vite-env.d.ts | 13 ++++++ vite.config.ts | 3 ++ 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/api/redirect-on-auth-failure.test.ts create mode 100644 src/api/redirect-on-auth-failure.ts diff --git a/.env.local.sample b/.env.local.sample index 402d275d..0472b90b 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -4,3 +4,7 @@ VITE_BACKEND_API_VERSION=api/v1/ VITE_DEBUG_MODE=true VITE_DEV_LOGGER_LEVEL=3 + +# Optional OSC login URL. When set, the app redirects here on a 401 from the +# manager (expired token / not logged in) so the user can re-authenticate. +# AUTH=https://app.osaas.io/?redirect=/dashboard/service/eyevinn-intercom-manager diff --git a/README.md b/README.md index 9af2dc71..954387c1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Decide whether or not debug mode should be on or not `VITE_DEBUG_MODE=true` Choose desired level of logging `VITE_DEV_LOGGER_LEVEL=3` +Optionally set an OSC login URL with `AUTH=https://app.osaas.io/?redirect=/dashboard/service/eyevinn-intercom-manager`. When this build-time env var is set, the app redirects the browser to it whenever the manager returns a 401 (expired token or not logged in), letting the user (re)authenticate. When unset, behavior is unchanged. + ``` LOGGER LEVELS 0 = no logs diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 257223ef..621012b2 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -10,6 +10,6 @@ fi echo "VITE_BACKEND_URL=$API_URL" -VITE_BACKEND_URL=$API_URL npm run build && \ +VITE_BACKEND_URL=$API_URL AUTH=$AUTH npm run build && \ cp -r /app/dist/* /usr/share/nginx/html/ && \ nginx -g 'daemon off;' diff --git a/src/api/handle-fetch-request.ts b/src/api/handle-fetch-request.ts index 8e7da8b0..45ab9726 100644 --- a/src/api/handle-fetch-request.ts +++ b/src/api/handle-fetch-request.ts @@ -1,3 +1,5 @@ +import { maybeRedirectToAuth } from "./redirect-on-auth-failure.ts"; + const isSuccessful = (r: Response) => r.status >= 200 && r.status <= 399; export const handleFetchRequest = async ( @@ -19,6 +21,9 @@ export const handleFetchRequest = async ( if (!isSuccess) { const { status } = response; + // When built with the `AUTH` env var, redirect to the OSC login URL on a + // 401 before throwing. No-op (and existing reauth flow runs) when unset. + maybeRedirectToAuth(status); let err: Error; if (text) { err = new Error(text); diff --git a/src/api/redirect-on-auth-failure.test.ts b/src/api/redirect-on-auth-failure.test.ts new file mode 100644 index 00000000..3bff15a0 --- /dev/null +++ b/src/api/redirect-on-auth-failure.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { maybeRedirectToAuth } from "./redirect-on-auth-failure.ts"; + +const AUTH_URL = + "https://app.osaas.io/?redirect=/dashboard/service/eyevinn-intercom-manager"; + +describe("maybeRedirectToAuth", () => { + let assignSpy: ReturnType; + + beforeEach(() => { + // happy-dom would perform a real navigation on assign; stub it out so we can + // assert the call without side effects. + assignSpy = vi + .spyOn(window.location, "assign") + .mockImplementation(() => {}); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it("redirects to AUTH on a 401 when AUTH is set", () => { + vi.stubEnv("AUTH", AUTH_URL); + + const result = maybeRedirectToAuth(401); + + expect(result).toBe(true); + expect(assignSpy).toHaveBeenCalledTimes(1); + expect(assignSpy).toHaveBeenCalledWith(AUTH_URL); + }); + + it("does nothing on a 401 when AUTH is unset", () => { + vi.stubEnv("AUTH", ""); + + const result = maybeRedirectToAuth(401); + + expect(result).toBe(false); + expect(assignSpy).not.toHaveBeenCalled(); + }); + + it("does nothing on a non-401 status even when AUTH is set", () => { + vi.stubEnv("AUTH", AUTH_URL); + + expect(maybeRedirectToAuth(500)).toBe(false); + expect(maybeRedirectToAuth(200)).toBe(false); + expect(maybeRedirectToAuth(403)).toBe(false); + expect(assignSpy).not.toHaveBeenCalled(); + }); + + it("does not redirect if already at the AUTH url (loop guard)", () => { + vi.stubEnv("AUTH", window.location.href); + + const result = maybeRedirectToAuth(401); + + expect(result).toBe(false); + expect(assignSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/api/redirect-on-auth-failure.ts b/src/api/redirect-on-auth-failure.ts new file mode 100644 index 00000000..aff33b9b --- /dev/null +++ b/src/api/redirect-on-auth-failure.ts @@ -0,0 +1,22 @@ +/** + * When the app is built with the `AUTH` env var set to an OSC login URL and the + * manager returns HTTP 401 (token expired / not logged in), navigate the browser + * to that login URL so the user can (re)authenticate. + * + * When `AUTH` is unset the function is a no-op and returns false, leaving the + * existing OSC reauth flow unchanged. + * + * @returns true if a redirect was triggered, otherwise false. + */ +export const maybeRedirectToAuth = (status: number): boolean => { + if (status !== 401) return false; + + const authUrl = import.meta.env.AUTH; + if (typeof authUrl !== "string" || authUrl.length === 0) return false; + + // Guard against a redirect loop if we are already at the AUTH url. + if (window.location.href === authUrl) return false; + + window.location.assign(authUrl); + return true; +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index b1f45c78..b05a7dbf 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,2 +1,15 @@ /// /// + +interface ImportMetaEnv { + /** + * Optional OSC login URL. When set at build time (e.g. + * `AUTH=https://app.osaas.io/?redirect=/dashboard/service/eyevinn-intercom-manager`), + * the app redirects here whenever the manager returns HTTP 401. + */ + readonly AUTH?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/vite.config.ts b/vite.config.ts index 3c438ca9..29d13cac 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,9 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), svgr()], + // Expose the literally-named `AUTH` build-time env var (no VITE_ prefix) to + // `import.meta.env` in addition to the default `VITE_`-prefixed vars. + envPrefix: ["VITE_", "AUTH"], test: { globals: true, environment: "happy-dom",