fix(dashboard): restore tokenized control ui links
parent
e78ae48e69
commit
c5194d8148
|
|
@ -83,8 +83,8 @@ describe("dashboardCommand", () => {
|
||||||
customBindHost: undefined,
|
customBindHost: undefined,
|
||||||
basePath: undefined,
|
basePath: undefined,
|
||||||
});
|
});
|
||||||
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/");
|
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||||
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/");
|
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||||
expect(runtime.log).toHaveBeenCalledWith(
|
expect(runtime.log).toHaveBeenCalledWith(
|
||||||
"Opened in your browser. Keep that tab to control OpenClaw.",
|
"Opened in your browser. Keep that tab to control OpenClaw.",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export async function dashboardCommand(
|
||||||
const bind = cfg.gateway?.bind ?? "loopback";
|
const bind = cfg.gateway?.bind ?? "loopback";
|
||||||
const basePath = cfg.gateway?.controlUi?.basePath;
|
const basePath = cfg.gateway?.controlUi?.basePath;
|
||||||
const customBindHost = cfg.gateway?.customBindHost;
|
const customBindHost = cfg.gateway?.customBindHost;
|
||||||
|
const token = cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
|
||||||
|
|
||||||
const links = resolveControlUiLinks({
|
const links = resolveControlUiLinks({
|
||||||
port,
|
port,
|
||||||
|
|
@ -30,7 +31,10 @@ export async function dashboardCommand(
|
||||||
customBindHost,
|
customBindHost,
|
||||||
basePath,
|
basePath,
|
||||||
});
|
});
|
||||||
const dashboardUrl = links.httpUrl;
|
// Prefer URL fragment to avoid leaking auth tokens via query params.
|
||||||
|
const dashboardUrl = token
|
||||||
|
? `${links.httpUrl}#token=${encodeURIComponent(token)}`
|
||||||
|
: links.httpUrl;
|
||||||
|
|
||||||
runtime.log(`Dashboard URL: ${dashboardUrl}`);
|
runtime.log(`Dashboard URL: ${dashboardUrl}`);
|
||||||
|
|
||||||
|
|
@ -48,6 +52,7 @@ export async function dashboardCommand(
|
||||||
hint = formatControlUiSshHint({
|
hint = formatControlUiSshHint({
|
||||||
port,
|
port,
|
||||||
basePath,
|
basePath,
|
||||||
|
token: token || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -179,16 +179,24 @@ export async function detectBrowserOpenSupport(): Promise<BrowserOpenSupport> {
|
||||||
return { ok: true, command: resolved.command };
|
return { ok: true, command: resolved.command };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatControlUiSshHint(params: { port: number; basePath?: string }): string {
|
export function formatControlUiSshHint(params: {
|
||||||
|
port: number;
|
||||||
|
basePath?: string;
|
||||||
|
token?: string;
|
||||||
|
}): string {
|
||||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||||
const uiPath = basePath ? `${basePath}/` : "/";
|
const uiPath = basePath ? `${basePath}/` : "/";
|
||||||
const localUrl = `http://localhost:${params.port}${uiPath}`;
|
const localUrl = `http://localhost:${params.port}${uiPath}`;
|
||||||
|
const authedUrl = params.token
|
||||||
|
? `${localUrl}#token=${encodeURIComponent(params.token)}`
|
||||||
|
: undefined;
|
||||||
const sshTarget = resolveSshTargetHint();
|
const sshTarget = resolveSshTargetHint();
|
||||||
return [
|
return [
|
||||||
"No GUI detected. Open from your computer:",
|
"No GUI detected. Open from your computer:",
|
||||||
`ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`,
|
`ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`,
|
||||||
"Then open:",
|
"Then open:",
|
||||||
localUrl,
|
localUrl,
|
||||||
|
authedUrl,
|
||||||
"Docs:",
|
"Docs:",
|
||||||
"https://docs.openclaw.ai/gateway/remote",
|
"https://docs.openclaw.ai/gateway/remote",
|
||||||
"https://docs.openclaw.ai/web/control-ui",
|
"https://docs.openclaw.ai/web/control-ui",
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,10 @@ export async function finalizeOnboardingWizard(
|
||||||
customBindHost: settings.customBindHost,
|
customBindHost: settings.customBindHost,
|
||||||
basePath: controlUiBasePath,
|
basePath: controlUiBasePath,
|
||||||
});
|
});
|
||||||
const dashboardUrl = links.httpUrl;
|
const authedUrl =
|
||||||
|
settings.authMode === "token" && settings.gatewayToken
|
||||||
|
? `${links.httpUrl}#token=${encodeURIComponent(settings.gatewayToken)}`
|
||||||
|
: links.httpUrl;
|
||||||
const gatewayProbe = await probeGatewayReachable({
|
const gatewayProbe = await probeGatewayReachable({
|
||||||
url: links.wsUrl,
|
url: links.wsUrl,
|
||||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||||
|
|
@ -275,7 +278,10 @@ export async function finalizeOnboardingWizard(
|
||||||
|
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
`Web UI: ${dashboardUrl}`,
|
`Web UI: ${links.httpUrl}`,
|
||||||
|
settings.authMode === "token" && settings.gatewayToken
|
||||||
|
? `Web UI (with token): ${authedUrl}`
|
||||||
|
: undefined,
|
||||||
`Gateway WS: ${links.wsUrl}`,
|
`Gateway WS: ${links.wsUrl}`,
|
||||||
gatewayStatusLine,
|
gatewayStatusLine,
|
||||||
"Docs: https://docs.openclaw.ai/web/control-ui",
|
"Docs: https://docs.openclaw.ai/web/control-ui",
|
||||||
|
|
@ -312,7 +318,7 @@ export async function finalizeOnboardingWizard(
|
||||||
`Generate token: ${formatCliCommand("openclaw doctor --generate-gateway-token")}`,
|
`Generate token: ${formatCliCommand("openclaw doctor --generate-gateway-token")}`,
|
||||||
"Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).",
|
"Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).",
|
||||||
`Open the dashboard anytime: ${formatCliCommand("openclaw dashboard --no-open")}`,
|
`Open the dashboard anytime: ${formatCliCommand("openclaw dashboard --no-open")}`,
|
||||||
"Paste the token into Control UI settings if prompted.",
|
"If prompted: paste the token into Control UI settings (or use the tokenized dashboard URL).",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Token",
|
"Token",
|
||||||
);
|
);
|
||||||
|
|
@ -341,22 +347,24 @@ export async function finalizeOnboardingWizard(
|
||||||
} else if (hatchChoice === "web") {
|
} else if (hatchChoice === "web") {
|
||||||
const browserSupport = await detectBrowserOpenSupport();
|
const browserSupport = await detectBrowserOpenSupport();
|
||||||
if (browserSupport.ok) {
|
if (browserSupport.ok) {
|
||||||
controlUiOpened = await openUrl(dashboardUrl);
|
controlUiOpened = await openUrl(authedUrl);
|
||||||
if (!controlUiOpened) {
|
if (!controlUiOpened) {
|
||||||
controlUiOpenHint = formatControlUiSshHint({
|
controlUiOpenHint = formatControlUiSshHint({
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
basePath: controlUiBasePath,
|
basePath: controlUiBasePath,
|
||||||
|
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controlUiOpenHint = formatControlUiSshHint({
|
controlUiOpenHint = formatControlUiSshHint({
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
basePath: controlUiBasePath,
|
basePath: controlUiBasePath,
|
||||||
|
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
`Dashboard link: ${dashboardUrl}`,
|
`Dashboard link (with token): ${authedUrl}`,
|
||||||
controlUiOpened
|
controlUiOpened
|
||||||
? "Opened in your browser. Keep that tab to control OpenClaw."
|
? "Opened in your browser. Keep that tab to control OpenClaw."
|
||||||
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
||||||
|
|
@ -442,23 +450,25 @@ export async function finalizeOnboardingWizard(
|
||||||
if (shouldOpenControlUi) {
|
if (shouldOpenControlUi) {
|
||||||
const browserSupport = await detectBrowserOpenSupport();
|
const browserSupport = await detectBrowserOpenSupport();
|
||||||
if (browserSupport.ok) {
|
if (browserSupport.ok) {
|
||||||
controlUiOpened = await openUrl(dashboardUrl);
|
controlUiOpened = await openUrl(authedUrl);
|
||||||
if (!controlUiOpened) {
|
if (!controlUiOpened) {
|
||||||
controlUiOpenHint = formatControlUiSshHint({
|
controlUiOpenHint = formatControlUiSshHint({
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
basePath: controlUiBasePath,
|
basePath: controlUiBasePath,
|
||||||
|
token: settings.gatewayToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controlUiOpenHint = formatControlUiSshHint({
|
controlUiOpenHint = formatControlUiSshHint({
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
basePath: controlUiBasePath,
|
basePath: controlUiBasePath,
|
||||||
|
token: settings.gatewayToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
`Dashboard link: ${dashboardUrl}`,
|
`Dashboard link (with token): ${authedUrl}`,
|
||||||
controlUiOpened
|
controlUiOpened
|
||||||
? "Opened in your browser. Keep that tab to control OpenClaw."
|
? "Opened in your browser. Keep that tab to control OpenClaw."
|
||||||
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
||||||
|
|
|
||||||
|
|
@ -82,18 +82,26 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySettingsFromUrl(host: SettingsHost) {
|
export function applySettingsFromUrl(host: SettingsHost) {
|
||||||
if (!window.location.search) {
|
if (!window.location.search && !window.location.hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams(window.location.search);
|
const url = new URL(window.location.href);
|
||||||
const tokenRaw = params.get("token");
|
const params = new URLSearchParams(url.search);
|
||||||
const passwordRaw = params.get("password");
|
const hashParams = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
|
||||||
const sessionRaw = params.get("session");
|
|
||||||
const gatewayUrlRaw = params.get("gatewayUrl");
|
const tokenRaw = params.get("token") ?? hashParams.get("token");
|
||||||
|
const passwordRaw = params.get("password") ?? hashParams.get("password");
|
||||||
|
const sessionRaw = params.get("session") ?? hashParams.get("session");
|
||||||
|
const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl");
|
||||||
let shouldCleanUrl = false;
|
let shouldCleanUrl = false;
|
||||||
|
|
||||||
if (tokenRaw != null) {
|
if (tokenRaw != null) {
|
||||||
|
const token = tokenRaw.trim();
|
||||||
|
if (token && token !== host.settings.token) {
|
||||||
|
applySettings(host, { ...host.settings, token });
|
||||||
|
}
|
||||||
params.delete("token");
|
params.delete("token");
|
||||||
|
hashParams.delete("token");
|
||||||
shouldCleanUrl = true;
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +111,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||||
(host as { password: string }).password = password;
|
(host as { password: string }).password = password;
|
||||||
}
|
}
|
||||||
params.delete("password");
|
params.delete("password");
|
||||||
|
hashParams.delete("password");
|
||||||
shouldCleanUrl = true;
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,14 +133,16 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||||
host.pendingGatewayUrl = gatewayUrl;
|
host.pendingGatewayUrl = gatewayUrl;
|
||||||
}
|
}
|
||||||
params.delete("gatewayUrl");
|
params.delete("gatewayUrl");
|
||||||
|
hashParams.delete("gatewayUrl");
|
||||||
shouldCleanUrl = true;
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldCleanUrl) {
|
if (!shouldCleanUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.search = params.toString();
|
url.search = params.toString();
|
||||||
|
const nextHash = hashParams.toString();
|
||||||
|
url.hash = nextHash ? `#${nextHash}` : "";
|
||||||
window.history.replaceState({}, "", url.toString());
|
window.history.replaceState({}, "", url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,11 @@ describe("control UI routing", () => {
|
||||||
expect(container.scrollTop).toBe(maxScroll);
|
expect(container.scrollTop).toBe(maxScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips token URL params without importing them", async () => {
|
it("hydrates token from URL params and strips it", async () => {
|
||||||
const app = mountApp("/ui/overview?token=abc123");
|
const app = mountApp("/ui/overview?token=abc123");
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
|
|
||||||
expect(app.settings.token).toBe("");
|
expect(app.settings.token).toBe("abc123");
|
||||||
expect(window.location.pathname).toBe("/ui/overview");
|
expect(window.location.pathname).toBe("/ui/overview");
|
||||||
expect(window.location.search).toBe("");
|
expect(window.location.search).toBe("");
|
||||||
});
|
});
|
||||||
|
|
@ -169,7 +169,7 @@ describe("control UI routing", () => {
|
||||||
expect(window.location.search).toBe("");
|
expect(window.location.search).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not override stored settings from URL token params", async () => {
|
it("hydrates token from URL params even when settings already set", async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"openclaw.control.settings.v1",
|
"openclaw.control.settings.v1",
|
||||||
JSON.stringify({ token: "existing-token" }),
|
JSON.stringify({ token: "existing-token" }),
|
||||||
|
|
@ -177,8 +177,17 @@ describe("control UI routing", () => {
|
||||||
const app = mountApp("/ui/overview?token=abc123");
|
const app = mountApp("/ui/overview?token=abc123");
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
|
|
||||||
expect(app.settings.token).toBe("existing-token");
|
expect(app.settings.token).toBe("abc123");
|
||||||
expect(window.location.pathname).toBe("/ui/overview");
|
expect(window.location.pathname).toBe("/ui/overview");
|
||||||
expect(window.location.search).toBe("");
|
expect(window.location.search).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hydrates token from URL hash and strips it", async () => {
|
||||||
|
const app = mountApp("/ui/overview#token=abc123");
|
||||||
|
await app.updateComplete;
|
||||||
|
|
||||||
|
expect(app.settings.token).toBe("abc123");
|
||||||
|
expect(window.location.pathname).toBe("/ui/overview");
|
||||||
|
expect(window.location.hash).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue