History

fix(web): show friendly error for invalid pairing token (#2222)

Saikrishna Ambeti committed 6 months ago

Commit 8f50ca81 parent 4ceabac2 files · +63 −1Browse files

Unified · Split

2 files
  1. apps/web/src/authBootstrap.test.ts +28 −0
  2. apps/web/src/environments/primary/auth.ts +35 −1
apps/web/src/authBootstrap.test.ts+28−0
@@ -353,6 +353,34 @@ describe("resolveInitialServerAuthGateState", () => { · 352 unmodified lines
353353 expect(fetchMock).toHaveBeenCalledTimes(3);
354354 });
355355
356+ it("surfaces a friendly error message when an invalid pairing token is submitted", async () => {
357+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
358+ jsonResponse(
359+ {
360+ error: "Invalid bootstrap credential.",
361+ },
362+ {
363+ status: 401,
364+ },
365+ ),
366+ );
367+ vi.stubGlobal("fetch", fetchMock);
368+
369+ const { submitServerAuthCredential } = await import("./environments/primary");
370+
371+ await expect(submitServerAuthCredential("bad-token")).rejects.toThrow(
372+ "Invalid pairing token. Check the token and try again.",
373+ );
374+ expect(fetchMock).toHaveBeenCalledWith("http://localhost/api/auth/bootstrap", {
375+ body: JSON.stringify({ credential: "bad-token" }),
376+ credentials: "include",
377+ headers: {
378+ "content-type": "application/json",
379+ },
380+ method: "POST",
381+ });
382+ });
383+
356384 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {
357385 vi.useFakeTimers();
358386 const fetchMock = vi
apps/web/src/environments/primary/auth.ts+35−1
@@ -110,6 +110,40 @@ async function readErrorMessage(response: Response, fallbackMessage: string): Pr · 109 unmodified lines
110110 return text || fallbackMessage;
111111 }
112112
113+const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([
114+ "Invalid bootstrap credential.",
115+ "Unknown bootstrap credential.",
116+]);
117+
118+function parseBootstrapErrorMessage(message: string): string {
119+ const trimmed = message.trim();
120+ if (trimmed.length === 0) {
121+ return "";
122+ }
123+
124+ try {
125+ const parsed = JSON.parse(trimmed) as {
126+ error?: unknown;
127+ };
128+ if (typeof parsed.error === "string" && parsed.error.trim().length > 0) {
129+ return parsed.error.trim();
130+ }
131+ } catch {
132+ // Not JSON; fall back to plain text.
133+ }
134+
135+ return trimmed;
136+}
137+
138+function toFriendlyBootstrapErrorMessage(status: number, message: string): string {
139+ const parsedMessage = parseBootstrapErrorMessage(message);
140+ if (status === 401 && INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES.has(parsedMessage)) {
141+ return "Invalid pairing token. Check the token and try again.";
142+ }
143+
144+ return parsedMessage;
145+}
146+
113147 async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {
114148 return retryTransientBootstrap(async () => {
115149 const payload: AuthBootstrapInput = { credential };
@@ -123,7 +157,7 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot · 7 unmodified lines
123157 });
124158
125159 if (!response.ok) {
126− const message = await response.text();
160+ const message = toFriendlyBootstrapErrorMessage(response.status, await response.text());
127161 throw new BootstrapHttpError({
128162 message: message || `Failed to bootstrap auth session (${response.status}).`,
129163 status: response.status,