Merge pull request #492 from pikasTech/fix/cloud-web-auth-cookie
fix: restore cloud web server auth from local session
This commit is contained in:
@@ -42,6 +42,18 @@ test("cloud web route policy proxies public Code Agent chat without gating other
|
|||||||
publicRoute: true,
|
publicRoute: true,
|
||||||
routeKey: "GET /v1/agent/chat/result/trc_live"
|
routeKey: "GET /v1/agent/chat/result/trc_live"
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1"), {
|
||||||
|
proxy: true,
|
||||||
|
authRequired: true,
|
||||||
|
publicRoute: false,
|
||||||
|
routeKey: "GET /v1"
|
||||||
|
});
|
||||||
|
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/m3/status"), {
|
||||||
|
proxy: true,
|
||||||
|
authRequired: true,
|
||||||
|
publicRoute: false,
|
||||||
|
routeKey: "GET /v1/m3/status"
|
||||||
|
});
|
||||||
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.mjs"), {
|
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.mjs"), {
|
||||||
proxy: false,
|
proxy: false,
|
||||||
authRequired: false,
|
authRequired: false,
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ async function readActiveSession(config) {
|
|||||||
if (serverSession.available) {
|
if (serverSession.available) {
|
||||||
if (!serverSession.authenticated && config.mode !== "server") {
|
if (!serverSession.authenticated && config.mode !== "server") {
|
||||||
const localSession = readLocalSession(config);
|
const localSession = readLocalSession(config);
|
||||||
if (localSession.authenticated) return localSession;
|
if (localSession.authenticated) {
|
||||||
|
const restoredServerSession = await restoreServerSessionFromLocal(config);
|
||||||
|
if (restoredServerSession.authenticated) return restoredServerSession;
|
||||||
|
return localSession;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
authenticated: serverSession.authenticated,
|
authenticated: serverSession.authenticated,
|
||||||
@@ -90,6 +94,18 @@ async function readActiveSession(config) {
|
|||||||
return readLocalSession(config);
|
return readLocalSession(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restoreServerSessionFromLocal(config) {
|
||||||
|
const serverResult = await attemptServerLogin(config.username, config.password);
|
||||||
|
if (!serverResult.available || !serverResult.authenticated) return { authenticated: false };
|
||||||
|
writeLocalSession(config);
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
mode: "server",
|
||||||
|
user: serverResult.user,
|
||||||
|
expiresAt: serverResult.expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchServerSession() {
|
async function fetchServerSession() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(AUTH_ROUTE.session, {
|
const response = await fetch(AUTH_ROUTE.session, {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { ensureWorkbenchAuth } from "./auth.mjs";
|
||||||
|
|
||||||
|
const AUTH_STORAGE_KEY = "hwlab.cloudWorkbench.auth.v1";
|
||||||
|
|
||||||
|
test("local auth restore refreshes the server cookie before showing the workbench", async () => {
|
||||||
|
const harness = installAuthHarness({
|
||||||
|
localSession: validLocalSession(),
|
||||||
|
responses: {
|
||||||
|
"/auth/session": { authenticated: false },
|
||||||
|
"/auth/login": {
|
||||||
|
authenticated: true,
|
||||||
|
user: { username: "admin" },
|
||||||
|
expiresAt: "2026-05-27T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await ensureWorkbenchAuth({ loginShell: harness.loginShell, appShell: harness.appShell });
|
||||||
|
|
||||||
|
assert.equal(session.authenticated, true);
|
||||||
|
assert.equal(session.mode, "server");
|
||||||
|
assert.deepEqual(harness.fetchCalls.map((call) => call.path), ["/auth/session", "/auth/login"]);
|
||||||
|
assert.deepEqual(harness.fetchCalls.map((call) => call.method), ["GET", "POST"]);
|
||||||
|
assert.deepEqual(JSON.parse(harness.fetchCalls[1].body), { username: "admin", password: "hwlab2026" });
|
||||||
|
assert.equal(harness.loginShell.hidden, true);
|
||||||
|
assert.equal(harness.appShell.hidden, false);
|
||||||
|
assert.equal(globalThis.document.body.dataset.authState, "authenticated");
|
||||||
|
} finally {
|
||||||
|
harness.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("local auth restore remains usable when server cookie refresh is unavailable", async () => {
|
||||||
|
const harness = installAuthHarness({
|
||||||
|
localSession: validLocalSession(),
|
||||||
|
responses: {
|
||||||
|
"/auth/session": { authenticated: false },
|
||||||
|
"/auth/login": { authenticated: false, error: "invalid_credentials", status: 401 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await ensureWorkbenchAuth({ loginShell: harness.loginShell, appShell: harness.appShell });
|
||||||
|
|
||||||
|
assert.equal(session.authenticated, true);
|
||||||
|
assert.equal(session.mode, "local");
|
||||||
|
assert.deepEqual(harness.fetchCalls.map((call) => call.path), ["/auth/session", "/auth/login"]);
|
||||||
|
assert.equal(harness.loginShell.hidden, true);
|
||||||
|
assert.equal(harness.appShell.hidden, false);
|
||||||
|
assert.equal(globalThis.document.body.dataset.authState, "authenticated");
|
||||||
|
} finally {
|
||||||
|
harness.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function installAuthHarness({ localSession, responses }) {
|
||||||
|
const previous = {
|
||||||
|
window: globalThis.window,
|
||||||
|
document: globalThis.document,
|
||||||
|
fetch: globalThis.fetch,
|
||||||
|
config: globalThis.HWLAB_CLOUD_WEB_CONFIG
|
||||||
|
};
|
||||||
|
const storage = new Map();
|
||||||
|
if (localSession) storage.set(AUTH_STORAGE_KEY, JSON.stringify(localSession));
|
||||||
|
const fetchCalls = [];
|
||||||
|
const loginShell = elementStub();
|
||||||
|
const appShell = elementStub();
|
||||||
|
|
||||||
|
globalThis.HWLAB_CLOUD_WEB_CONFIG = { auth: { mode: "auto", username: "admin", password: "hwlab2026" } };
|
||||||
|
globalThis.document = { body: { dataset: {} } };
|
||||||
|
globalThis.window = {
|
||||||
|
localStorage: {
|
||||||
|
getItem: (key) => storage.get(key) ?? null,
|
||||||
|
setItem: (key, value) => storage.set(key, String(value)),
|
||||||
|
removeItem: (key) => storage.delete(key)
|
||||||
|
},
|
||||||
|
setTimeout: () => 0,
|
||||||
|
clearTimeout: () => {}
|
||||||
|
};
|
||||||
|
globalThis.fetch = async (path, options = {}) => {
|
||||||
|
const response = responses[path] ?? { status: 404, authenticated: false };
|
||||||
|
fetchCalls.push({
|
||||||
|
path,
|
||||||
|
method: options.method ?? "GET",
|
||||||
|
body: options.body ?? null,
|
||||||
|
credentials: options.credentials ?? null
|
||||||
|
});
|
||||||
|
const status = Number(response.status ?? (response.authenticated === false && response.error ? 401 : 200));
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
ok: status >= 200 && status < 300,
|
||||||
|
json: async () => ({ ...response, status: undefined })
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginShell,
|
||||||
|
appShell,
|
||||||
|
fetchCalls,
|
||||||
|
restore() {
|
||||||
|
globalThis.window = previous.window;
|
||||||
|
globalThis.document = previous.document;
|
||||||
|
globalThis.fetch = previous.fetch;
|
||||||
|
globalThis.HWLAB_CLOUD_WEB_CONFIG = previous.config;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validLocalSession() {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
username: "admin",
|
||||||
|
issuedAt: Date.now() - 60_000,
|
||||||
|
expiresAt: Date.now() + 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementStub() {
|
||||||
|
return {
|
||||||
|
hidden: false,
|
||||||
|
attributes: new Map(),
|
||||||
|
setAttribute(name, value) {
|
||||||
|
this.attributes.set(name, String(value));
|
||||||
|
},
|
||||||
|
removeAttribute(name) {
|
||||||
|
this.attributes.delete(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
|
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
|
||||||
"check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
|
"check": "node --test auth.test.mjs code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json",
|
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json",
|
||||||
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json",
|
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json",
|
||||||
|
|||||||
Reference in New Issue
Block a user