fix(web): токен с недопустимыми символами — экран входа, а не «хаб не отвечает» (#21)

Сохранённый в localStorage токен с кириллицей, пробелом или переводом строки
давал TypeError у fetch, который превращался в HubDownError. Теперь call()
проверяет TOKEN_RE до запроса и бросает AuthError.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-14 20:12:35 +04:00
parent 51c2772dcc
commit c124e082ae
2 changed files with 10 additions and 1 deletions

View File

@ -41,6 +41,13 @@ describe("api", () => {
expect(fetch).not.toHaveBeenCalled(); expect(fetch).not.toHaveBeenCalled();
}); });
it("токен с недопустимыми символами — AuthError без запроса", async () => {
setToken("ток ен");
const fetch = mockFetch(() => json(200, {}));
await expect(api.get("/board")).rejects.toBeInstanceOf(AuthError);
expect(fetch).not.toHaveBeenCalled();
});
it("checkToken проверяет переданный токен, не сохранённый", async () => { it("checkToken проверяет переданный токен, не сохранённый", async () => {
const fetch = mockFetch(() => json(200, { kind_labels: {} })); const fetch = mockFetch(() => json(200, { kind_labels: {} }));
await checkToken("candidate"); await checkToken("candidate");

View File

@ -1,6 +1,6 @@
// единственный клиент HTTP API хаба: тот же хост, клиентский токен в Authorization, ответы — JSON. // единственный клиент HTTP API хаба: тот же хост, клиентский токен в Authorization, ответы — JSON.
// Ошибки: AuthError — 401 или нет токена; HubDownError — сеть или 5xx; ApiError — прочие 4xx с {error, detail} // Ошибки: AuthError — 401 или нет токена; HubDownError — сеть или 5xx; ApiError — прочие 4xx с {error, detail}
import { getToken } from "../auth/token.ts"; import { getToken, TOKEN_RE } from "../auth/token.ts";
import type { HubSettings } from "./types.ts"; import type { HubSettings } from "./types.ts";
export class AuthError extends Error {} export class AuthError extends Error {}
@ -29,6 +29,8 @@ function withParams(path: string, params?: Params): string {
async function call<T>(method: string, path: string, body?: unknown, token = getToken()): Promise<T> { async function call<T>(method: string, path: string, body?: unknown, token = getToken()): Promise<T> {
if (!token) throw new AuthError("нет токена"); if (!token) throw new AuthError("нет токена");
// токен с недопустимыми символами (например, оставленный прежней доской) — сразу на экран входа, а не «хаб не отвечает»
if (!TOKEN_RE.test(token)) throw new AuthError("недопустимые символы в токене");
const headers: Record<string, string> = { Authorization: "Bearer " + token, Accept: "application/json" }; const headers: Record<string, string> = { Authorization: "Bearer " + token, Accept: "application/json" };
if (body !== undefined) headers["Content-Type"] = "application/json; charset=utf-8"; if (body !== undefined) headers["Content-Type"] = "application/json; charset=utf-8";
let resp: Response; let resp: Response;