diff --git a/app/(main)/ipsec/page.tsx b/app/(main)/ipsec/page.tsx index f241af4c..87ca624f 100644 --- a/app/(main)/ipsec/page.tsx +++ b/app/(main)/ipsec/page.tsx @@ -300,6 +300,13 @@ function IpsecPage() { const failedServerIds = new Set(failures.map(f => f.serverId)) const scopeUnknown = isLive && (!hasLoaded || !!dataError || (effectiveServerId === ALL_SERVERS_ID ? failures.length > 0 : failedServerIds.has(effectiveServerId))) + const canManage = (serverId: string) => isLive && hasLoaded && !dataError && !inventoryError && + !loading && !busy && !historyRestoring && !failedServerIds.has(serverId) && liveServers.some(s => s.id === serverId) + const assertDeviceReady = (serverId: string) => { + if (!canManage(serverId)) throw new Error("Сначала подтвердите актуальное состояние устройства: повторите опрос IPsec") + } + const hasReadyDevice = displayServers.some(s => canManage(s.id)) + const scopedClients = useMemo(() => { if (effectiveServerId === ALL_SERVERS_ID) return displayClients return displayClients.filter((c) => c.serverId === effectiveServerId) @@ -370,6 +377,7 @@ function IpsecPage() { }, [displayServers, displayClients]) const handleInit = async (form: IpsecInitFormState) => { + assertDeviceReady(form.serverId) setBusy(true) try { await initIpsecServer(backendUrl, { @@ -391,6 +399,7 @@ function IpsecPage() { } const handleCreateUser = async (form: IpsecUserFormState) => { + assertDeviceReady(form.serverId) setBusy(true) try { const res = await createIpsecUser(backendUrl, { @@ -424,6 +433,7 @@ function IpsecPage() { const handleEditUser = async (form: IpsecUserFormState) => { if (!editing) return + assertDeviceReady(editing.serverId) setBusy(true) try { await patchIpsecUser(backendUrl, editing.serverId, editing.rosId, { @@ -490,6 +500,8 @@ function IpsecPage() { const confirmDelete = async (force = false) => { if (!pendingDelete || deleteLock.current || (force && !forceAllowed)) return const target = pendingDelete + const serverId = target.kind === "user" ? target.client.serverId : target.kind === "server" ? target.summary.serverId : target.serverId + if (!canManage(serverId)) { setDeleteError("Повторите опрос IPsec перед удалением"); return } deleteLock.current = true setBusy(true) setDeleteError(null) @@ -523,6 +535,7 @@ function IpsecPage() { const handleEditPeer = async (form: IpsecPeerFormState) => { if (!editingPeer) return + assertDeviceReady(editingPeer.serverId) setBusy(true) try { await patchIpsecPeer(backendUrl, editingPeer.serverId, editingPeer.peer.rosId, { @@ -586,7 +599,7 @@ function IpsecPage() { )} {isLive ? ( - @@ -713,7 +726,7 @@ function IpsecPage() { searchPlaceholder="Поиск по имени / CN / IP…" actions={ isLive ? ( - @@ -722,6 +735,7 @@ function IpsecPage() { /> {isLive && !hasLoaded ?

{loading ? "Загрузка клиентов IPsec…" : "Клиенты IPsec пока неизвестны"}

: {isLive && !hasLoaded ?

Состояние серверов IPsec пока неизвестно

: { setSelectedServerId(s.serverId); setInitOpen(true) } : undefined} onRemove={isLive ? (s) => setPendingDelete({ kind: "server", summary: s }) : undefined} onEditPeer={isLive ? openEditPeer : undefined} @@ -814,7 +829,7 @@ function IpsecPage() { { setUserOpen(v); if (!v) setEditing(null) }} - servers={serverOptions} + servers={serverOptions.filter(s => canManage(s.id) || busy)} peers={peerOptions} busy={busy} defaultServerId={sheetServerId} diff --git a/components/ipsec/ipsec-server-grid.tsx b/components/ipsec/ipsec-server-grid.tsx index 8742bbf4..44cf1fc5 100644 --- a/components/ipsec/ipsec-server-grid.tsx +++ b/components/ipsec/ipsec-server-grid.tsx @@ -150,6 +150,7 @@ function CertRow({ } export interface IpsecServerGridProps { + canManage?: (serverId: string) => boolean servers: IpsecServerSummaryDto[] onInit?: (row: IpsecServerSummaryDto) => void onRemove?: (row: IpsecServerSummaryDto) => void @@ -161,6 +162,7 @@ export interface IpsecServerGridProps { function IpsecServerGrid({ servers, + canManage, onInit, onRemove, onEditPeer, @@ -208,8 +210,8 @@ function IpsecServerGrid({ onEditPeer(s.serverId, peer) : undefined} - onDelete={onDeletePeer ? (peer) => onDeletePeer(s.serverId, peer) : undefined} + onEdit={onEditPeer && (!canManage || canManage(s.serverId)) ? (peer) => onEditPeer(s.serverId, peer) : undefined} + onDelete={onDeletePeer && (!canManage || canManage(s.serverId)) ? (peer) => onDeletePeer(s.serverId, peer) : undefined} /> ))} @@ -228,8 +230,8 @@ function IpsecServerGrid({ onDeleteCert(s.serverId, cert) : undefined} - onExport={onExportCert ? (cert) => onExportCert(s.serverId, cert) : undefined} + onDelete={onDeleteCert && (!canManage || canManage(s.serverId)) ? (cert) => onDeleteCert(s.serverId, cert) : undefined} + onExport={onExportCert && (!canManage || canManage(s.serverId)) ? (cert) => onExportCert(s.serverId, cert) : undefined} /> ))} @@ -250,6 +252,7 @@ function IpsecServerGrid({ type="button" size="sm" variant="outline" + disabled={!onInit || (canManage && !canManage(s.serverId))} onClick={() => onInit?.(s)} title={s.initialized ? "Обновить managed-конфиг сервера" : "Инициализировать IKEv2-сервер"} > @@ -264,6 +267,7 @@ function IpsecServerGrid({ className="size-8 text-destructive" aria-label="Удалить IKEv2-сервер" title="Удалить managed-объекты IKEv2 (identity/mode-config/peer/pool/NAT + сертификаты)" + disabled={canManage && !canManage(s.serverId)} onClick={() => onRemove(s)} > diff --git a/components/ipsec/ipsec-users-grid.tsx b/components/ipsec/ipsec-users-grid.tsx index ab53f730..c5ac39e7 100644 --- a/components/ipsec/ipsec-users-grid.tsx +++ b/components/ipsec/ipsec-users-grid.tsx @@ -28,6 +28,7 @@ import { } from "lucide-react" export interface IpsecUsersGridProps { + canManage?: (serverId: string) => boolean clients: IpsecClientDto[] compactServer?: boolean emptyAction?: ReactNode @@ -40,6 +41,7 @@ export interface IpsecUsersGridProps { function IpsecUsersGrid({ clients, + canManage, compactServer = false, emptyAction, emptyTitle = "Нет клиентов IKEv2", @@ -63,6 +65,7 @@ function IpsecUsersGrid({