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
- 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.
150 lines
5.6 KiB
TypeScript
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 }
|