feat(web): soft-redirect to login on 401 via a navigate bridge (#48)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { redirectToLogin, setNavigate } from "./auth-redirect";
|
||||
|
||||
function stubLocation(pathname: string, search = "") {
|
||||
const assign = vi.fn();
|
||||
vi.stubGlobal("location", { pathname, search, assign });
|
||||
return assign;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setNavigate(null);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("uses the registered navigate to soft-redirect with reason + from", () => {
|
||||
const assign = stubLocation("/objects/abc", "?x=1");
|
||||
const navigate = vi.fn();
|
||||
setNavigate(navigate);
|
||||
|
||||
redirectToLogin();
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(
|
||||
"/login?reason=expired&from=%2Fobjects%2Fabc%3Fx%3D1",
|
||||
{ replace: true },
|
||||
);
|
||||
expect(assign).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("falls back to a hard navigation when no navigate is registered", () => {
|
||||
const assign = stubLocation("/objects/abc");
|
||||
|
||||
redirectToLogin();
|
||||
|
||||
expect(assign).toHaveBeenCalledWith("/login?reason=expired&from=%2Fobjects%2Fabc");
|
||||
});
|
||||
|
||||
test("does nothing when already on /login", () => {
|
||||
stubLocation("/login");
|
||||
const navigate = vi.fn();
|
||||
setNavigate(navigate);
|
||||
|
||||
redirectToLogin();
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,7 +1,23 @@
|
||||
/** Hard-navigate to login. Isolated so it can be spied/mocked in tests and swapped
|
||||
* for a router navigation if needed. */
|
||||
type NavigateFn = (to: string, opts?: { replace?: boolean }) => void;
|
||||
|
||||
let navigateFn: NavigateFn | null = null;
|
||||
|
||||
/** Register (or clear) the router's navigate fn. Called by NavigationBridge. */
|
||||
export function setNavigate(fn: NavigateFn | null): void {
|
||||
navigateFn = fn;
|
||||
}
|
||||
|
||||
/** Soft-redirect to login on a 401, preserving SPA state and the return path.
|
||||
* Falls back to a hard navigation when no router navigate is registered yet
|
||||
* (e.g. a 401 during the very first load). No-op when already on /login. */
|
||||
export function redirectToLogin(): void {
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.assign("/login");
|
||||
const { pathname, search } = window.location;
|
||||
if (pathname === "/login") return;
|
||||
const from = encodeURIComponent(pathname + search);
|
||||
const target = `/login?reason=expired&from=${from}`;
|
||||
if (navigateFn) {
|
||||
navigateFn(target, { replace: true });
|
||||
} else {
|
||||
window.location.assign(target);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user