Add managed Nginx HTTP-01 gateway activation and certificate status
This commit is contained in:
1 parent
6a1bace3e9
commit
cb28b235ef
10 files changed
+379
-23
No files matched your search
@@ -753,14 +753,15 @@
|
||||
},
|
||||
"tls": {
|
||||
"title": "Domain & certificate",
|
||||
"description": "Use OpsX VPN for automatic HTTP-01 certificates or preview an external Nginx / Certbot gateway.",
|
||||
"description": "Configure a public domain and use Nginx / Certbot for HTTP-01 issuance and renewal.",
|
||||
"domain": "Configured domain", "activeDomain": "Active domain", "saveDomain": "Save domain", "restartRequired": "Domain saved; restart OpsX VPN to enable ports 80/443 and automatic issuance", "saveFailed": "Could not save domain", "dns": "DNS", "certificate": "Certificate", "valid": "Valid", "pending": "Pending first HTTPS handshake",
|
||||
"notConfigured": "Disabled", "saved": "Domain configuration saved", "issuer": "Issuer", "expires": "Expires", "renewal": "Renewal", "automatic": "Automatic (autocert)",
|
||||
"ports": "HTTP / HTTPS ports", "externalPorts": "Managed by external Nginx; listener status not checked", "setupHint": "Restart to apply a saved domain or mode. Before switching to Nginx, follow the deployment guide to install it, issue a certificate, and validate the configuration. Go mode caches certificates in data/acme; saving settings alone does not issue a certificate.",
|
||||
"refresh": "Refresh status", "openHttps": "Open HTTPS health check", "loadFailed": "Could not load certificate status",
|
||||
"mode": "Public entry mode", "goMode": "Go automatic certificate (binds 80/443)", "nginxMode": "Nginx proxy (external 80/443)", "saveMode": "Save entry mode",
|
||||
"nginxHint": "Saving this mode does not install or start Nginx. Install Nginx and Certbot, verify issuance, configuration and renewal, then restart OpsX VPN. Go will no longer bind 80/443.",
|
||||
"externalRenewal": "Managed by Certbot (separate deployment required)", "previewNginx": "Preview Nginx configuration", "previewOnly": "Read-only preview; nothing has been installed or enabled. Use HTTP for issuance first, then enable HTTPS after the certificate exists.", "httpSite": "HTTP-01 site", "httpsSite": "HTTPS management site",
|
||||
"nginxHint": "Save the Nginx mode and domain, restart the service, then enable the site and request a certificate. Public ports 80/443 must be open.",
|
||||
"externalRenewal": "Managed by Certbot", "previewNginx": "Preview Nginx configuration", "previewOnly": "Configuration preview; HTTP handles issuance and HTTPS starts after the certificate exists.", "httpSite": "HTTP-01 site", "httpsSite": "HTTPS management site",
|
||||
"siteStage": "Site stage", "stage_none": "Inactive", "stage_http": "HTTP-01 ready; certificate pending", "stage_https": "HTTPS configuration enabled", "activateSite": "Enable site and issue certificate", "activateHint": "Checks existing sites and ports first. Certbot registration uses no email address, so renewal failures cannot be reported by email.", "siteReady": "HTTPS site and certificate are ready", "siteFailed": "Gateway or certificate setup failed; see output",
|
||||
"setupTitle": "Optional HTTPS public gateway", "setupStepCheck": "Detect system", "setupStepInstall": "Install dependencies", "setupStepConfigure": "Configure gateway and certificate", "detectedSystem": "Server OS", "setupUnsupported": "Automatic setup requires root on Ubuntu or TencentOS Server 4", "setupScope": "Use only if you choose the Nginx gateway. Installs Nginx / Certbot and prepares the validation directory. It does not install VPN protocol dependencies, enable ports, or issue certificates.", "setupButton": "Prepare Nginx / Certbot", "setupComplete": "Dependencies are ready; configure the site and certificate next", "setupFailed": "Initialization failed; inspect the output", "setupInterrupted": "Log connection interrupted; refresh status. Installation may still be running on the server",
|
||||
"commonSetupTitle": "First-time shared network setup", "commonSetupHint": "Check permissions, default route and IPv4 forwarding. Enable shared forwarding only when needed; install protocol-specific dependencies separately in Create Tunnel.", "hostRoot": "Privileges", "hostRoute": "Default route", "hostForward": "IPv4 forwarding", "enableForwarding": "Enable IPv4 forwarding", "forwardingEnabled": "IPv4 forwarding enabled"
|
||||
},
|
||||
|
||||
@@ -753,14 +753,15 @@
|
||||
},
|
||||
"tls": {
|
||||
"title": "域名与证书",
|
||||
"description": "可选择由 opsx-vpn 自动管理 HTTP-01 证书,或预览 Nginx / Certbot 外部入口配置。",
|
||||
"description": "配置公网域名,使用 Nginx / Certbot 申请并自动续签 HTTP-01 证书。",
|
||||
"domain": "配置域名", "activeDomain": "当前监听域名", "saveDomain": "保存域名", "restartRequired": "域名已保存,重启 opsx-vpn 后才会监听 80/443 并启用自动签发", "saveFailed": "保存域名失败", "dns": "DNS 解析", "certificate": "证书", "valid": "有效", "pending": "等待首次 HTTPS 请求签发",
|
||||
"notConfigured": "未启用", "saved": "域名配置已保存", "issuer": "颁发者", "expires": "到期时间", "renewal": "续签", "automatic": "自动(autocert)",
|
||||
"ports": "HTTP / HTTPS 端口", "externalPorts": "由外部 Nginx 管理,尚未检测监听状态", "setupHint": "保存域名或入口模式后需重启生效;切换为 Nginx 前须按部署文档完成安装、证书签发和配置校验。Go 模式证书缓存于 data/acme;保存配置本身不会签发证书。",
|
||||
"refresh": "刷新状态", "openHttps": "打开 HTTPS 健康检查", "loadFailed": "读取证书状态失败",
|
||||
"mode": "公网入口模式", "goMode": "Go 自动证书(进程监听 80/443)", "nginxMode": "Nginx 代理(外部监听 80/443)", "saveMode": "保存入口模式",
|
||||
"nginxHint": "仅保存模式不会安装或启动 Nginx。先安装 Nginx 和 Certbot,完成签发、配置检查与续期测试,再重启 opsx-vpn;重启后 Go 不再监听 80/443。",
|
||||
"externalRenewal": "由 Certbot 管理(需单独部署)", "previewNginx": "预览 Nginx 配置", "previewOnly": "以下为只读配置预览,尚未安装或启用;HTTP 站点先用于签发,HTTPS 站点只在证书已存在后启用。", "httpSite": "HTTP-01 站点", "httpsSite": "HTTPS 管理站点",
|
||||
"nginxHint": "选择 Nginx 模式并保存域名后重启服务,再点击启用站点申请证书。端口 80/443 需允许公网访问。",
|
||||
"externalRenewal": "由 Certbot 管理", "previewNginx": "预览 Nginx 配置", "previewOnly": "以下为配置预览;HTTP 站点用于签发,HTTPS 站点在证书存在后启用。", "httpSite": "HTTP-01 站点", "httpsSite": "HTTPS 管理站点",
|
||||
"siteStage": "站点阶段", "stage_none": "未启用", "stage_http": "HTTP-01 已准备,证书待签发", "stage_https": "HTTPS 配置已启用", "activateSite": "启用站点并申请证书", "activateHint": "启用前会检查现有站点及端口;冲突时停止并提示。证书采用 Certbot 无邮箱注册,续签失败时不会收到邮件通知。", "siteReady": "HTTPS 站点与证书已就绪", "siteFailed": "站点或证书配置失败,请查看输出",
|
||||
"setupTitle": "可选 · HTTPS 公网入口", "setupStepCheck": "检测系统", "setupStepInstall": "安装依赖", "setupStepConfigure": "配置入口与证书", "detectedSystem": "服务器系统", "setupUnsupported": "自动初始化仅支持以 root 运行的 Ubuntu 或 TencentOS Server 4", "setupScope": "仅在选择 Nginx 入口时点击:按需安装 Nginx / Certbot 并准备验证目录;不会安装 VPN 协议依赖、启用端口或签发证书。", "setupButton": "准备 Nginx / Certbot", "setupComplete": "依赖已准备,继续配置站点与证书", "setupFailed": "初始化失败,请查看输出", "setupInterrupted": "日志连接中断,请刷新状态;服务器上的安装可能仍在继续",
|
||||
"commonSetupTitle": "首次引导 · 通用网络环境", "commonSetupHint": "检查服务权限、默认出口和 IPv4 转发。仅按需开启通用转发;各专线协议的依赖在“创建专线”中分别安装。", "hostRoot": "运行权限", "hostRoute": "默认出口", "hostForward": "IPv4 转发", "enableForwarding": "开启 IPv4 转发", "forwardingEnabled": "IPv4 转发已开启"
|
||||
},
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</template>
|
||||
<el-button :loading="hostLoading" @click="loadHostReadiness">{{ $t('tls.refresh') }}</el-button>
|
||||
</el-card>
|
||||
<el-steps :active="nginxSetup?.nginx_installed && nginxSetup?.certbot_installed && nginxSetup?.webroot_ready ? 2 : 1" finish-status="success" style="margin-bottom:16px">
|
||||
<el-steps :active="nginxSetup?.site_stage === 'https' ? 3 : nginxSetup?.nginx_installed && nginxSetup?.certbot_installed && nginxSetup?.webroot_ready ? 2 : 1" finish-status="success" style="margin-bottom:16px">
|
||||
<el-step :title="$t('tls.setupStepCheck')" />
|
||||
<el-step :title="$t('tls.setupStepInstall')" />
|
||||
<el-step :title="$t('tls.setupStepConfigure')" />
|
||||
@@ -65,6 +65,7 @@
|
||||
<template v-if="nginxSetup">
|
||||
<p class="tls-hint">{{ $t('tls.detectedSystem') }}:{{ nginxSetup.os }} {{ nginxSetup.version }} · {{ nginxSetup.package_manager || '—' }}</p>
|
||||
<p class="tls-hint">Nginx:{{ nginxSetup.nginx_installed ? '✓' : '—' }} · Certbot:{{ nginxSetup.certbot_installed ? '✓' : '—' }} · HTTP-01 webroot:{{ nginxSetup.webroot_ready ? '✓' : '—' }}</p>
|
||||
<p class="tls-hint">{{ $t('tls.siteStage') }}:{{ $t(`tls.stage_${nginxSetup.site_stage || 'none'}`) }}</p>
|
||||
<el-alert v-if="!nginxSetup.supported || !nginxSetup.root" type="warning" :closable="false" :title="$t('tls.setupUnsupported')" style="margin-bottom:12px" />
|
||||
</template>
|
||||
<el-alert type="info" :closable="false" :title="$t('tls.setupScope')" style="margin-bottom:12px" />
|
||||
@@ -92,7 +93,7 @@
|
||||
<el-descriptions :column="1" size="small" border v-if="tlsStatus">
|
||||
<el-descriptions-item :label="$t('tls.domain')">{{ tlsStatus.configured_domain || $t('tls.notConfigured') }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tls.mode')">{{ tlsStatus.mode === 'nginx' ? $t('tls.nginxMode') : $t('tls.goMode') }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tls.activeDomain')">{{ tlsStatus.domain || $t('tls.notConfigured') }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tls.activeDomain')">{{ tlsStatus.mode === 'nginx' && nginxSetup?.site_stage !== 'https' ? $t('tls.notConfigured') : tlsStatus.domain || $t('tls.notConfigured') }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tls.dns')">{{ tlsStatus.dns?.join(', ') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tls.certificate')">
|
||||
<el-tag :type="tlsStatus.certificate_valid ? 'success' : 'warning'">{{ tlsStatus.certificate_valid ? $t('tls.valid') : tlsStatus.enabled ? $t('tls.pending') : $t('tls.notConfigured') }}</el-tag>
|
||||
@@ -104,7 +105,10 @@
|
||||
</el-descriptions>
|
||||
<p class="tls-hint">{{ $t('tls.setupHint') }}</p>
|
||||
<el-button :loading="tlsLoading" @click="loadTLS">{{ $t('tls.refresh') }}</el-button>
|
||||
<el-button v-if="tlsStatus?.enabled" type="primary" tag="a" :href="`https://${tlsStatus.domain}/api/health`" target="_blank" rel="noopener noreferrer">{{ $t('tls.openHttps') }}</el-button>
|
||||
<el-button v-if="tlsStatus?.enabled || (tlsStatus?.certificate_valid && nginxSetup?.site_stage === 'https')" type="primary" tag="a" :href="`https://${tlsStatus.domain}/api/health`" target="_blank" rel="noopener noreferrer">{{ $t('tls.openHttps') }}</el-button>
|
||||
<el-button type="primary" :loading="nginxActivating" :disabled="!nginxSetup?.root || !nginxSetup?.nginx_installed || !nginxSetup?.certbot_installed || !nginxSetup?.webroot_ready || nginxSetup?.running || tlsStatus?.mode !== 'nginx' || tlsStatus?.restart_required || !tlsStatus?.configured_domain" @click="activateNginx">{{ $t('tls.activateSite') }}</el-button>
|
||||
<p class="tls-hint">{{ $t('tls.activateHint') }}</p>
|
||||
<pre v-if="nginxActivateLog" ref="nginxActivateLogView" class="nginx-preview" role="log" aria-live="polite">{{ nginxActivateLog }}</pre>
|
||||
<el-button :loading="nginxLoading" :disabled="!tlsStatus?.configured_domain" @click="loadNginxPreview">{{ $t('tls.previewNginx') }}</el-button>
|
||||
<template v-if="nginxPreview">
|
||||
<p class="tls-hint">{{ $t('tls.previewOnly') }}</p>
|
||||
@@ -188,6 +192,52 @@ const nginxSetupLoading = ref(false)
|
||||
const nginxInstalling = ref(false)
|
||||
const nginxInstallLog = ref('')
|
||||
const nginxLogView = ref(null)
|
||||
const nginxActivating = ref(false)
|
||||
const nginxActivateLog = ref('')
|
||||
const nginxActivateLogView = ref(null)
|
||||
|
||||
async function activateNginx() {
|
||||
nginxActivating.value = true
|
||||
nginxActivateLog.value = ''
|
||||
let completed = false
|
||||
try {
|
||||
const response = await fetch('/api/system/tls/nginx-activate', {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${auth.token}` }
|
||||
})
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}))
|
||||
throw new Error(body.error || `${response.status} ${response.statusText}`)
|
||||
}
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
const show = (line) => {
|
||||
if (!line) return
|
||||
const event = JSON.parse(line)
|
||||
nginxActivateLog.value = (nginxActivateLog.value + (event.message || '')).slice(-100000)
|
||||
if (event.type === 'done') completed = true
|
||||
if (event.type === 'error') throw new Error(event.message)
|
||||
nextTick(() => { if (nginxActivateLogView.value) nginxActivateLogView.value.scrollTop = nginxActivateLogView.value.scrollHeight })
|
||||
}
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop()
|
||||
for (const line of lines) show(line)
|
||||
}
|
||||
if (buffer) show(buffer)
|
||||
if (!completed) throw new Error(t('tls.setupInterrupted'))
|
||||
ElMessage.success(t('tls.siteReady'))
|
||||
} catch (e) {
|
||||
nginxActivateLog.value += `\n${e.message}\n`
|
||||
ElMessage.error(t('tls.siteFailed'))
|
||||
} finally {
|
||||
nginxActivating.value = false
|
||||
await Promise.all([loadNginxSetup(), loadTLS()])
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNginxSetup() {
|
||||
nginxSetupLoading.value = true
|
||||
|
||||
Reference in new issue
Block a user