fix(webui): require token_issue_secret for LAN access with frontend auth

When host is set to 0.0.0.0, the gateway now enforces that either token
or token_issue_secret must be configured — it refuses to start otherwise.

Bootstrap endpoint behavior:
- token_issue_secret configured: always validate regardless of source IP
  (handles reverse-proxy scenarios where all connections appear as localhost)
- No secret: only localhost can bootstrap (local dev mode)

The frontend shows an authentication form when bootstrap returns 401/403,
persists the secret in localStorage, and retries automatically on reload.
This commit is contained in:
chengyongru
2026-05-06 23:51:51 +08:00
committed by Xubin Ren
parent 034bea1a44
commit 4efd904ccc
8 changed files with 265 additions and 49 deletions
+37 -1
View File
@@ -1,15 +1,51 @@
import type { BootstrapResponse } from "./types";
const SECRET_STORAGE_KEY = "nanobot-webui.bootstrap-secret";
/** Read a previously saved bootstrap secret from localStorage. */
export function loadSavedSecret(): string {
if (typeof window === "undefined") return "";
try {
return window.localStorage.getItem(SECRET_STORAGE_KEY) ?? "";
} catch {
return "";
}
}
/** Persist the bootstrap secret so page reloads don't re-prompt. */
export function saveSecret(secret: string): void {
try {
window.localStorage.setItem(SECRET_STORAGE_KEY, secret);
} catch {
// ignore storage errors (private mode, etc.)
}
}
/** Clear the saved bootstrap secret (sign out). */
export function clearSavedSecret(): void {
try {
window.localStorage.removeItem(SECRET_STORAGE_KEY);
} catch {
// ignore
}
}
/**
* Fetch a short-lived token + the WebSocket path from the gateway's
* ``/webui/bootstrap`` endpoint. Localhost-only on the server side.
* ``/webui/bootstrap`` endpoint.
*/
export async function fetchBootstrap(
baseUrl: string = "",
secret: string = "",
): Promise<BootstrapResponse> {
const headers: Record<string, string> = {};
if (secret) {
headers["X-Nanobot-Auth"] = secret;
}
const res = await fetch(`${baseUrl}/webui/bootstrap`, {
method: "GET",
credentials: "same-origin",
headers,
});
if (!res.ok) {
throw new Error(`bootstrap failed: HTTP ${res.status}`);