fix(panel): route a setup-locked session to the wizard, not a permission error (tracker #8)
A session that still owes forced onboarding gets 403 setup_required from every protected route, but the panel rendered it as the generic forbidden line — the one step that unlocks the app read as missing authorization. api.ts now announces the code on a window event (client module has no router) and the App shell, inside the Router, navigates to /setup; the wizard resumes from the surviving session with or without a token. Other 403s are untouched. Panel tests: +2 (fires on setup_required, silent on any other 403).
This commit is contained in:
3 files changed
+83
-2
No files matched your search
@@ -44,6 +44,21 @@ function isApiError(x: unknown): x is { error: { code: string; message: string }
|
||||
);
|
||||
}
|
||||
|
||||
// A locked session — one that still owes the forced onboarding (passkey
|
||||
// enrollment) — gets `403 setup_required` from every protected route. Rendered as
|
||||
// a generic permission error that reads as "you may not do this", when the truth
|
||||
// is "one step remains and completing it unlocks the app" (#8). api.ts cannot
|
||||
// navigate (no router here), so it announces the code on a window event; the
|
||||
// App-shell listener routes the person to /setup, which resumes from the session
|
||||
// without needing a token. Non-browser callers keep the plain error.
|
||||
export const SETUP_REQUIRED_EVENT = "felis:setup-required";
|
||||
|
||||
function announceSetupRequired(err: ApiError): void {
|
||||
if (err.status !== 403 || err.code !== "setup_required") return;
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(new Event(SETUP_REQUIRED_EVENT));
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const { apiBase } = await loadConfig();
|
||||
const res = await fetch(`${apiBase}${path}`, {
|
||||
@@ -62,6 +77,7 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
||||
code: isApiError(parsed) ? parsed.error.code : "error",
|
||||
message: isApiError(parsed) ? parsed.error.message : res.statusText,
|
||||
};
|
||||
announceSetupRequired(err);
|
||||
throw err;
|
||||
}
|
||||
return parsed as T;
|
||||
@@ -90,6 +106,7 @@ async function requestRaw<T>(
|
||||
code: isApiError(parsed) ? parsed.error.code : "error",
|
||||
message: isApiError(parsed) ? parsed.error.message : res.statusText,
|
||||
};
|
||||
announceSetupRequired(err);
|
||||
throw err;
|
||||
}
|
||||
return parsed as T;
|
||||
@@ -501,6 +518,7 @@ export const api = {
|
||||
/* non-JSON error body (e.g. an ingress page): keep the status line */
|
||||
}
|
||||
const err: ApiError = { status: res.status, code, message };
|
||||
announceSetupRequired(err);
|
||||
throw err;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
|
||||
Reference in new issue
Block a user