mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(dashboard): keep gateway tokens out of URL storage
This commit is contained in:
@@ -151,6 +151,9 @@ describe("control UI routing", () => {
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
@@ -167,12 +170,18 @@ describe("control UI routing", () => {
|
||||
it("hydrates token from URL params even when settings already set", async () => {
|
||||
localStorage.setItem(
|
||||
"openclaw.control.settings.v1",
|
||||
JSON.stringify({ token: "existing-token" }),
|
||||
JSON.stringify({ token: "existing-token", gatewayUrl: "wss://gateway.example/openclaw" }),
|
||||
);
|
||||
const app = mountApp("/ui/overview?token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}")).toMatchObject({
|
||||
gatewayUrl: "wss://gateway.example/openclaw",
|
||||
});
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
@@ -182,6 +191,9 @@ describe("control UI routing", () => {
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
@@ -24,40 +24,147 @@ function createStorageMock(): Storage {
|
||||
};
|
||||
}
|
||||
|
||||
function setTestLocation(params: { protocol: string; host: string; pathname: string }) {
|
||||
if (typeof window !== "undefined" && window.history?.replaceState) {
|
||||
window.history.replaceState({}, "", params.pathname);
|
||||
return;
|
||||
}
|
||||
vi.stubGlobal("location", {
|
||||
protocol: params.protocol,
|
||||
host: params.host,
|
||||
pathname: params.pathname,
|
||||
} as Location);
|
||||
}
|
||||
|
||||
function setControlUiBasePath(value: string | undefined) {
|
||||
if (typeof window === "undefined") {
|
||||
vi.stubGlobal(
|
||||
"window",
|
||||
value == null
|
||||
? ({} as Window & typeof globalThis)
|
||||
: ({ __OPENCLAW_CONTROL_UI_BASE_PATH__: value } as Window & typeof globalThis),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (value == null) {
|
||||
delete window.__OPENCLAW_CONTROL_UI_BASE_PATH__;
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(window, "__OPENCLAW_CONTROL_UI_BASE_PATH__", {
|
||||
value,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function expectedGatewayUrl(basePath: string): string {
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${proto}://${location.host}${basePath}`;
|
||||
}
|
||||
|
||||
describe("loadSettings default gateway URL derivation", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal("localStorage", createStorageMock());
|
||||
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
||||
localStorage.clear();
|
||||
setControlUiBasePath(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
setControlUiBasePath(undefined);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("uses configured base path and normalizes trailing slash", async () => {
|
||||
vi.stubGlobal("location", {
|
||||
setTestLocation({
|
||||
protocol: "https:",
|
||||
host: "gateway.example:8443",
|
||||
pathname: "/ignored/path",
|
||||
} as Location);
|
||||
vi.stubGlobal("window", { __OPENCLAW_CONTROL_UI_BASE_PATH__: " /openclaw/ " } as Window &
|
||||
typeof globalThis);
|
||||
});
|
||||
setControlUiBasePath(" /openclaw/ ");
|
||||
|
||||
const { loadSettings } = await import("./storage.ts");
|
||||
expect(loadSettings().gatewayUrl).toBe("wss://gateway.example:8443/openclaw");
|
||||
expect(loadSettings().gatewayUrl).toBe(expectedGatewayUrl("/openclaw"));
|
||||
});
|
||||
|
||||
it("infers base path from nested pathname when configured base path is not set", async () => {
|
||||
vi.stubGlobal("location", {
|
||||
setTestLocation({
|
||||
protocol: "http:",
|
||||
host: "gateway.example:18789",
|
||||
pathname: "/apps/openclaw/chat",
|
||||
} as Location);
|
||||
vi.stubGlobal("window", {} as Window & typeof globalThis);
|
||||
});
|
||||
|
||||
const { loadSettings } = await import("./storage.ts");
|
||||
expect(loadSettings().gatewayUrl).toBe("ws://gateway.example:18789/apps/openclaw");
|
||||
expect(loadSettings().gatewayUrl).toBe(expectedGatewayUrl("/apps/openclaw"));
|
||||
});
|
||||
|
||||
it("ignores and scrubs legacy persisted tokens", async () => {
|
||||
setTestLocation({
|
||||
protocol: "https:",
|
||||
host: "gateway.example:8443",
|
||||
pathname: "/",
|
||||
});
|
||||
localStorage.setItem(
|
||||
"openclaw.control.settings.v1",
|
||||
JSON.stringify({
|
||||
gatewayUrl: "wss://gateway.example:8443/openclaw",
|
||||
token: "persisted-token",
|
||||
sessionKey: "agent",
|
||||
}),
|
||||
);
|
||||
|
||||
const { loadSettings } = await import("./storage.ts");
|
||||
expect(loadSettings()).toMatchObject({
|
||||
gatewayUrl: "wss://gateway.example:8443/openclaw",
|
||||
token: "",
|
||||
sessionKey: "agent",
|
||||
});
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}")).toEqual({
|
||||
gatewayUrl: "wss://gateway.example:8443/openclaw",
|
||||
sessionKey: "agent",
|
||||
lastActiveSessionKey: "agent",
|
||||
theme: "system",
|
||||
chatFocusMode: false,
|
||||
chatShowThinking: true,
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navGroupsCollapsed: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not persist gateway tokens when saving settings", async () => {
|
||||
setTestLocation({
|
||||
protocol: "https:",
|
||||
host: "gateway.example:8443",
|
||||
pathname: "/",
|
||||
});
|
||||
|
||||
const { saveSettings } = await import("./storage.ts");
|
||||
saveSettings({
|
||||
gatewayUrl: "wss://gateway.example:8443/openclaw",
|
||||
token: "memory-only-token",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "system",
|
||||
chatFocusMode: false,
|
||||
chatShowThinking: true,
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navGroupsCollapsed: {},
|
||||
});
|
||||
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}")).toEqual({
|
||||
gatewayUrl: "wss://gateway.example:8443/openclaw",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "system",
|
||||
chatFocusMode: false,
|
||||
chatShowThinking: true,
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navGroupsCollapsed: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const KEY = "openclaw.control.settings.v1";
|
||||
|
||||
type PersistedUiSettings = Omit<UiSettings, "token"> & { token?: never };
|
||||
|
||||
import { isSupportedLocale } from "../i18n/index.ts";
|
||||
import { inferBasePathFromPathname, normalizeBasePath } from "./navigation.ts";
|
||||
import type { ThemeMode } from "./theme.ts";
|
||||
@@ -50,12 +52,13 @@ export function loadSettings(): UiSettings {
|
||||
return defaults;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as Partial<UiSettings>;
|
||||
return {
|
||||
const settings = {
|
||||
gatewayUrl:
|
||||
typeof parsed.gatewayUrl === "string" && parsed.gatewayUrl.trim()
|
||||
? parsed.gatewayUrl.trim()
|
||||
: defaults.gatewayUrl,
|
||||
token: typeof parsed.token === "string" ? parsed.token : defaults.token,
|
||||
// Gateway auth is intentionally in-memory only; scrub any legacy persisted token on load.
|
||||
token: defaults.token,
|
||||
sessionKey:
|
||||
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
|
||||
? parsed.sessionKey.trim()
|
||||
@@ -89,11 +92,31 @@ export function loadSettings(): UiSettings {
|
||||
: defaults.navGroupsCollapsed,
|
||||
locale: isSupportedLocale(parsed.locale) ? parsed.locale : undefined,
|
||||
};
|
||||
if ("token" in parsed) {
|
||||
persistSettings(settings);
|
||||
}
|
||||
return settings;
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSettings(next: UiSettings) {
|
||||
localStorage.setItem(KEY, JSON.stringify(next));
|
||||
persistSettings(next);
|
||||
}
|
||||
|
||||
function persistSettings(next: UiSettings) {
|
||||
const persisted: PersistedUiSettings = {
|
||||
gatewayUrl: next.gatewayUrl,
|
||||
sessionKey: next.sessionKey,
|
||||
lastActiveSessionKey: next.lastActiveSessionKey,
|
||||
theme: next.theme,
|
||||
chatFocusMode: next.chatFocusMode,
|
||||
chatShowThinking: next.chatShowThinking,
|
||||
splitRatio: next.splitRatio,
|
||||
navCollapsed: next.navCollapsed,
|
||||
navGroupsCollapsed: next.navGroupsCollapsed,
|
||||
...(next.locale ? { locale: next.locale } : {}),
|
||||
};
|
||||
localStorage.setItem(KEY, JSON.stringify(persisted));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user