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
353 expect(fetchMock).toHaveBeenCalledTimes(3);353 expect(fetchMock).toHaveBeenCalledTimes(3);
354 });354 });
355 355
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
356 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {384 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {
357 vi.useFakeTimers();385 vi.useFakeTimers();
358 const fetchMock = vi386 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
110 return text || fallbackMessage;110 return text || fallbackMessage;
111}111}
112 112
113const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([
114 "Invalid bootstrap credential.",
115 "Unknown bootstrap credential.",
116]);
117
118function 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
138function 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
113async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {147async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {
114 return retryTransientBootstrap(async () => {148 return retryTransientBootstrap(async () => {
115 const payload: AuthBootstrapInput = { credential };149 const payload: AuthBootstrapInput = { credential };
@@ -123,7 +157,7 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot · 7 unmodified lines
123 });157 });
124 158
125 if (!response.ok) {159 if (!response.ok) {
126 const message = await response.text();160 const message = toFriendlyBootstrapErrorMessage(response.status, await response.text());
127 throw new BootstrapHttpError({161 throw new BootstrapHttpError({
128 message: message || `Failed to bootstrap auth session (${response.status}).`,162 message: message || `Failed to bootstrap auth session (${response.status}).`,
129 status: response.status,163 status: response.status,