补充证书历史版本前端恢复入口
保存证书历史版本列表、恢复确认、幂等提交和任务详情跳转的前端实现,作为后续样式与测试优化基线。
This commit is contained in:
parent
3aa1d50251
commit
bd5f31946c
@ -65,6 +65,12 @@ export function useDashboard() {
|
|||||||
const importSaving = ref(false);
|
const importSaving = ref(false);
|
||||||
const importError = ref("");
|
const importError = ref("");
|
||||||
const importResult = ref(null);
|
const importResult = ref(null);
|
||||||
|
const showCertificateVersions = ref(false);
|
||||||
|
const versionDomain = ref(null);
|
||||||
|
const certificateVersions = ref([]);
|
||||||
|
const versionsLoading = ref(false);
|
||||||
|
const versionsError = ref("");
|
||||||
|
const restoreSubmitting = ref("");
|
||||||
let importIdempotencyKey = "";
|
let importIdempotencyKey = "";
|
||||||
const importFiles = ref({
|
const importFiles = ref({
|
||||||
certificate: null,
|
certificate: null,
|
||||||
@ -92,6 +98,7 @@ export function useDashboard() {
|
|||||||
showServerForm.value ||
|
showServerForm.value ||
|
||||||
showSettings.value ||
|
showSettings.value ||
|
||||||
showCertificateImport.value ||
|
showCertificateImport.value ||
|
||||||
|
showCertificateVersions.value ||
|
||||||
deploymentResult.value !== null ||
|
deploymentResult.value !== null ||
|
||||||
showTaskDetails.value,
|
showTaskDetails.value,
|
||||||
);
|
);
|
||||||
@ -847,6 +854,64 @@ export function useDashboard() {
|
|||||||
if (!importSaving.value) showCertificateImport.value = false;
|
if (!importSaving.value) showCertificateImport.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openCertificateVersions(item) {
|
||||||
|
versionDomain.value = item;
|
||||||
|
certificateVersions.value = [];
|
||||||
|
versionsError.value = "";
|
||||||
|
versionsLoading.value = true;
|
||||||
|
showCertificateVersions.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetchAdmin(
|
||||||
|
`/api/v1/domains/${item.id}/certificate/versions`,
|
||||||
|
);
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(result.error || "读取证书历史版本失败");
|
||||||
|
certificateVersions.value = Array.isArray(result.items) ? result.items : [];
|
||||||
|
} catch (err) {
|
||||||
|
versionsError.value = err.message || "读取证书历史版本失败";
|
||||||
|
} finally {
|
||||||
|
versionsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCertificateVersions() {
|
||||||
|
if (!restoreSubmitting.value) showCertificateVersions.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreCertificateVersion(version) {
|
||||||
|
if (!version?.history_id || version.current || restoreSubmitting.value) return;
|
||||||
|
if (!window.confirm("确定恢复这个证书历史版本吗?恢复只更新 Scert 本地托管证书,不代表所有远端服务器已经部署。")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const domain = versionDomain.value;
|
||||||
|
if (!domain) return;
|
||||||
|
const suffix = crypto.randomUUID ? crypto.randomUUID() : createRandomSuffix();
|
||||||
|
restoreSubmitting.value = version.history_id;
|
||||||
|
versionsError.value = "";
|
||||||
|
try {
|
||||||
|
const response = await fetchAdmin(
|
||||||
|
`/api/v1/domains/${domain.id}/certificate/versions/${encodeURIComponent(version.history_id)}/restore`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Idempotency-Key": `certificate-restore-${domain.id}-${suffix}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
const replayed = response.status === 409 && result.replayed;
|
||||||
|
if (!(response.ok || replayed) || !result.task?.id) {
|
||||||
|
throw new Error(result.error || "提交证书历史版本恢复失败");
|
||||||
|
}
|
||||||
|
notice.value = `证书历史版本恢复任务已提交:${result.task.id},恢复完成后请另行确认远端部署状态`;
|
||||||
|
showCertificateVersions.value = false;
|
||||||
|
await loadData();
|
||||||
|
await openTaskDetails(result.task);
|
||||||
|
} catch (err) {
|
||||||
|
versionsError.value = err.message || "提交证书历史版本恢复失败";
|
||||||
|
} finally {
|
||||||
|
restoreSubmitting.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectCertificateImportFile(kind, event) {
|
function selectCertificateImportFile(kind, event) {
|
||||||
importFiles.value[kind] = event.target.files?.[0] || null;
|
importFiles.value[kind] = event.target.files?.[0] || null;
|
||||||
importError.value = "";
|
importError.value = "";
|
||||||
@ -1127,6 +1192,15 @@ export function useDashboard() {
|
|||||||
closeCertificateImport,
|
closeCertificateImport,
|
||||||
selectCertificateImportFile,
|
selectCertificateImportFile,
|
||||||
importCertificate,
|
importCertificate,
|
||||||
|
showCertificateVersions,
|
||||||
|
versionDomain,
|
||||||
|
certificateVersions,
|
||||||
|
versionsLoading,
|
||||||
|
versionsError,
|
||||||
|
restoreSubmitting,
|
||||||
|
openCertificateVersions,
|
||||||
|
closeCertificateVersions,
|
||||||
|
restoreCertificateVersion,
|
||||||
saveServer,
|
saveServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
saveDomain,
|
saveDomain,
|
||||||
|
|||||||
@ -74,6 +74,15 @@ const {
|
|||||||
closeCertificateImport,
|
closeCertificateImport,
|
||||||
selectCertificateImportFile,
|
selectCertificateImportFile,
|
||||||
importCertificate,
|
importCertificate,
|
||||||
|
showCertificateVersions,
|
||||||
|
versionDomain,
|
||||||
|
certificateVersions,
|
||||||
|
versionsLoading,
|
||||||
|
versionsError,
|
||||||
|
restoreSubmitting,
|
||||||
|
openCertificateVersions,
|
||||||
|
closeCertificateVersions,
|
||||||
|
restoreCertificateVersion,
|
||||||
settingsSaving,
|
settingsSaving,
|
||||||
settingsError,
|
settingsError,
|
||||||
dnsProviderCatalog,
|
dnsProviderCatalog,
|
||||||
@ -384,6 +393,9 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
|||||||
<button class="text-button" type="button" @click="openCertificateImport(item)">
|
<button class="text-button" type="button" @click="openCertificateImport(item)">
|
||||||
导入证书
|
导入证书
|
||||||
</button>
|
</button>
|
||||||
|
<button class="text-button" type="button" @click="openCertificateVersions(item)">
|
||||||
|
历史版本
|
||||||
|
</button>
|
||||||
<button class="text-button" type="button" @click="openEdit(item)">
|
<button class="text-button" type="button" @click="openEdit(item)">
|
||||||
编辑</button
|
编辑</button
|
||||||
><button
|
><button
|
||||||
@ -495,6 +507,54 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showCertificateVersions" class="modal-backdrop" @click.self="closeCertificateVersions">
|
||||||
|
<section class="modal-card certificate-history-modal" role="dialog" aria-modal="true" aria-label="证书历史版本">
|
||||||
|
<div class="modal-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">证书管理</p>
|
||||||
|
<h2>证书历史版本</h2>
|
||||||
|
<p class="muted">托管域名:{{ versionDomain?.name }}</p>
|
||||||
|
</div>
|
||||||
|
<button class="close-button" type="button" aria-label="关闭" @click="closeCertificateVersions">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="security-hint">
|
||||||
|
这里仅恢复 Scert 本地托管证书。恢复完成不等于远端服务器已部署,请在任务完成后单独确认部署状态。
|
||||||
|
</p>
|
||||||
|
<div v-if="versionsLoading" class="empty-state">正在读取证书历史版本……</div>
|
||||||
|
<p v-else-if="versionsError" class="form-error" role="alert">{{ versionsError }}</p>
|
||||||
|
<div v-else-if="certificateVersions.length === 0" class="empty-state compact-empty">
|
||||||
|
<strong>暂无可显示的历史版本</strong>
|
||||||
|
<span>证书成功保存后,历史版本会出现在这里。</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="certificate-history-list">
|
||||||
|
<article v-for="version in certificateVersions" :key="version.history_id || version.version" class="certificate-history-row">
|
||||||
|
<div>
|
||||||
|
<div class="domain-title-line">
|
||||||
|
<strong>{{ version.current ? "当前版本" : "历史版本" }}</strong>
|
||||||
|
<span v-if="version.current" class="tag success">正在使用</span>
|
||||||
|
</div>
|
||||||
|
<div class="domain-meta">
|
||||||
|
<span>版本:{{ version.version || "未记录" }}</span>
|
||||||
|
<span>保存时间:{{ formatDate(version.saved_at) }}</span>
|
||||||
|
<span>有效期至:{{ formatDate(version.not_after) }}</span>
|
||||||
|
<span v-if="version.fingerprint_sha256">指纹:{{ version.fingerprint_sha256 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!version.current"
|
||||||
|
class="secondary-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="!!restoreSubmitting"
|
||||||
|
@click="restoreCertificateVersion(version)"
|
||||||
|
>
|
||||||
|
{{ restoreSubmitting === version.history_id ? "提交中……" : "恢复此版本" }}
|
||||||
|
</button>
|
||||||
|
<span v-else class="muted">当前版本不可重复恢复</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DomainModal v-if="showForm" :state="dashboard" />
|
<DomainModal v-if="showForm" :state="dashboard" />
|
||||||
|
|
||||||
<ServerModal v-if="showServerForm" :state="dashboard" />
|
<ServerModal v-if="showServerForm" :state="dashboard" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user