refactor(frontend): one account per browser, its local state under plain keys
The per-tab account pin, the X-Siftlode-Account header and ?account= go: the session cookie alone says who is asking. localStorage keys lose their account suffix; the browser records whose state it holds (siftlode.owner), BootGate hands it to the account the server answered as, and a different account starts from a clean slate (the public /watch keys stay). The first boot moves the account's old per-account copies onto the plain keys and drops other accounts' copies. Until the claim, and after another tab claimed the browser for someone else, the account helpers read and write nothing. The account panel lists no other accounts: "Add another account" becomes "Sign in as someone else" (Google's account chooser; the pick replaces this account, a cancel keeps it), shown where Google sign-in is configured.
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
// The shared HTTP machinery behind every api.* domain module: the fetch wrapper (`req`), the typed
|
||||
// error, structured-error localization, the connectivity toast, the per-tab active-account plumbing,
|
||||
// error, structured-error localization, the connectivity toast, the identity headers,
|
||||
// and the keepalive beacon. Domain modules import from here; nothing here imports a domain module,
|
||||
// so the runtime graph is a clean barrel → domain → core with no cycles.
|
||||
import { getActiveToasts, notify, remove } from "../notifications";
|
||||
import i18n from "../../i18n";
|
||||
import { formatBytes, formatDecimal } from "../format";
|
||||
import { reportError } from "../errorDialog";
|
||||
import { activeAccountId, pinActiveAccount } from "../activeAccount";
|
||||
import { logRequest, QUIET_HEADER, REQUEST_ID_HEADER, ROUTE_HEADER, send } from "../http";
|
||||
import { currentDebugSession, newRequestId } from "../log";
|
||||
|
||||
@@ -268,28 +267,16 @@ export const setupHeaders = (token: string) => ({
|
||||
"X-Setup-Token": token,
|
||||
});
|
||||
|
||||
// --- Per-tab active account --------------------------------------------------------------
|
||||
// The signed session cookie is the browser's account "wallet"; which account a given TAB acts
|
||||
// as is chosen here and sent per-request, so two tabs can run two accounts at once. Stored in
|
||||
// sessionStorage (per-tab, survives F5, not shared across tabs). null = use the session default.
|
||||
// The storage key + reader live in storage.ts (so its per-account helpers can use them too).
|
||||
const ACTIVE_ACCOUNT_HEADER = "X-Siftlode-Account";
|
||||
// The debug session this tab is recording: the server tags its own lines of the request with
|
||||
// it, and logs them at the session's level. Ignored by the server unless it is the caller's own.
|
||||
const DEBUG_SESSION_HEADER = "X-Debug-Session";
|
||||
|
||||
export const getActiveAccount = activeAccountId;
|
||||
|
||||
/** Who this request is from — the tab's account and the debug session it records into — as the
|
||||
* headers every request path sends (`req`, `beacon`, `uploadReq`): one definition, so a path
|
||||
* cannot quietly go out without them. */
|
||||
/** What this request carries besides the session cookie (which alone says who is asking) — the
|
||||
* debug session it records into — as the headers every request path sends (`req`, `beacon`,
|
||||
* `uploadReq`): one definition, so a path cannot quietly go out without them. */
|
||||
export function identityHeaders(): Record<string, string> {
|
||||
const active = getActiveAccount();
|
||||
const session = currentDebugSession();
|
||||
return {
|
||||
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
|
||||
...(session ? { [DEBUG_SESSION_HEADER]: session } : {}),
|
||||
};
|
||||
return session ? { [DEBUG_SESSION_HEADER]: session } : {};
|
||||
}
|
||||
|
||||
// Fire-and-forget POST that survives page unload (keepalive) — for every write a closing page must
|
||||
@@ -315,8 +302,6 @@ export function beacon(url: string, body: Record<string, unknown>, onSettled?: (
|
||||
}
|
||||
}
|
||||
|
||||
export const setActiveAccount = pinActiveAccount;
|
||||
|
||||
export async function req<T = unknown>(
|
||||
url: string,
|
||||
opts: RequestInit = {},
|
||||
@@ -398,7 +383,7 @@ export async function req<T = unknown>(
|
||||
|
||||
// Upload with progress. `req` rides `fetch`, which can't report upload progress, so a file upload
|
||||
// uses XHR instead: the caller gets a 0–100% callback as the body streams and an optional
|
||||
// AbortSignal to cancel a large upload. Everything else mirrors `req` — the per-tab account header,
|
||||
// AbortSignal to cancel a large upload. Everything else mirrors `req` — the identity headers,
|
||||
// credentials, the structured-detail localization, the global error modal for 4xx/5xx, and an
|
||||
// `HttpError` throw — so callers handle failures exactly as they do a `req` call.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user