Files
MikrotikManager/components/ipsec/ipsec-peer-sheet.tsx
T
Denozordec 7365d8d8fb
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-test (push) Successful in 2m39s
Docker images / frontend-image (push) Successful in 2m56s
Docker images / updater-image (push) Successful in 44s
Docker images / backend-image (push) Successful in 2m35s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 15s
feat(ipsec): enhance IPsec management with peer and certificate handling
- Added support for managing IPsec peers, including editing and deleting functionality.
- Introduced a new UI component for displaying peers and their details within the IPsec server grid.
- Updated the IPsec user creation form to allow binding identities to specific peers.
- Enhanced backend routes and services to handle peer patching and deletion requests.
- Improved data structures to accommodate multiple peers and certificates for each server.
- Added tests to ensure proper functionality of new peer management features.
2026-09-12 21:04:49 +07:00

150 lines
5.6 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import type { IpsecPeerDto } from "@mmapp/contracts/ipsec"
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Sheet, SheetContent, SheetHeader, SheetTitle,
SheetDescription, SheetFooter, SheetClose,
} from "@/components/ui/sheet"
export type IpsecPeerFormState = {
name: string
address: string
exchangeMode: string
passive: boolean
certificate: string
profile: string
disabled: boolean
}
export const defaultIpsecPeerForm = (): IpsecPeerFormState => ({
name: "",
address: "0.0.0.0/0",
exchangeMode: "ike2",
passive: true,
certificate: "",
profile: "",
disabled: false,
})
const EXCHANGE_MODES = ["ike2", "main", "aggressive", "base"]
/** Редактирование существующего peer (сервера) RouterOS. */
function IpsecPeerSheet({
open,
onOpenChange,
busy,
editing,
certificates,
onSubmit,
}: {
open: boolean
onOpenChange: (v: boolean) => void
busy?: boolean
editing: IpsecPeerDto | null
certificates: string[]
onSubmit: (form: IpsecPeerFormState) => void | Promise<void>
}) {
const [form, setForm] = useState<IpsecPeerFormState>(defaultIpsecPeerForm)
const set = <K extends keyof IpsecPeerFormState>(k: K, v: IpsecPeerFormState[K]) =>
setForm((f) => ({ ...f, [k]: v }))
useEffect(() => {
if (!open || !editing) return
queueMicrotask(() => setForm({
name: editing.name,
address: editing.address ?? "",
exchangeMode: editing.exchangeMode ?? "ike2",
passive: editing.passive ?? false,
certificate: editing.certificate ?? "",
profile: editing.profile ?? "",
disabled: editing.disabled,
}))
}, [open, editing])
const canSubmit = useMemo(() => form.name.trim().length > 0, [form.name])
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col gap-0 p-0">
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<SheetTitle>{editing ? `Peer «${editing.name}»` : "Peer"}</SheetTitle>
<SheetDescription>
{editing?.managed
? "Managed peer: изменения применяются напрямую на роутере"
: "Существующий peer RouterOS: изменения применяются напрямую"}
</SheetDescription>
</SheetHeader>
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-5">
<div className="flex flex-col gap-4">
<SectionTitle>Основные</SectionTitle>
<FormField label="Имя" required>
<Input value={form.name} onChange={(e) => set("name", e.target.value)} />
</FormField>
<FormField label="Адрес" hint="Обычно 0.0.0.0/0 для road-warrior">
<Input className="font-mono" value={form.address} onChange={(e) => set("address", e.target.value)} />
</FormField>
<FormField label="Exchange mode">
<select
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
value={form.exchangeMode}
onChange={(e) => set("exchangeMode", e.target.value)}
>
{EXCHANGE_MODES.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</FormField>
<FormField label="Сертификат" hint="Серверный сертификат роутера">
<Input
className="font-mono"
list="ipsec-peer-certs"
placeholder="ipsec-server"
value={form.certificate}
onChange={(e) => set("certificate", e.target.value)}
/>
<datalist id="ipsec-peer-certs">
{certificates.map((c) => <option key={c} value={c} />)}
</datalist>
</FormField>
<FormField label="Profile" hint="Пусто — не менять">
<Input className="font-mono" value={form.profile} onChange={(e) => set("profile", e.target.value)} />
</FormField>
</div>
<div className="flex flex-col gap-4">
<SectionTitle>Состояние</SectionTitle>
<div className="flex items-center justify-between">
<p className="text-sm font-medium">Passive</p>
<FormToggle checked={form.passive} onChange={(v) => set("passive", v)} />
</div>
<div className="flex items-center justify-between">
<p className="text-sm font-medium">Отключён</p>
<FormToggle checked={form.disabled} onChange={(v) => set("disabled", v)} />
</div>
</div>
</div>
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
<SheetClose render={<Button variant="outline" className="flex-1" disabled={busy} />}>
Отмена
</SheetClose>
<Button
className="flex-1"
disabled={!canSubmit || busy}
onClick={() => void onSubmit(form)}
>
{busy ? "Сохранение…" : "Сохранить"}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
export { IpsecPeerSheet }