Add guarded IP certificate flow and renewal history
This commit is contained in:
1 parent
dd94a4cc84
commit
d3699abb18
14 files changed
+343
-56
No files matched your search
@@ -966,6 +966,9 @@
|
||||
"saveEntry": "Save public entry",
|
||||
"hostRequired": "Enter a domain or IPv4 address",
|
||||
"certListTitle": "Certificates",
|
||||
"httpNoCertificate": "This HTTP gateway has no TLS certificate or renewal. Switch to HTTPS to request one.",
|
||||
"ipCertificateRequirement": "IP certificates last about six days. Automated issuance needs Certbot 5.4+, public port 80, and scheduled renewal. Unsupported server versions cannot issue one.",
|
||||
"renewalHistory": "Recent successful renewals",
|
||||
"addCert": "Add certificate",
|
||||
"replaceCert": "Replace",
|
||||
"certSource": "Source",
|
||||
|
||||
@@ -966,6 +966,9 @@
|
||||
"saveEntry": "保存公网入口",
|
||||
"hostRequired": "请填写域名或 IPv4 地址",
|
||||
"certListTitle": "证书",
|
||||
"httpNoCertificate": "当前是 HTTP 入口,没有 TLS 证书,也不会自动续签。切换 HTTPS 后才能申请证书。",
|
||||
"ipCertificateRequirement": "IP 证书有效期约 6 天;自动申请需要 Certbot 5.4+、公网 80 端口和定时续签。当前服务器版本不足时会阻止签发。",
|
||||
"renewalHistory": "最近成功续签",
|
||||
"addCert": "添加证书",
|
||||
"replaceCert": "更换",
|
||||
"certSource": "来源",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<b>{{ $t('tls.certListTitle') }}</b>
|
||||
<el-button type="primary" :disabled="legacyMode" @click="openAddCert">{{ $t('tls.addCert') }}</el-button>
|
||||
<el-button type="primary" :disabled="legacyMode || entry.scheme === 'http'" @click="openAddCert">{{ $t('tls.addCert') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="certRows" size="default" v-loading="tlsLoading">
|
||||
@@ -68,7 +68,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tls.certSource')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.source === 'upload' ? 'info' : 'success'">{{ $t(row.source === 'upload' ? 'tls.sourceUpload' : 'tls.sourceHttp01') }}</el-tag>
|
||||
<el-tag size="small" :type="row.source === 'upload' ? 'info' : 'success'">{{ row.source === 'upload' ? $t('tls.sourceUpload') : row.source === 'http01' ? $t('tls.sourceHttp01') : '—' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tls.certificate')" width="110">
|
||||
@@ -87,11 +87,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" width="120" fixed="right">
|
||||
<template #default>
|
||||
<el-button link type="primary" @click="openAddCert">{{ $t('tls.replaceCert') }}</el-button>
|
||||
<el-button link type="primary" :disabled="legacyMode" @click="openAddCert">{{ $t('tls.replaceCert') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :description="$t('tls.noCerts')" /></template>
|
||||
<template #empty><el-empty :description="$t(entry.scheme === 'http' ? 'tls.httpNoCertificate' : 'tls.noCerts')" /></template>
|
||||
</el-table>
|
||||
<el-alert v-if="entry.scheme === 'http'" type="info" :closable="false" :title="$t('tls.httpNoCertificate')" style="margin-top:12px" />
|
||||
<el-alert v-if="entry.scheme === 'https' && isIPv4(entry.host)" type="warning" :closable="false" :title="$t('tls.ipCertificateRequirement')" style="margin-top:12px" />
|
||||
<div v-if="recentRenewals.length && entry.scheme === 'https'" class="tls-renewals">
|
||||
<b>{{ $t('tls.renewalHistory') }}</b>
|
||||
<p v-for="event in recentRenewals" :key="`${event.time}-${event.host}`" class="tls-hint">{{ new Date(event.time).toLocaleString() }} · {{ event.host }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Nginx 准备(折叠) -->
|
||||
@@ -124,7 +130,7 @@
|
||||
<!-- 添加证书弹窗 -->
|
||||
<el-dialog v-model="certDialog" :title="$t('tls.addCert')" width="640px" destroy-on-close @closed="resetCertForm">
|
||||
<div class="method-cards">
|
||||
<div class="method-card" :class="{ active: certForm.method === 'upload' }" @click="certForm.method = 'upload'">
|
||||
<div v-if="!isIPv4(entry.host)" class="method-card" :class="{ active: certForm.method === 'upload' }" @click="certForm.method = 'upload'">
|
||||
<el-radio v-model="certForm.method" value="upload">{{ $t('tls.methodUpload') }}</el-radio>
|
||||
</div>
|
||||
<div class="method-card" :class="{ active: certForm.method === 'http01' }" @click="certForm.method = 'http01'">
|
||||
@@ -231,6 +237,7 @@ const entry = reactive({
|
||||
host: '',
|
||||
port: 80,
|
||||
})
|
||||
const recentRenewals = computed(() => (tlsStatus.value?.renewal_events || []).filter(e => e.host === entry.host).slice(0, 5))
|
||||
|
||||
const certForm = reactive({
|
||||
method: 'upload',
|
||||
@@ -246,20 +253,25 @@ const publicBase = computed(() => {
|
||||
return buildPublicBase(entry)
|
||||
})
|
||||
|
||||
function isIPv4(host) {
|
||||
const parts = String(host || '').split('.')
|
||||
return parts.length === 4 && parts.every(p => /^\d{1,3}$/.test(p) && Number(p) <= 255)
|
||||
}
|
||||
|
||||
const certRows = computed(() => {
|
||||
const s = tlsStatus.value
|
||||
if (!s) return []
|
||||
const host = s.configured_domain || s.domain || entry.host
|
||||
if (!host && !s.certificate_valid && !s.enabled && !s.certificate_cached) return []
|
||||
const source = s.cert_source || (s.mode === 'nginx' || s.enabled ? 'http01' : (s.certificate_cached ? 'upload' : 'http01'))
|
||||
if (!s || entry.scheme !== 'https' || s.configured_scheme !== 'https') return []
|
||||
const host = s.configured_domain || s.domain
|
||||
if (!host || (!s.certificate_cached && !s.certificate_valid) || s.configured_public_base !== s.public_base) return []
|
||||
const source = s.cert_source || (s.enabled ? 'http01' : '')
|
||||
return [{
|
||||
host,
|
||||
source,
|
||||
valid: !!s.certificate_valid,
|
||||
pending: !!s.enabled && !s.certificate_valid,
|
||||
expires: s.not_after || null,
|
||||
expires: s.not_after && !s.not_after.startsWith('0001-') ? s.not_after : null,
|
||||
renewalFailed: !!s.renewal_failed,
|
||||
renewalLabel: s.enabled ? t('tls.automatic') : s.mode === 'nginx' ? t('tls.externalRenewal') : '—',
|
||||
renewalLabel: source === 'http01' ? (s.enabled ? t('tls.automatic') : t('tls.externalRenewal')) : '—',
|
||||
}]
|
||||
})
|
||||
|
||||
@@ -268,7 +280,7 @@ function onSchemeChange() {
|
||||
}
|
||||
|
||||
function resetCertForm() {
|
||||
certForm.method = 'upload'
|
||||
certForm.method = isIPv4(entry.host) ? 'http01' : 'upload'
|
||||
certForm.certPem = ''
|
||||
certForm.keyPem = ''
|
||||
certForm.certFileName = ''
|
||||
@@ -481,13 +493,14 @@ onMounted(() => { if (!auth.mustChange) refreshAll() })
|
||||
.settings-column :deep(.el-card__header) { background: #f9fbfe; color: #21344e; }
|
||||
.card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
||||
.tls-hint { color: var(--text-secondary); line-height: 1.6; font-size: 13px; margin-top: 6px; }
|
||||
.tls-renewals { margin-top: 12px; }
|
||||
.derived-base :deep(.el-input__wrapper) { background: var(--el-fill-color-light); }
|
||||
.derived-base :deep(.el-input__inner) { color: var(--el-text-color-secondary); cursor: default; }
|
||||
.nginx-preview { max-height: 280px; overflow: auto; background: #172239; color: #e7eefc; border-radius: 8px; padding: 14px; font-size: 12px; line-height: 1.5; margin-top: 8px; }
|
||||
.scheme-group { width: 100%; }
|
||||
.scheme-group :deep(.el-radio-button) { flex: 1; }
|
||||
.scheme-group :deep(.el-radio-button__inner) { width: 100%; }
|
||||
.method-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.method-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
|
||||
.method-card {
|
||||
border: 1px solid var(--el-border-color); border-radius: 10px; padding: 14px 16px; cursor: pointer;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
|
||||
Reference in new issue
Block a user