From 4a4c11c6bfe7d82ff7d0fde884671dfb18c571df Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 00:27:50 +0700 Subject: [PATCH] feat(firewall): rename revoke function to delete and update related UI components Refactored the revoke functionality for firewall clients to be more accurately represented as a delete operation. Updated the corresponding API call to use the DELETE method and modified the UI components to reflect this change, including confirmation dialogs and success messages. Adjusted tests to ensure the new delete functionality works as intended. --- apps/web/src/queries/firewall.ts | 8 +++--- apps/web/src/routes/_auth/firewall.tsx | 35 ++++++++++++++---------- internal/httpapi/routes_firewall_test.go | 31 +++++++++++---------- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/apps/web/src/queries/firewall.ts b/apps/web/src/queries/firewall.ts index 14d697e..52e42f6 100644 --- a/apps/web/src/queries/firewall.ts +++ b/apps/web/src/queries/firewall.ts @@ -60,16 +60,16 @@ export function useApproveFirewallClient() { }) } -export function useRevokeFirewallClient() { +export function useDeleteFirewallClient() { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => - apiJSON<{ status: string }>(`/v1/firewall/clients/${id}/revoke`, { method: 'POST' }), + apiJSON(`/v1/firewall/clients/${id}`, { method: 'DELETE' }), onSuccess: () => { - toast.success('Клиент отключён') + toast.success('Клиент удалён') void qc.invalidateQueries({ queryKey: firewallKeys.clients() }) }, - onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось отклонить'), + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить'), }) } diff --git a/apps/web/src/routes/_auth/firewall.tsx b/apps/web/src/routes/_auth/firewall.tsx index 0fee8ff..4c255ba 100644 --- a/apps/web/src/routes/_auth/firewall.tsx +++ b/apps/web/src/routes/_auth/firewall.tsx @@ -31,8 +31,8 @@ import { firewallRulesQueryOptions, useApproveFirewallClient, useCreateFirewallRule, + useDeleteFirewallClient, useDeleteFirewallRule, - useRevokeFirewallClient, } from '@/queries/firewall' import type { BgpCommunity, FirewallClient } from '@/types/api' @@ -56,7 +56,7 @@ function FirewallPage() { const clientsQ = useQuery(firewallClientsQueryOptions()) const rulesQ = useQuery(firewallRulesQueryOptions('tenant')) const approve = useApproveFirewallClient() - const revoke = useRevokeFirewallClient() + const deleteClient = useDeleteFirewallClient() const createRule = useCreateFirewallRule() const deleteRule = useDeleteFirewallRule() @@ -82,8 +82,13 @@ function FirewallPage() { const communities = communitiesQ.data?.items ?? [] - const clients = clientsQ.data?.items ?? [] - const pending = clients.filter((c) => c.status === 'pending') + const { activeClients, pending } = useMemo(() => { + const all = clientsQ.data?.items ?? [] + return { + activeClients: all.filter((c) => c.status !== 'revoked'), + pending: all.filter((c) => c.status === 'pending'), + } + }, [clientsQ.data?.items]) const rules = rulesQ.data?.items ?? [] const installCmd = useMemo(() => { @@ -190,18 +195,18 @@ function FirewallPage() { - Клиенты ({clients.length}) + Клиенты ({activeClients.length}) Правила ({rules.length}) Запросы ({pending.length}) approve.mutate(id)} - onReject={(id) => revoke.mutate(id)} + onReject={(id) => deleteClient.mutate(id)} approvePending={approve.isPending} - rejectPending={revoke.isPending} + rejectPending={deleteClient.isPending} /> @@ -263,9 +268,9 @@ function FirewallPage() { approve.mutate(id)} - onReject={(id) => revoke.mutate(id)} + onReject={(id) => deleteClient.mutate(id)} approvePending={approve.isPending} - rejectPending={revoke.isPending} + rejectPending={deleteClient.isPending} emptyTitle="Нет pending-запросов" /> @@ -342,7 +347,7 @@ function ClientsTable({ } title="Отклонить запрос?" - description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} — токен перестанет работать.`} + description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} — запись будет удалена, токен перестанет работать.`} confirmLabel="Отклонить" destructive onConfirm={() => onReject(c.id)} @@ -358,12 +363,12 @@ function ClientsTable({ className="text-destructive" disabled={rejectPending} > - Отозвать + Удалить } - title="Отозвать клиент?" - description={`${c.name} — blocklist перестанет отдаваться, токен будет недействителен.`} - confirmLabel="Отозвать" + title="Удалить клиент?" + description={`${c.name} — запись будет удалена, blocklist и токен перестанут работать.`} + confirmLabel="Удалить" destructive onConfirm={() => onReject(c.id)} /> diff --git a/internal/httpapi/routes_firewall_test.go b/internal/httpapi/routes_firewall_test.go index b68d6b2..f2ec0f3 100644 --- a/internal/httpapi/routes_firewall_test.go +++ b/internal/httpapi/routes_firewall_test.go @@ -2,6 +2,7 @@ package httpapi import ( "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -205,7 +206,7 @@ func TestFirewallInstallContext(t *testing.T) { } } -func TestFirewallRevokePendingClient(t *testing.T) { +func TestFirewallDeletePendingClient(t *testing.T) { srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed}) if err != nil { t.Fatal(err) @@ -241,24 +242,24 @@ func TestFirewallRevokePendingClient(t *testing.T) { t.Fatal("missing client_id") } - reqRevoke, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/revoke", nil) - reqRevoke.Header.Set("Authorization", "Bearer opkey") - respRevoke, err := client.Do(reqRevoke) + reqDelete, _ := http.NewRequest(http.MethodDelete, ts.URL+"/v1/firewall/clients/"+clientID, nil) + reqDelete.Header.Set("Authorization", "Bearer opkey") + respDelete, err := client.Do(reqDelete) if err != nil { t.Fatal(err) } - defer func() { _ = respRevoke.Body.Close() }() - if respRevoke.StatusCode != http.StatusOK { - b, _ := io.ReadAll(respRevoke.Body) - t.Fatalf("revoke status=%d body=%s", respRevoke.StatusCode, b) + defer func() { _ = respDelete.Body.Close() }() + if respDelete.StatusCode != http.StatusNoContent { + b, _ := io.ReadAll(respDelete.Body) + t.Fatalf("delete status=%d body=%s", respDelete.StatusCode, b) } - got, err := srv.Store().GetFirewallClient(tenant, clientID) - if err != nil { - t.Fatal(err) + _, err = srv.Store().GetFirewallClient(tenant, clientID) + if err == nil { + t.Fatal("client should be deleted") } - if got.Status != "revoked" { - t.Fatalf("status=%q want revoked", got.Status) + if !errors.Is(err, store.ErrNotFound) { + t.Fatalf("delete err=%v", err) } reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil) @@ -268,8 +269,8 @@ func TestFirewallRevokePendingClient(t *testing.T) { t.Fatal(err) } defer func() { _ = respBlock.Body.Close() }() - if respBlock.StatusCode != http.StatusForbidden { - t.Fatalf("revoked blocklist want 403 got %d", respBlock.StatusCode) + if respBlock.StatusCode != http.StatusUnauthorized { + t.Fatalf("deleted blocklist want 401 got %d", respBlock.StatusCode) } }