Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.local.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;'
5 changes: 5 additions & 0 deletions src/api/handle-fetch-request.ts
Original file line number Diff line number Diff line change
@@ -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 <T>(
Expand All @@ -19,6 +21,9 @@ export const handleFetchRequest = async <T>(

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);
Expand Down
59 changes: 59 additions & 0 deletions src/api/redirect-on-auth-failure.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.spyOn>;

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();
});
});
22 changes: 22 additions & 0 deletions src/api/redirect-on-auth-failure.ts
Original file line number Diff line number Diff line change
@@ -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;
};
13 changes: 13 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

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;
}
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading