API, UI, Linux/MikroTik agents, IP lists, политики, stats, CI и интеграция с auth-portal/EvoBGP. Co-authored-by: Cursor <[email protected]>
183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
import { useState } from 'react'
|
|
import { PageHeader, PageShell } from '@/components/reui-kit'
|
|
import { Frame, FrameHeader, FrameTitle } from '@/components/reui/frame'
|
|
import { rulesQueryOptions, listsQueryOptions, agentsQueryOptions } from '@/queries'
|
|
import { apiFetch } from '@/lib/api'
|
|
import { Button } from '@evofw/ui/components/button'
|
|
import { Input } from '@evofw/ui/components/input'
|
|
import { Label } from '@evofw/ui/components/label'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@evofw/ui/components/select'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@evofw/ui/components/table'
|
|
|
|
export const Route = createFileRoute('/_auth/rules')({
|
|
component: RulesPage,
|
|
})
|
|
|
|
function RulesPage() {
|
|
const qc = useQueryClient()
|
|
const rulesQ = useQuery(rulesQueryOptions())
|
|
const listsQ = useQuery(listsQueryOptions())
|
|
const agentsQ = useQuery(agentsQueryOptions())
|
|
const [priority, setPriority] = useState('100')
|
|
const [action, setAction] = useState<'allow' | 'deny'>('deny')
|
|
const [listId, setListId] = useState('')
|
|
const [agentId, setAgentId] = useState('tenant')
|
|
|
|
const create = useMutation({
|
|
mutationFn: () =>
|
|
apiFetch('/api/v1/rules', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
priority: Number(priority),
|
|
action,
|
|
list_id: listId || null,
|
|
agent_id: agentId === 'tenant' ? null : agentId,
|
|
}),
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('Правило создано')
|
|
void qc.invalidateQueries({ queryKey: ['rules'] })
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
})
|
|
|
|
const remove = useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiFetch(`/api/v1/rules/${id}`, { method: 'DELETE' }),
|
|
onSuccess: () => void qc.invalidateQueries({ queryKey: ['rules'] }),
|
|
})
|
|
|
|
return (
|
|
<PageShell>
|
|
<PageHeader
|
|
title="Правила"
|
|
description="Упорядоченные allow/deny по списку или CIDR (tenant + per-agent)"
|
|
/>
|
|
|
|
<Frame>
|
|
<FrameHeader>
|
|
<FrameTitle>Новое правило</FrameTitle>
|
|
</FrameHeader>
|
|
<div className="grid max-w-xl gap-3 sm:grid-cols-2">
|
|
<div className="flex flex-col gap-2">
|
|
<Label>Priority</Label>
|
|
<Input
|
|
value={priority}
|
|
onChange={(e) => setPriority(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label>Action</Label>
|
|
<Select
|
|
value={action}
|
|
onValueChange={(v) => setAction(v as 'allow' | 'deny')}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="deny">deny</SelectItem>
|
|
<SelectItem value="allow">allow</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label>Список</Label>
|
|
<Select value={listId} onValueChange={setListId}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="IP list" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(listsQ.data?.items ?? []).map((l) => (
|
|
<SelectItem key={l.id} value={l.id}>
|
|
{l.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<Label>Scope</Label>
|
|
<Select value={agentId} onValueChange={setAgentId}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="tenant">Tenant default</SelectItem>
|
|
{(agentsQ.data?.items ?? []).map((a) => (
|
|
<SelectItem key={a.id} value={a.id}>
|
|
{a.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button
|
|
className="sm:col-span-2"
|
|
disabled={!listId || create.isPending}
|
|
onClick={() => create.mutate()}
|
|
>
|
|
Создать
|
|
</Button>
|
|
</div>
|
|
</Frame>
|
|
|
|
<Frame>
|
|
<FrameHeader>
|
|
<FrameTitle>Все правила</FrameTitle>
|
|
</FrameHeader>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Prio</TableHead>
|
|
<TableHead>Action</TableHead>
|
|
<TableHead>Agent</TableHead>
|
|
<TableHead>List / CIDR</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(rulesQ.data?.items ?? []).map((r) => (
|
|
<TableRow key={r.id}>
|
|
<TableCell>{r.priority}</TableCell>
|
|
<TableCell>{r.action}</TableCell>
|
|
<TableCell className="text-xs">
|
|
{r.agent_id ?? 'tenant'}
|
|
</TableCell>
|
|
<TableCell className="font-mono text-xs">
|
|
{r.cidr ?? r.list_id ?? '—'}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => remove.mutate(r.id)}
|
|
>
|
|
Удалить
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Frame>
|
|
</PageShell>
|
|
)
|
|
}
|