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:
@@ -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 : 'Не удалось удалить'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user