feat(api, web): enhance agent installation process with invited status and policy support
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m48s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Updated the agent enrollment process to include an 'invited' status, allowing for better tracking of agent states.
- Implemented support for install links that can now include an `install_link_id`, facilitating the transition from invited to pending status upon enrollment.
- Enhanced the MikroTik installation script to include the `EvofwInstallLinkId` for better tracking and management.
- Added new API endpoints for fetching agent policies and serving MikroTik-specific installation scripts.
- Improved the web UI to reflect the new agent statuses and provide copyable installation commands for agents.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 01:45:38 +07:00
co-authored by Cursor
parent ef56da4d91
commit d5784b9f35
20 changed files with 793 additions and 104 deletions
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Copy } from 'lucide-react'
import { apiFetch } from '@/lib/api'
@@ -23,6 +23,7 @@ import {
SheetHeader,
SheetTitle,
} from '@evofw/ui/components/sheet'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
/**
* Create agent install invite — Sheet.
@@ -44,12 +45,9 @@ interface AddAgentSheetProps {
onOpenChange: (open: boolean) => void
}
async function copyText(text: string) {
await navigator.clipboard.writeText(text)
toast.success('Скопировано')
}
export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
const qc = useQueryClient()
const { copyToClipboard } = useCopyToClipboard()
const [name, setName] = useState('web-01')
const [platform, setPlatform] = useState<Platform>('linux')
const [created, setCreated] = useState<InstallLink | null>(null)
@@ -70,13 +68,20 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
}),
onSuccess: (link) => {
setCreated(link)
toast.success('Ссылка создана')
toast.success('Агент создан')
void qc.invalidateQueries({ queryKey: ['agents'] })
},
onError: (e: Error) => toast.error(e.message),
})
const canCreate = Boolean(name.trim()) && !create.isPending
function handleCopy(text: string) {
if (!text) return
copyToClipboard(text)
toast.success('Скопировано')
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-md">
@@ -86,8 +91,8 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
</SheetTitle>
<SheetDescription>
{created
? 'Скопируйте one-liner и выполните на хосте. Затем одобрите агента в списке.'
: 'Создайте короткую install-ссылку с именем клиента.'}
? 'Агент уже в списке (Invited). Скопируйте one-liner и выполните на хосте.'
: 'Создайте агента и короткую install-ссылку.'}
</SheetDescription>
</SheetHeader>
@@ -139,9 +144,7 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
variant="outline"
size="sm"
className="self-start"
onClick={() =>
void copyText(created.curl?.by_id ?? '')
}
onClick={() => handleCopy(created.curl?.by_id ?? '')}
>
<Copy data-icon="inline-start" />
Копировать
@@ -159,9 +162,7 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
variant="outline"
size="sm"
className="self-start"
onClick={() =>
void copyText(created.curl?.by_slug ?? '')
}
onClick={() => handleCopy(created.curl?.by_slug ?? '')}
>
<Copy data-icon="inline-start" />
Копировать
@@ -182,7 +183,7 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
setCreated(null)
}}
>
Ещё ссылка
Ещё агент
</Button>
<Button onClick={() => onOpenChange(false)}>Готово</Button>
</>
@@ -191,11 +192,8 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
<Button variant="outline" onClick={() => onOpenChange(false)}>
Отмена
</Button>
<Button
disabled={!canCreate}
onClick={() => create.mutate()}
>
Создать ссылку
<Button disabled={!canCreate} onClick={() => create.mutate()}>
Создать
</Button>
</>
)}
+2
View File
@@ -16,6 +16,7 @@ const STATUS_VARIANT: Record<string, BadgeVariant> = {
allow: 'success-light',
pending_push: 'secondary',
pending: 'warning-light',
invited: 'info-light',
warning: 'warning-light',
degraded: 'warning-light',
conflict: 'destructive-light',
@@ -54,6 +55,7 @@ const STATUS_LABELS: Record<string, string> = {
synced: 'Синхронизировано',
pending_push: 'Ожидает отправки',
pending: 'Pending',
invited: 'Invited',
approved: 'Approved',
revoked: 'Revoked',
enabled: 'Включён',