fix(ui): сохранять черновики и результат настроек ACME
This commit is contained in:
1 parent
1c407b77de
commit
594e60a1f4
7 files changed
+223
-161
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Suspense, useMemo, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { readCertificatesUrl, patchCertificatesUrl, type CertificateStatusFilter } from "@/lib/certificates-url-state"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
@@ -13,6 +13,7 @@ import { OpsPanel } from "@/components/ops-panel"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { DataPageToolbar } from "@/components/data-page-toolbar"
|
||||
import { CertificatesDataGrid } from "@/components/data-grids/certificates-data-grid"
|
||||
import { CertificateAcmeSettings } from "@/components/certificates/certificate-acme-settings"
|
||||
import { CertificateRenewSettingsPanel } from "@/components/certificates/certificate-renew-settings"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
|
||||
@@ -36,10 +37,7 @@ import type { CertificateView } from "@/lib/certificate-view"
|
||||
import { toFrontendServer } from "@/entities/server/model/mappers"
|
||||
import {
|
||||
createCertificateIssueJob,
|
||||
getAcmeSettings,
|
||||
getCertificateIssueJob,
|
||||
putAcmeSettings,
|
||||
testAcmeSettings,
|
||||
} from "@/shared/api/certificates"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
@@ -163,87 +161,6 @@ function CertPartKpi({
|
||||
)
|
||||
}
|
||||
|
||||
function CertPartAcmeSettings({
|
||||
acmeDirectoryUrl,
|
||||
setAcmeDirectoryUrl,
|
||||
acmeZoneId,
|
||||
setAcmeZoneId,
|
||||
acmeTokenDraft,
|
||||
setAcmeTokenDraft,
|
||||
acmeTokenConfigured,
|
||||
acmeSaveBusy,
|
||||
onTest,
|
||||
onSave,
|
||||
}: {
|
||||
acmeDirectoryUrl: string
|
||||
setAcmeDirectoryUrl: (v: string) => void
|
||||
acmeZoneId: string
|
||||
setAcmeZoneId: (v: string) => void
|
||||
acmeTokenDraft: string
|
||||
setAcmeTokenDraft: (v: string) => void
|
||||
acmeTokenConfigured: boolean
|
||||
acmeSaveBusy: boolean
|
||||
onTest: () => void
|
||||
onSave: () => void
|
||||
}) {
|
||||
return (
|
||||
<OpsPanel
|
||||
title="ACME · Cloudflare DNS-01"
|
||||
description="Публичные Let's Encrypt для зон в Cloudflare выпускаются на backend и импортируются на RouterOS 7.22+."
|
||||
contentClassName="px-5 py-4 flex flex-col gap-3"
|
||||
>
|
||||
<form
|
||||
className="grid gap-3 md:grid-cols-2"
|
||||
autoComplete="off"
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">ACME directory URL</label>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
name="acme-directory-url"
|
||||
value={acmeDirectoryUrl}
|
||||
onChange={(e) => setAcmeDirectoryUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">Cloudflare zone id (опционально)</label>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
name="cloudflare-zone-id"
|
||||
value={acmeZoneId}
|
||||
onChange={(e) => setAcmeZoneId(e.target.value)}
|
||||
placeholder="Авто по домену"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 md:col-span-2">
|
||||
<label className="text-sm font-medium">Cloudflare API token</label>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
name="cloudflare-api-token"
|
||||
value={acmeTokenDraft}
|
||||
onChange={(e) => setAcmeTokenDraft(e.target.value)}
|
||||
placeholder={
|
||||
acmeTokenConfigured
|
||||
? "Токен сохранён — введите новый для замены"
|
||||
: "API token с правом DNS"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" disabled={acmeSaveBusy} onClick={onTest}>
|
||||
Проверить Cloudflare
|
||||
</Button>
|
||||
<Button size="sm" disabled={acmeSaveBusy} onClick={onSave}>
|
||||
Сохранить настройки
|
||||
</Button>
|
||||
</div>
|
||||
</OpsPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function CertPartReference() {
|
||||
return (
|
||||
<OpsPanel
|
||||
@@ -466,14 +383,6 @@ function CertificatesContent() {
|
||||
const [issueTrustApi, setIssueTrustApi] = useState(true)
|
||||
|
||||
|
||||
const [acmeDirectoryUrl, setAcmeDirectoryUrl] = useState(
|
||||
"https://acme-v02.api.letsencrypt.org/directory",
|
||||
)
|
||||
const [acmeZoneId, setAcmeZoneId] = useState("")
|
||||
const [acmeTokenDraft, setAcmeTokenDraft] = useState("")
|
||||
const [acmeTokenConfigured, setAcmeTokenConfigured] = useState(false)
|
||||
const [acmeSaveBusy, setAcmeSaveBusy] = useState(false)
|
||||
|
||||
const displayCerts = useMemo(() => {
|
||||
if (!prefsHydrated) return []
|
||||
if (isLive) return Object.values(snapshots).flatMap(snapshot => snapshot.confirmed?.certificates ?? [])
|
||||
@@ -510,22 +419,6 @@ function CertificatesContent() {
|
||||
|
||||
const loadLive = catalog.reload
|
||||
|
||||
const loadAcmeSettings = useCallback(async () => {
|
||||
if (!liveReady || !canRead) return
|
||||
try {
|
||||
const s = await getAcmeSettings(backendUrl)
|
||||
setAcmeDirectoryUrl(s.directoryUrl)
|
||||
setAcmeZoneId(s.defaultZoneId ?? "")
|
||||
setAcmeTokenConfigured(s.tokenConfigured)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [backendUrl, liveReady, canRead])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => { void loadAcmeSettings() })
|
||||
}, [loadAcmeSettings])
|
||||
|
||||
const expiring = useMemo(
|
||||
() => scopedCerts.filter((c) => c.status === "valid" && c.daysLeft !== null && c.daysLeft >= 0 && c.daysLeft <= 30),
|
||||
[scopedCerts],
|
||||
@@ -601,38 +494,6 @@ function CertificatesContent() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveAcmeSettings() {
|
||||
if (!liveReady) return
|
||||
setAcmeSaveBusy(true)
|
||||
try {
|
||||
const saved = await putAcmeSettings(backendUrl, {
|
||||
directoryUrl: acmeDirectoryUrl.trim(),
|
||||
defaultZoneId: acmeZoneId.trim() || null,
|
||||
cloudflareApiToken: acmeTokenDraft.trim() ? acmeTokenDraft.trim() : undefined,
|
||||
})
|
||||
setAcmeTokenConfigured(saved.tokenConfigured)
|
||||
setAcmeTokenDraft("")
|
||||
toast.success("Настройки ACME сохранены")
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Не удалось сохранить настройки")
|
||||
} finally {
|
||||
setAcmeSaveBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestAcme() {
|
||||
if (!liveReady) return
|
||||
try {
|
||||
const res = await testAcmeSettings(backendUrl, {
|
||||
cloudflareApiToken: acmeTokenDraft.trim() || undefined,
|
||||
})
|
||||
if (res.ok) toast.success(res.message ?? "Cloudflare API доступен")
|
||||
else toast.error(res.message ?? "Проверка не прошла")
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Проверка Cloudflare не удалась")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ServerRailLayout
|
||||
@@ -717,24 +578,7 @@ function CertificatesContent() {
|
||||
|
||||
<CertificateRenewSettingsPanel backendUrl={backendUrl} liveReady={liveReady} canRead={canRead} canWrite={canWrite} />
|
||||
|
||||
{liveReady && canWrite && (
|
||||
<CertPartAcmeSettings
|
||||
acmeDirectoryUrl={acmeDirectoryUrl}
|
||||
setAcmeDirectoryUrl={setAcmeDirectoryUrl}
|
||||
acmeZoneId={acmeZoneId}
|
||||
setAcmeZoneId={setAcmeZoneId}
|
||||
acmeTokenDraft={acmeTokenDraft}
|
||||
setAcmeTokenDraft={setAcmeTokenDraft}
|
||||
acmeTokenConfigured={acmeTokenConfigured}
|
||||
acmeSaveBusy={acmeSaveBusy}
|
||||
onTest={() => {
|
||||
void handleTestAcme()
|
||||
}}
|
||||
onSave={() => {
|
||||
void handleSaveAcmeSettings()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CertificateAcmeSettings backendUrl={backendUrl} liveReady={liveReady} canRead={canRead} canWrite={canWrite} />
|
||||
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
|
||||
Reference in new issue
Block a user