From 6f82036e3845eb617e199df9c18bdd0b24775875 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Fri, 3 Jul 2026 17:14:11 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D1=82=D0=B8=D0=BF=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20API-?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82,=20=D1=82=D0=B8=D0=BF?= =?UTF-8?q?=D1=8B=20DTO,=20TanStack=20Query=20=D1=85=D1=83=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/client.test.ts | 46 ++++++++++++++++++++++ web/src/api/client.ts | 47 ++++++++++++++++++++++ web/src/api/types.ts | 36 +++++++++++++++++ web/src/hooks/useApi.ts | 81 ++++++++++++++++++++++++++++++++++++++ web/src/lib/config.ts | 2 + 5 files changed, 212 insertions(+) create mode 100644 web/src/api/client.test.ts create mode 100644 web/src/api/client.ts create mode 100644 web/src/api/types.ts create mode 100644 web/src/hooks/useApi.ts create mode 100644 web/src/lib/config.ts diff --git a/web/src/api/client.test.ts b/web/src/api/client.test.ts new file mode 100644 index 0000000..c894b9c --- /dev/null +++ b/web/src/api/client.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { api } from "./client" +import { DEFAULT_PROJECT_ID } from "@/lib/config" + +beforeEach(() => { vi.restoreAllMocks() }) + +function mockFetch(body: unknown, ok = true, status = 200) { + return vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok, status, + json: async () => body, + text: async () => JSON.stringify(body), + } as Response) +} + +describe("api client", () => { + it("lists accounts at project-scoped path", async () => { + const spy = mockFetch([{ id: "a1", provider: "selectel", comment: "" }]) + const accounts = await api.listAccounts() + expect(accounts).toHaveLength(1) + expect(spy).toHaveBeenCalledWith( + `/api/v1/projects/${DEFAULT_PROJECT_ID}/accounts`, + expect.objectContaining({ method: "GET" }), + ) + }) + + it("sends secret on account creation but path has no secret leakage in response typing", async () => { + const spy = mockFetch({ id: "a2", provider: "selectel", comment: "prod" }) + await api.createAccount({ provider: "selectel", secret: "TOKEN", comment: "prod" }) + const [, opts] = spy.mock.calls[0] + expect((opts as RequestInit).method).toBe("POST") + expect(String((opts as RequestInit).body)).toContain("TOKEN") + }) + + it("throws on non-ok response", async () => { + mockFetch({ error: "boom" }, false, 500) + await expect(api.listDomains()).rejects.toThrow() + }) + + it("applies with prune flag", async () => { + const spy = mockFetch({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 }) + await api.applyDomain("d1", { applyUpdates: true, applyPrunes: true }) + const [url, opts] = spy.mock.calls[0] + expect(url).toContain("/domains/d1/apply") + expect(String((opts as RequestInit).body)).toContain("applyPrunes") + }) +}) diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..4d5d65b --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,47 @@ +import { API_BASE } from "@/lib/config" +import type { + Account, CreateAccountInput, Template, CreateTemplateInput, + Domain, CreateDomainInput, ChangesetResponse, ApplyRequest, +} from "./types" + +async function req(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { "Content-Type": "application/json" }, + method: "GET", + ...init, + }) + if (!res.ok) { + let msg = `HTTP ${res.status}` + try { const b = await res.json(); if (b?.error) msg = b.error } catch { /* ignore */ } + throw new Error(msg) + } + if (res.status === 204) return undefined as T + return (await res.json()) as T +} + +export const api = { + listAccounts: () => req("/accounts"), + createAccount: (input: CreateAccountInput) => + req("/accounts", { method: "POST", body: JSON.stringify(input) }), + deleteAccount: (id: string) => req(`/accounts/${id}`, { method: "DELETE" }), + + listTemplates: () => req("/templates"), + createTemplate: (input: CreateTemplateInput) => + req