fix(ui): restore control-ui query token imports
This commit is contained in:
parent
4f620bebe5
commit
9872c551c2
@ -242,7 +242,7 @@ http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-toke
|
||||
Notes:
|
||||
|
||||
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
|
||||
- `token` is imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; it is not stored in localStorage.
|
||||
- `token` is preferably imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; legacy `?token=` query params are also imported once for compatibility and then removed.
|
||||
- `password` is kept in memory only.
|
||||
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
|
||||
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.
|
||||
|
||||
@ -54,6 +54,8 @@ type SettingsHost = {
|
||||
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
|
||||
logsPollInterval: number | null;
|
||||
debugPollInterval: number | null;
|
||||
pendingGatewayUrl?: string | null;
|
||||
pendingGatewayToken?: string | null;
|
||||
};
|
||||
|
||||
function createStorageMock(): Storage {
|
||||
@ -80,6 +82,49 @@ function createStorageMock(): Storage {
|
||||
};
|
||||
}
|
||||
|
||||
function setTestWindowUrl(urlString: string) {
|
||||
const current = new URL(urlString);
|
||||
const history = {
|
||||
replaceState: vi.fn((_state: unknown, _title: string, nextUrl: string | URL) => {
|
||||
const next = new URL(String(nextUrl), current.toString());
|
||||
current.href = next.toString();
|
||||
current.protocol = next.protocol;
|
||||
current.host = next.host;
|
||||
current.pathname = next.pathname;
|
||||
current.search = next.search;
|
||||
current.hash = next.hash;
|
||||
}),
|
||||
};
|
||||
const locationLike = {
|
||||
get href() {
|
||||
return current.toString();
|
||||
},
|
||||
get protocol() {
|
||||
return current.protocol;
|
||||
},
|
||||
get host() {
|
||||
return current.host;
|
||||
},
|
||||
get pathname() {
|
||||
return current.pathname;
|
||||
},
|
||||
get search() {
|
||||
return current.search;
|
||||
},
|
||||
get hash() {
|
||||
return current.hash;
|
||||
},
|
||||
};
|
||||
vi.stubGlobal("window", {
|
||||
location: locationLike,
|
||||
history,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
} as unknown as Window & typeof globalThis);
|
||||
vi.stubGlobal("location", locationLike as Location);
|
||||
return { history, location: locationLike };
|
||||
}
|
||||
|
||||
const createHost = (tab: Tab): SettingsHost => ({
|
||||
settings: {
|
||||
gatewayUrl: "",
|
||||
@ -233,3 +278,48 @@ describe("setTabFromRoute", () => {
|
||||
expect(root.style.colorScheme).toBe("light");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applySettingsFromUrl", () => {
|
||||
let appSettings: AppSettingsModule;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal("localStorage", createStorageMock());
|
||||
vi.stubGlobal("sessionStorage", createStorageMock());
|
||||
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
||||
setTestWindowUrl("https://control.example/ui/overview");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("hydrates query token params and strips them from the URL", async () => {
|
||||
appSettings ??= await import("./app-settings.ts");
|
||||
setTestWindowUrl("https://control.example/ui/overview?token=abc123");
|
||||
const host = createHost("overview");
|
||||
host.settings.gatewayUrl = "wss://control.example/openclaw";
|
||||
|
||||
appSettings.applySettingsFromUrl(host);
|
||||
|
||||
expect(host.settings.token).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
|
||||
it("keeps query token params pending when a gatewayUrl confirmation is required", async () => {
|
||||
appSettings ??= await import("./app-settings.ts");
|
||||
setTestWindowUrl(
|
||||
"https://control.example/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123",
|
||||
);
|
||||
const host = createHost("overview");
|
||||
host.settings.gatewayUrl = "wss://control.example/openclaw";
|
||||
|
||||
appSettings.applySettingsFromUrl(host);
|
||||
|
||||
expect(host.settings.token).toBe("");
|
||||
expect(host.pendingGatewayUrl).toBe("wss://other-gateway.example/openclaw");
|
||||
expect(host.pendingGatewayToken).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@ -107,7 +107,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl");
|
||||
const nextGatewayUrl = gatewayUrlRaw?.trim() ?? "";
|
||||
const gatewayUrlChanged = Boolean(nextGatewayUrl && nextGatewayUrl !== host.settings.gatewayUrl);
|
||||
const tokenRaw = hashParams.get("token");
|
||||
const tokenRaw = hashParams.get("token") ?? params.get("token");
|
||||
const passwordRaw = params.get("password") ?? hashParams.get("password");
|
||||
const sessionRaw = params.get("session") ?? hashParams.get("session");
|
||||
let shouldCleanUrl = false;
|
||||
|
||||
@ -146,11 +146,11 @@ describe("control UI routing", () => {
|
||||
expect(container.scrollTop).toBe(maxScroll);
|
||||
});
|
||||
|
||||
it("strips query token params without importing them", async () => {
|
||||
it("hydrates token from query params and strips them", async () => {
|
||||
const app = mountApp("/ui/overview?token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
|
||||
undefined,
|
||||
);
|
||||
@ -236,6 +236,28 @@ describe("control UI routing", () => {
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
it("keeps a query token pending until the gateway URL change is confirmed", async () => {
|
||||
const app = mountApp(
|
||||
"/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123",
|
||||
);
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.gatewayUrl).not.toBe("wss://other-gateway.example/openclaw");
|
||||
expect(app.settings.token).toBe("");
|
||||
|
||||
const confirmButton = Array.from(app.querySelectorAll<HTMLButtonElement>("button")).find(
|
||||
(button) => button.textContent?.trim() === "Confirm",
|
||||
);
|
||||
expect(confirmButton).not.toBeUndefined();
|
||||
confirmButton?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.gatewayUrl).toBe("wss://other-gateway.example/openclaw");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(window.location.search).toBe("");
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
it("restores the token after a same-tab refresh", async () => {
|
||||
const first = mountApp("/ui/overview#token=abc123");
|
||||
await first.updateComplete;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user