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.
This commit is contained in:
Denozordec
2026-07-09 00:27:50 +07:00
parent e51999c908
commit 4a4c11c6bf
3 changed files with 40 additions and 34 deletions
+4 -4
View File
@@ -60,16 +60,16 @@ export function useApproveFirewallClient() {
}) })
} }
export function useRevokeFirewallClient() { export function useDeleteFirewallClient() {
const qc = useQueryClient() const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (id: string) => mutationFn: (id: string) =>
apiJSON<{ status: string }>(`/v1/firewall/clients/${id}/revoke`, { method: 'POST' }), apiJSON<void>(`/v1/firewall/clients/${id}`, { method: 'DELETE' }),
onSuccess: () => { onSuccess: () => {
toast.success('Клиент отключён') toast.success('Клиент удалён')
void qc.invalidateQueries({ queryKey: firewallKeys.clients() }) void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
}, },
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось отклонить'), onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить'),
}) })
} }
+20 -15
View File
@@ -31,8 +31,8 @@ import {
firewallRulesQueryOptions, firewallRulesQueryOptions,
useApproveFirewallClient, useApproveFirewallClient,
useCreateFirewallRule, useCreateFirewallRule,
useDeleteFirewallClient,
useDeleteFirewallRule, useDeleteFirewallRule,
useRevokeFirewallClient,
} from '@/queries/firewall' } from '@/queries/firewall'
import type { BgpCommunity, FirewallClient } from '@/types/api' import type { BgpCommunity, FirewallClient } from '@/types/api'
@@ -56,7 +56,7 @@ function FirewallPage() {
const clientsQ = useQuery(firewallClientsQueryOptions()) const clientsQ = useQuery(firewallClientsQueryOptions())
const rulesQ = useQuery(firewallRulesQueryOptions('tenant')) const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
const approve = useApproveFirewallClient() const approve = useApproveFirewallClient()
const revoke = useRevokeFirewallClient() const deleteClient = useDeleteFirewallClient()
const createRule = useCreateFirewallRule() const createRule = useCreateFirewallRule()
const deleteRule = useDeleteFirewallRule() const deleteRule = useDeleteFirewallRule()
@@ -82,8 +82,13 @@ function FirewallPage() {
const communities = communitiesQ.data?.items ?? [] const communities = communitiesQ.data?.items ?? []
const clients = clientsQ.data?.items ?? [] const { activeClients, pending } = useMemo(() => {
const pending = clients.filter((c) => c.status === 'pending') 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 rules = rulesQ.data?.items ?? []
const installCmd = useMemo(() => { const installCmd = useMemo(() => {
@@ -190,18 +195,18 @@ function FirewallPage() {
<Tabs defaultValue="clients"> <Tabs defaultValue="clients">
<TabsList> <TabsList>
<TabsTrigger value="clients">Клиенты ({clients.length})</TabsTrigger> <TabsTrigger value="clients">Клиенты ({activeClients.length})</TabsTrigger>
<TabsTrigger value="rules">Правила ({rules.length})</TabsTrigger> <TabsTrigger value="rules">Правила ({rules.length})</TabsTrigger>
<TabsTrigger value="requests">Запросы ({pending.length})</TabsTrigger> <TabsTrigger value="requests">Запросы ({pending.length})</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="clients" className="mt-4"> <TabsContent value="clients" className="mt-4">
<ClientsTable <ClientsTable
clients={clients} clients={activeClients}
onApprove={(id) => approve.mutate(id)} onApprove={(id) => approve.mutate(id)}
onReject={(id) => revoke.mutate(id)} onReject={(id) => deleteClient.mutate(id)}
approvePending={approve.isPending} approvePending={approve.isPending}
rejectPending={revoke.isPending} rejectPending={deleteClient.isPending}
/> />
</TabsContent> </TabsContent>
@@ -263,9 +268,9 @@ function FirewallPage() {
<ClientsTable <ClientsTable
clients={pending} clients={pending}
onApprove={(id) => approve.mutate(id)} onApprove={(id) => approve.mutate(id)}
onReject={(id) => revoke.mutate(id)} onReject={(id) => deleteClient.mutate(id)}
approvePending={approve.isPending} approvePending={approve.isPending}
rejectPending={revoke.isPending} rejectPending={deleteClient.isPending}
emptyTitle="Нет pending-запросов" emptyTitle="Нет pending-запросов"
/> />
</TabsContent> </TabsContent>
@@ -342,7 +347,7 @@ function ClientsTable({
</Button> </Button>
} }
title="Отклонить запрос?" title="Отклонить запрос?"
description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} — токен перестанет работать.`} description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} запись будет удалена, токен перестанет работать.`}
confirmLabel="Отклонить" confirmLabel="Отклонить"
destructive destructive
onConfirm={() => onReject(c.id)} onConfirm={() => onReject(c.id)}
@@ -358,12 +363,12 @@ function ClientsTable({
className="text-destructive" className="text-destructive"
disabled={rejectPending} disabled={rejectPending}
> >
Отозвать Удалить
</Button> </Button>
} }
title="Отозвать клиент?" title="Удалить клиент?"
description={`${c.name} — blocklist перестанет отдаваться, токен будет недействителен.`} description={`${c.name} запись будет удалена, blocklist и токен перестанут работать.`}
confirmLabel="Отозвать" confirmLabel="Удалить"
destructive destructive
onConfirm={() => onReject(c.id)} onConfirm={() => onReject(c.id)}
/> />
+16 -15
View File
@@ -2,6 +2,7 @@ package httpapi
import ( import (
"encoding/json" "encoding/json"
"errors"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "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}) srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -241,24 +242,24 @@ func TestFirewallRevokePendingClient(t *testing.T) {
t.Fatal("missing client_id") t.Fatal("missing client_id")
} }
reqRevoke, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/revoke", nil) reqDelete, _ := http.NewRequest(http.MethodDelete, ts.URL+"/v1/firewall/clients/"+clientID, nil)
reqRevoke.Header.Set("Authorization", "Bearer opkey") reqDelete.Header.Set("Authorization", "Bearer opkey")
respRevoke, err := client.Do(reqRevoke) respDelete, err := client.Do(reqDelete)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer func() { _ = respRevoke.Body.Close() }() defer func() { _ = respDelete.Body.Close() }()
if respRevoke.StatusCode != http.StatusOK { if respDelete.StatusCode != http.StatusNoContent {
b, _ := io.ReadAll(respRevoke.Body) b, _ := io.ReadAll(respDelete.Body)
t.Fatalf("revoke status=%d body=%s", respRevoke.StatusCode, b) t.Fatalf("delete status=%d body=%s", respDelete.StatusCode, b)
} }
got, err := srv.Store().GetFirewallClient(tenant, clientID) _, err = srv.Store().GetFirewallClient(tenant, clientID)
if err != nil { if err == nil {
t.Fatal(err) t.Fatal("client should be deleted")
} }
if got.Status != "revoked" { if !errors.Is(err, store.ErrNotFound) {
t.Fatalf("status=%q want revoked", got.Status) t.Fatalf("delete err=%v", err)
} }
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil) reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
@@ -268,8 +269,8 @@ func TestFirewallRevokePendingClient(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer func() { _ = respBlock.Body.Close() }() defer func() { _ = respBlock.Body.Close() }()
if respBlock.StatusCode != http.StatusForbidden { if respBlock.StatusCode != http.StatusUnauthorized {
t.Fatalf("revoked blocklist want 403 got %d", respBlock.StatusCode) t.Fatalf("deleted blocklist want 401 got %d", respBlock.StatusCode)
} }
} }