fix(ui): подтверждать инициализацию IPsec и сохранять форму
This commit is contained in:
1 parent
4a8d696e4e
commit
f9413d5100
7 files changed
+144
-27
No files matched your search
@@ -370,10 +370,6 @@ export default function IpsecPage() {
|
|||||||
toast.success("IKEv2-сервер инициализирован")
|
toast.success("IKEv2-сервер инициализирован")
|
||||||
setInitOpen(false)
|
setInitOpen(false)
|
||||||
await loadLive()
|
await loadLive()
|
||||||
} catch (e) {
|
|
||||||
toast.error("Ошибка инициализации", {
|
|
||||||
description: e instanceof Error ? e.message : String(e),
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
|
import { FormField, SectionTitle } from "@/components/form-kit"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Sheet, SheetContent, SheetHeader, SheetTitle,
|
Sheet, SheetContent, SheetHeader, SheetTitle,
|
||||||
@@ -45,31 +46,58 @@ function IpsecInitSheet({
|
|||||||
onSubmit: (form: IpsecInitFormState) => void | Promise<void>
|
onSubmit: (form: IpsecInitFormState) => void | Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const [form, setForm] = useState<IpsecInitFormState>(defaultIpsecInitForm)
|
const [form, setForm] = useState<IpsecInitFormState>(defaultIpsecInitForm)
|
||||||
|
const submittingRef = useRef(false)
|
||||||
|
const openedRef = useRef(false)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const pending = busy || submitting
|
||||||
const set = <K extends keyof IpsecInitFormState>(k: K, v: IpsecInitFormState[K]) =>
|
const set = <K extends keyof IpsecInitFormState>(k: K, v: IpsecInitFormState[K]) =>
|
||||||
setForm((f) => ({ ...f, [k]: v }))
|
setForm((f) => ({ ...f, [k]: v }))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) { openedRef.current = false; return }
|
||||||
|
if (openedRef.current) return
|
||||||
|
openedRef.current = true
|
||||||
const server = servers.find((s) => s.id === (defaultServerId ?? ""))
|
const server = servers.find((s) => s.id === (defaultServerId ?? ""))
|
||||||
queueMicrotask(() => setForm({
|
queueMicrotask(() => {
|
||||||
...defaultIpsecInitForm(),
|
setError(null)
|
||||||
serverId: defaultServerId ?? "",
|
setForm({
|
||||||
serverEndpoint: server?.host ?? "",
|
...defaultIpsecInitForm(),
|
||||||
}))
|
serverId: defaultServerId ?? "",
|
||||||
|
serverEndpoint: server?.host ?? "",
|
||||||
|
})
|
||||||
|
})
|
||||||
}, [open, defaultServerId, servers])
|
}, [open, defaultServerId, servers])
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
form.serverId &&
|
form.serverId &&
|
||||||
form.serverEndpoint.trim() &&
|
form.serverEndpoint.trim() &&
|
||||||
/^(\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}|[\w.-]+\.[a-z]{2,})$/i.test(form.serverEndpoint.trim()) &&
|
(/^(?:\d{1,3}\.){3}\d{1,3}$/.test(form.serverEndpoint.trim())
|
||||||
|
? form.serverEndpoint.trim().split(".").every(part => Number(part) <= 255)
|
||||||
|
: /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i.test(form.serverEndpoint.trim())) &&
|
||||||
/^\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}$/.test(form.poolCidr.trim()),
|
/^\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}$/.test(form.poolCidr.trim()),
|
||||||
)
|
)
|
||||||
}, [form])
|
}, [form])
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!canSubmit || busy || submittingRef.current) return
|
||||||
|
submittingRef.current = true
|
||||||
|
setSubmitting(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await onSubmit(form)
|
||||||
|
} catch (error) {
|
||||||
|
setError(error instanceof Error ? error.message : "Не удалось инициализировать сервер")
|
||||||
|
} finally {
|
||||||
|
submittingRef.current = false
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={value => { if (!submittingRef.current && !busy) onOpenChange(value) }}>
|
||||||
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col gap-0 p-0">
|
<SheetContent showCloseButton={!pending} 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">
|
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
||||||
<SheetTitle>Инициализация IKEv2-сервера</SheetTitle>
|
<SheetTitle>Инициализация IKEv2-сервера</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
@@ -87,10 +115,11 @@ function IpsecInitSheet({
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<fieldset disabled={pending} className="flex flex-col gap-4">
|
||||||
<SectionTitle>Основные</SectionTitle>
|
<SectionTitle>Основные</SectionTitle>
|
||||||
<FormField label="Сервер" required>
|
<FormField label="Сервер" required>
|
||||||
<select
|
<select
|
||||||
|
aria-label="Сервер"
|
||||||
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]"
|
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.serverId}
|
value={form.serverId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -111,6 +140,7 @@ function IpsecInitSheet({
|
|||||||
<Input
|
<Input
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
placeholder="vpn.example.com"
|
placeholder="vpn.example.com"
|
||||||
|
aria-label="Адрес сервера (CN/SAN)"
|
||||||
value={form.serverEndpoint}
|
value={form.serverEndpoint}
|
||||||
onChange={(e) => set("serverEndpoint", e.target.value)}
|
onChange={(e) => set("serverEndpoint", e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -119,6 +149,7 @@ function IpsecInitSheet({
|
|||||||
<Input
|
<Input
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
placeholder="10.77.0.0/24"
|
placeholder="10.77.0.0/24"
|
||||||
|
aria-label="Подсеть клиентов"
|
||||||
value={form.poolCidr}
|
value={form.poolCidr}
|
||||||
onChange={(e) => set("poolCidr", e.target.value)}
|
onChange={(e) => set("poolCidr", e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -126,6 +157,7 @@ function IpsecInitSheet({
|
|||||||
<FormField label="DNS для клиентов" hint="Например 10.77.0.1 или 1.1.1.1">
|
<FormField label="DNS для клиентов" hint="Например 10.77.0.1 или 1.1.1.1">
|
||||||
<Input
|
<Input
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
|
aria-label="DNS для клиентов"
|
||||||
value={form.dns}
|
value={form.dns}
|
||||||
onChange={(e) => set("dns", e.target.value)}
|
onChange={(e) => set("dns", e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -137,21 +169,23 @@ function IpsecInitSheet({
|
|||||||
Managed srcnat masquerade для подсети клиентов (out-interface-list WAN)
|
Managed srcnat masquerade для подсети клиентов (out-interface-list WAN)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FormToggle checked={form.createNatRule} onChange={(v) => set("createNatRule", v)} />
|
<Switch aria-label="Интернет клиентам (NAT)" disabled={pending} checked={form.createNatRule} onCheckedChange={(v) => set("createNatRule", v)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
|
{pending && <p role="status" className="text-sm text-muted-foreground">Ожидание результата инициализации…</p>}
|
||||||
|
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
|
<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 render={<Button variant="outline" className="flex-1" disabled={pending} />}>
|
||||||
Отмена
|
Отмена
|
||||||
</SheetClose>
|
</SheetClose>
|
||||||
<Button
|
<Button
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!canSubmit || busy}
|
disabled={!canSubmit || pending}
|
||||||
onClick={() => void onSubmit(form)}
|
onClick={() => void submit()}
|
||||||
>
|
>
|
||||||
{busy ? "Инициализация…" : "Инициализировать"}
|
{pending ? "Инициализация…" : "Инициализировать"}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
+2
-2
@@ -80,7 +80,7 @@ flowchart LR
|
|||||||
|
|
||||||
## Текущая точка продолжения
|
## Текущая точка продолжения
|
||||||
|
|
||||||
По журналу этапы 0–8 завершены в границах локальной приёмки; внешние ограничения сохраняются. Этап 9 начат (блоки 9.1–9.13 — история всех шести разделов и восстановление); этапы 10–12 не начаты. Этап 11 разделён: 11А — пользователи, контейнеры, сертификаты, резервирование; 11Б — оповещения, сбор данных, интеграции, релизы, настройки.
|
По журналу этапы 0–8 завершены в границах локальной приёмки; внешние ограничения сохраняются. Этап 9 начат (блоки 9.1–9.14 — история, восстановление и инициализация IPsec); этапы 10–12 не начаты. Этап 11 разделён: 11А — пользователи, контейнеры, сертификаты, резервирование; 11Б — оповещения, сбор данных, интеграции, релизы, настройки.
|
||||||
|
|
||||||
Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8 и итоговая матрица](ux-ui-stage-8.md), [этап 9](ux-ui-stage-9.md).
|
Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8 и итоговая матрица](ux-ui-stage-8.md), [этап 9](ux-ui-stage-9.md).
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ flowchart LR
|
|||||||
|
|
||||||
Ближайший оставшийся объём:
|
Ближайший оставшийся объём:
|
||||||
|
|
||||||
1. Этап 9: карта разделов и схема сохранены; удаление, импорт, создание, переключение, контекст экспорта и выбор пира WireGuard исправлены в 9.1–9.7; в 9.8 проверено подтверждение восстановления. В 9.9 исправлены разметка и сохранение подтверждения при ошибке у всех шести потребителей. В 9.10 исправлены ошибки и поздние ответы загрузки истории WireGuard. В 9.11 подтверждение восстановления проверено у остальных пяти потребителей. В 9.12 обработаны ошибка/повтор/поздние ответы истории IPsec. В 9.13 подключены ошибка/повтор/защита поздних ответов остальных четырёх разделов и общая валидация ревизий всех шести. Далее — инициализация IPsec: подтверждение API, сохранение формы и защита повторной отправки; затем пользователи/сертификаты и GRE/VXLAN/Firewall/Filters.
|
1. Этап 9: карта разделов и схема сохранены; удаление, импорт, создание, переключение, контекст экспорта и выбор пира WireGuard исправлены в 9.1–9.7; в 9.8 проверено подтверждение восстановления. В 9.9 исправлены разметка и сохранение подтверждения при ошибке у всех шести потребителей. В 9.10 исправлены ошибки и поздние ответы загрузки истории WireGuard. В 9.11 подтверждение восстановления проверено у остальных пяти потребителей. В 9.12 обработаны ошибка/повтор/поздние ответы истории IPsec. В 9.13 подключены ошибка/повтор/защита поздних ответов остальных четырёх разделов и общая валидация ревизий всех шести. В 9.14 исправлены подтверждение инициализации IPsec, сохранение формы и защита повторной отправки. Далее — создание/редактирование пользователей IPsec, затем сертификаты и GRE/VXLAN/Firewall/Filters.
|
||||||
2. Сохранять контекст устройства, введённые значения при ошибке и подтверждать результат операций по API; локальные моки проверяют клиентские сценарии, реальные сетевые операции требуют тестового окружения.
|
2. Сохранять контекст устройства, введённые значения при ошибке и подтверждать результат операций по API; локальные моки проверяют клиентские сценарии, реальные сетевые операции требуют тестового окружения.
|
||||||
3. В этапе 12 выполнить полный аудит доступности (скринридер, контраст), производительности и сквозную регрессию. Ограничения этапа 8 — удалённые серверы месячного каталога, отсутствие экспорта и автоматического переподключения SSE — остаются явно зафиксированными, без заявления о реализации.
|
3. В этапе 12 выполнить полный аудит доступности (скринридер, контраст), производительности и сквозную регрессию. Ограничения этапа 8 — удалённые серверы месячного каталога, отсутствие экспорта и автоматического переподключения SSE — остаются явно зафиксированными, без заявления о реализации.
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -25,12 +25,21 @@
|
|||||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.13 — история всех шести разделов и восстановление |
|
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.14 — история, восстановление и инициализация IPsec |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 9.14 — инициализация IPsec (2026-09-29)
|
||||||
|
|
||||||
|
- Успех требует валидной сводки по contracts, совпадения serverId и initialized=true. Пустой ответ, неинициализированный сервер и чужое устройство не закрывают форму и не показывают успех. Backend возвращает сводку после применения; правила сетевой операции не изменялись.
|
||||||
|
- Форма сохраняет ввод и показывает ошибку внутри панели. Синхронный замок предотвращает повтор; до ответа заблокированы поля, NAT, отмена, Escape и внешний клик. Ожидание объявляется через status. Инициализация полей выполняется один раз на открытие, чтобы обновление списка серверов не стирало ввод.
|
||||||
|
- Исправлен ввод IPv4 без CIDR вместо ошибочного требования маски; домены сохранены, IPv4 с октетом больше 255 отклоняется. Поля и переключатель получили доступные имена. Полная валидация подсети остаётся на backend.
|
||||||
|
- Прошли новый E2E и 3 регрессионных теста таблиц IPsec/GRE, lint всех изменённых файлов и TypeScript; diff-check чистый. Первоначальный тест исправлен: перед кнопкой инициализации требуется вкладка «Сервер». Build не повторялся; последний успешный — блок 9.13.
|
||||||
|
- Следующий шаг: создание/редактирование пользователя IPsec — проверка контракта результата, сохранение формы, защита повторов; затем сертификаты и остальные разделы карты. Реальные RouterOS/JWT и транзакционность составной инициализации не проверены. Этап 9 открыт.
|
||||||
|
- Использовано 41% пятичасовой и 84% недельной квоты. Локальный коммит без push; изменения пользователя в skill/config/AGENTS сохранены.
|
||||||
|
|
||||||
## Блок 9.13 — общая проверка и загрузка истории (2026-09-29)
|
## Блок 9.13 — общая проверка и загрузка истории (2026-09-29)
|
||||||
|
|
||||||
- GRE, Firewall, Filters и рекурсивные маршруты подключены к ошибке/повтору общей панели. Ошибка не обнуляет сохранённые ревизии и не маскируется пустым списком. Поколение запроса защищает результат, ошибку и finally после закрытия, смены контекста и размонтирования; физическая отмена запросов не добавлялась.
|
- GRE, Firewall, Filters и рекурсивные маршруты подключены к ошибке/повтору общей панели. Ошибка не обнуляет сохранённые ревизии и не маскируется пустым списком. Поколение запроса защищает результат, ошибку и finally после закрытия, смены контекста и размонтирования; физическая отмена запросов не добавлялась.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Этап 9: VPN и сетевые операции
|
# Этап 9: VPN и сетевые операции
|
||||||
|
|
||||||
Статус: начат 2026-09-28, выполнены блоки 9.1–9.13. Основа — существующие ReUI PRO/base-nova, Frame, ServerTileRail, DataGrid и формы. Правила применения сетевой конфигурации не меняем.
|
Статус: начат 2026-09-28, выполнены блоки 9.1–9.14. Основа — существующие ReUI PRO/base-nova, Frame, ServerTileRail, DataGrid и формы. Правила применения сетевой конфигурации не меняем.
|
||||||
|
|
||||||
## Схема и референсы принятого плана
|
## Схема и референсы принятого плана
|
||||||
|
|
||||||
@@ -93,3 +93,7 @@ IPsec/GRE/Firewall/Filters/рекурсивные маршруты требую
|
|||||||
## Блок 9.13 — история всех разделов (2026-09-29)
|
## Блок 9.13 — история всех разделов (2026-09-29)
|
||||||
|
|
||||||
Ошибка/повтор и защита поздних ответов подключены к GRE/Firewall/Filters/рекурсивным маршрутам. Общая схема проверяет записи, сервер и раздел во всех шести потребителях, включая IPsec. Все 36 E2E истории/WireGuard и TypeScript прошли; исходный lint страниц не изменился, новые общие файлы и тест чистые. Результат build — в журнале. Далее — форма инициализации IPsec (handleInit): подтверждение, сохранение ввода и повторные нажатия. Реальные устройства и payload восстановления rules/routes остаются вне проверки этого блока.
|
Ошибка/повтор и защита поздних ответов подключены к GRE/Firewall/Filters/рекурсивным маршрутам. Общая схема проверяет записи, сервер и раздел во всех шести потребителях, включая IPsec. Все 36 E2E истории/WireGuard и TypeScript прошли; исходный lint страниц не изменился, новые общие файлы и тест чистые. Результат build — в журнале. Далее — форма инициализации IPsec (handleInit): подтверждение, сохранение ввода и повторные нажатия. Реальные устройства и payload восстановления rules/routes остаются вне проверки этого блока.
|
||||||
|
|
||||||
|
## Блок 9.14 — инициализация IPsec (2026-09-29)
|
||||||
|
|
||||||
|
Проверяются схема сводки, serverId и initialized=true. Ошибка остаётся в форме с введёнными значениями; замок блокирует повтор, редактирование и закрытие во время запроса. Исправлен ввод IPv4, добавлены доступные имена. Новый E2E и 3 теста таблиц прошли, lint/TypeScript чистые. Далее — создание/редактирование пользователей IPsec. Реальные устройства и транзакционность сетевой операции не проверены; последний build — 9.13.
|
||||||
+7
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { ipsecServerSummaryDtoSchema } from "@mmapp/contracts/ipsec"
|
||||||
import { requestJson } from "@/shared/api/http-client"
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import type {
|
import type {
|
||||||
IpsecCertBundle,
|
IpsecCertBundle,
|
||||||
@@ -24,10 +25,15 @@ export async function initIpsecServer(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
body: IpsecInitRequest,
|
body: IpsecInitRequest,
|
||||||
): Promise<IpsecServerSummaryDto> {
|
): Promise<IpsecServerSummaryDto> {
|
||||||
return requestJson<IpsecServerSummaryDto>(baseUrl, "/api/ipsec/server/init", {
|
const response = await requestJson<unknown>(baseUrl, "/api/ipsec/server/init", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
|
const result = ipsecServerSummaryDtoSchema.safeParse(response)
|
||||||
|
if (!result.success || result.data.serverId !== body.serverId || !result.data.initialized) {
|
||||||
|
throw new Error("Сервер не подтвердил инициализацию IKEv2")
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteIpsecServer(
|
export async function deleteIpsecServer(
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { test, expect } from "@playwright/test"
|
||||||
|
|
||||||
|
test("IPsec: инициализация сохраняет ввод, блокирует повтор и требует подтверждения", async ({ page }) => {
|
||||||
|
const summary = { serverId: "1", serverName: "IPsec роутер", initialized: false, ike2Ready: false, natRuleManaged: false, clientsTotal: 0, clientsOnline: 0, peers: [] }
|
||||||
|
let attempts = 0
|
||||||
|
let release: (() => void) | undefined
|
||||||
|
const gate = new Promise<void>(resolve => { release = resolve })
|
||||||
|
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||||
|
await page.route("**/api/**", async route => {
|
||||||
|
const path = new URL(route.request().url()).pathname
|
||||||
|
if (path === "/api/ipsec/server/init") {
|
||||||
|
attempts++
|
||||||
|
expect(route.request().postDataJSON()).toMatchObject({ serverId: "1", serverEndpoint: "192.0.2.1", poolCidr: "10.78.0.0/24", dns: "1.1.1.1", createNatRule: false })
|
||||||
|
if (attempts === 1) {
|
||||||
|
await gate
|
||||||
|
return route.fulfill({ status: 403, json: { error: "Инициализация запрещена тест" } })
|
||||||
|
}
|
||||||
|
if (attempts === 2) return route.fulfill({ json: {} })
|
||||||
|
if (attempts === 3) return route.fulfill({ json: summary })
|
||||||
|
if (attempts === 4) return route.fulfill({ json: { ...summary, initialized: true, serverId: "2" } })
|
||||||
|
return route.fulfill({ status: 201, json: { ...summary, initialized: true, ike2Ready: true } })
|
||||||
|
}
|
||||||
|
return route.fulfill({ json: path === "/api/auth/config" ? { required: false }
|
||||||
|
: path === "/api/ipsec" ? { servers: [summary], clients: [] }
|
||||||
|
: path === "/api/servers" ? [{ id: 1, name: "IPsec роутер", host: "192.0.2.1", enabled: true, type: "exit-node", country: "RU", status: "online" }] : {} })
|
||||||
|
})
|
||||||
|
await page.goto("/ipsec")
|
||||||
|
await page.getByRole("tab", { name: "Сервер", exact: true }).click()
|
||||||
|
await page.getByRole("button", { name: "Инициализировать", exact: true }).click()
|
||||||
|
const sheet = page.getByRole("dialog", { name: "Инициализация IKEv2-сервера" })
|
||||||
|
const submit = sheet.getByRole("button", { name: "Инициализировать", exact: true })
|
||||||
|
await expect(sheet.getByLabel("Адрес сервера (CN/SAN)")).toHaveValue("192.0.2.1")
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await sheet.getByLabel("Адрес сервера (CN/SAN)").fill("999.0.0.1")
|
||||||
|
await expect(submit).toBeDisabled()
|
||||||
|
await sheet.getByLabel("Адрес сервера (CN/SAN)").fill("vpn.example.com")
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await sheet.getByLabel("Адрес сервера (CN/SAN)").fill("192.0.2.1")
|
||||||
|
await sheet.getByLabel("Подсеть клиентов").fill("10.78.0.0/24")
|
||||||
|
await sheet.getByLabel("DNS для клиентов").fill("1.1.1.1")
|
||||||
|
await sheet.getByRole("switch", { name: "Интернет клиентам (NAT)" }).click()
|
||||||
|
await submit.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
|
||||||
|
await expect.poll(() => attempts).toBe(1)
|
||||||
|
await expect(sheet.getByRole("status")).toContainText("Ожидание результата")
|
||||||
|
await expect(sheet.getByLabel("Подсеть клиентов")).toBeDisabled()
|
||||||
|
await expect(sheet.getByLabel("Сервер", { exact: true })).toBeDisabled()
|
||||||
|
await expect(sheet.getByRole("switch")).toBeDisabled()
|
||||||
|
await expect(sheet.getByRole("button", { name: "Отмена" })).toBeDisabled()
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await page.mouse.click(10, 10)
|
||||||
|
await expect(sheet).toBeVisible()
|
||||||
|
release!()
|
||||||
|
await expect(sheet.getByRole("alert").filter({ hasText: "Инициализация запрещена тест" })).toBeVisible()
|
||||||
|
await expect(sheet.getByLabel("Подсеть клиентов")).toHaveValue("10.78.0.0/24")
|
||||||
|
await expect(sheet.getByLabel("DNS для клиентов")).toHaveValue("1.1.1.1")
|
||||||
|
for (let attempt = 2; attempt <= 4; attempt++) {
|
||||||
|
await submit.click()
|
||||||
|
await expect.poll(() => attempts).toBe(attempt)
|
||||||
|
await expect(sheet.getByRole("alert").filter({ hasText: "Сервер не подтвердил инициализацию IKEv2" })).toBeVisible()
|
||||||
|
await expect(page.getByText("IKEv2-сервер инициализирован", { exact: true })).toHaveCount(0)
|
||||||
|
}
|
||||||
|
await submit.click()
|
||||||
|
await expect(sheet).toHaveCount(0)
|
||||||
|
expect(attempts).toBe(5)
|
||||||
|
await page.getByRole("button", { name: "Инициализировать", exact: true }).click()
|
||||||
|
await expect(sheet.getByLabel("Подсеть клиентов")).toHaveValue("10.77.0.0/24")
|
||||||
|
await expect(sheet.getByText("Сервер не подтвердил инициализацию IKEv2", { exact: true })).toHaveCount(0)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user