feat(tls): add optional nginx setup and gateway preview

This commit is contained in:
opsx committed 2026-10-21 11:33:44 +08:00
1 parent 9f743e19ad
commit d2f1f5040e
15 files changed
+753 -16

No files matched your search

+154 -3
View File
@@ -46,15 +46,52 @@
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
<template #header><b>{{ $t('tls.title') }}</b></template>
<el-alert type="info" :closable="false" :title="$t('tls.description')" style="margin-bottom:16px" />
<el-card shadow="never" style="margin-bottom:16px">
<template #header><b>{{ $t('tls.commonSetupTitle') }}</b></template>
<el-alert type="info" :closable="false" :title="$t('tls.commonSetupHint')" style="margin-bottom:12px" />
<template v-if="hostReadiness">
<p class="tls-hint">{{ $t('tls.hostRoot') }}:{{ hostReadiness.root ? '✓' : '—' }} · {{ $t('tls.hostRoute') }}:{{ hostReadiness.default_route_ready ? '✓' : '—' }} · {{ $t('tls.hostForward') }}:{{ hostReadiness.ipv4_forwarding ? '✓' : '—' }}</p>
<el-button :loading="hostFixing" :disabled="!hostReadiness.root || hostReadiness.ipv4_forwarding" @click="enableForwarding">{{ $t('tls.enableForwarding') }}</el-button>
</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-step :title="$t('tls.setupStepCheck')" />
<el-step :title="$t('tls.setupStepInstall')" />
<el-step :title="$t('tls.setupStepConfigure')" />
</el-steps>
<el-card shadow="never" style="margin-bottom:16px">
<template #header><b>{{ $t('tls.setupTitle') }}</b></template>
<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>
<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" />
<el-button :loading="nginxSetupLoading" @click="loadNginxSetup">{{ $t('tls.refresh') }}</el-button>
<el-button type="primary" :loading="nginxInstalling" :disabled="!nginxSetup?.supported || !nginxSetup?.root || nginxSetup?.running || (nginxSetup?.nginx_installed && nginxSetup?.certbot_installed && nginxSetup?.webroot_ready)" @click="initializeNginx">{{ $t('tls.setupButton') }}</el-button>
<pre v-if="nginxInstallLog" ref="nginxLogView" class="nginx-preview" role="log" aria-live="polite">{{ nginxInstallLog }}</pre>
</el-card>
<el-form @submit.prevent="saveTLSDomain" label-position="top">
<el-form-item :label="$t('tls.domain')">
<el-input v-model.trim="tlsDomain" placeholder="test-link.appx365.com" maxlength="253" />
</el-form-item>
<el-button type="primary" :loading="tlsSaving" @click="saveTLSDomain">{{ $t('tls.saveDomain') }}</el-button>
</el-form>
<el-form label-position="top" style="margin-top:16px" @submit.prevent="saveTLSMode">
<el-form-item :label="$t('tls.mode')">
<el-select v-model="tlsMode" style="width:100%">
<el-option value="autocert" :label="$t('tls.goMode')" />
<el-option value="nginx" :label="$t('tls.nginxMode')" />
</el-select>
</el-form-item>
<el-button :loading="tlsSavingMode" @click="saveTLSMode">{{ $t('tls.saveMode') }}</el-button>
</el-form>
<el-alert v-if="tlsMode === 'nginx'" type="warning" :closable="false" :title="$t('tls.nginxHint')" style="margin:12px 0" />
<el-alert v-if="tlsStatus?.restart_required" type="warning" :closable="false" :title="$t('tls.restartRequired')" style="margin:12px 0" />
<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.dns')">{{ tlsStatus.dns?.join(', ') || '—' }}</el-descriptions-item>
<el-descriptions-item :label="$t('tls.certificate')">
@@ -62,12 +99,20 @@
</el-descriptions-item>
<el-descriptions-item v-if="tlsStatus.certificate_cached" :label="$t('tls.issuer')">{{ tlsStatus.issuer || '—' }}</el-descriptions-item>
<el-descriptions-item v-if="tlsStatus.certificate_cached" :label="$t('tls.expires')">{{ new Date(tlsStatus.not_after).toLocaleString() }}</el-descriptions-item>
<el-descriptions-item :label="$t('tls.renewal')">{{ tlsStatus.enabled ? $t('tls.automatic') : '—' }}</el-descriptions-item>
<el-descriptions-item :label="$t('tls.ports')">{{ tlsStatus.http_address || ':80' }} / {{ tlsStatus.https_address || ':443' }}</el-descriptions-item>
<el-descriptions-item :label="$t('tls.renewal')">{{ tlsStatus.enabled ? $t('tls.automatic') : tlsStatus.mode === 'nginx' ? $t('tls.externalRenewal') : '—' }}</el-descriptions-item>
<el-descriptions-item :label="$t('tls.ports')">{{ tlsStatus.mode === 'nginx' ? $t('tls.externalPorts') : `${tlsStatus.http_address || ':80'} / ${tlsStatus.https_address || ':443'}` }}</el-descriptions-item>
</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 :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>
<p class="tls-hint">{{ $t('tls.httpSite') }}</p>
<pre class="nginx-preview">{{ nginxPreview.http_site }}</pre>
<p class="tls-hint">{{ $t('tls.httpsSite') }}</p>
<pre class="nginx-preview">{{ nginxPreview.https_site }}</pre>
</template>
</el-card>
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('settings.auditTitle') }}</b></template>
@@ -112,6 +157,89 @@ const tlsStatus = ref(null)
const tlsLoading = ref(false)
const tlsSaving = ref(false)
const tlsDomain = ref('')
const tlsMode = ref('autocert')
const tlsSavingMode = ref(false)
const nginxLoading = ref(false)
const nginxPreview = ref(null)
const hostReadiness = ref(null)
const hostLoading = ref(false)
const hostFixing = ref(false)
async function loadHostReadiness() {
hostLoading.value = true
try {
const { data } = await api.get('/readiness')
hostReadiness.value = data
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.loadFailed')) }
finally { hostLoading.value = false }
}
async function enableForwarding() {
hostFixing.value = true
try {
await api.post('/system/fix', { action: 'ip_forward' })
await loadHostReadiness()
ElMessage.success(t('tls.forwardingEnabled'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.saveFailed')) }
finally { hostFixing.value = false }
}
const nginxSetup = ref(null)
const nginxSetupLoading = ref(false)
const nginxInstalling = ref(false)
const nginxInstallLog = ref('')
const nginxLogView = ref(null)
async function loadNginxSetup() {
nginxSetupLoading.value = true
try {
const { data } = await api.get('/system/tls/nginx-setup')
nginxSetup.value = data
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.loadFailed')) }
finally { nginxSetupLoading.value = false }
}
async function initializeNginx() {
nginxInstalling.value = true
nginxInstallLog.value = ''
let completed = false
try {
const response = await fetch('/api/system/tls/nginx-setup', {
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)
nginxInstallLog.value = (nginxInstallLog.value + (event.message || '')).slice(-100000)
if (event.type === 'done') completed = true
if (event.type === 'error') throw new Error(event.message)
nextTick(() => { if (nginxLogView.value) nginxLogView.value.scrollTop = nginxLogView.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.setupComplete'))
} catch (e) {
nginxInstallLog.value += `\n${e.message}\n`
ElMessage.error(t('tls.setupFailed'))
} finally {
nginxInstalling.value = false
await loadNginxSetup()
}
}
async function loadTLS() {
tlsLoading.value = true
@@ -119,15 +247,37 @@ async function loadTLS() {
const { data } = await api.get('/system/tls')
tlsStatus.value = data
tlsDomain.value = data.configured_domain || data.domain || ''
tlsMode.value = data.configured_mode || data.mode || 'autocert'
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.loadFailed')) }
finally { tlsLoading.value = false }
}
async function saveTLSMode() {
tlsSavingMode.value = true
try {
const { data } = await api.put('/system/tls/mode', { mode: tlsMode.value })
tlsStatus.value = data.status
nginxPreview.value = null
ElMessage.success(data.restart_required ? t('tls.restartRequired') : t('tls.saved'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.saveFailed')) }
finally { tlsSavingMode.value = false }
}
async function loadNginxPreview() {
nginxLoading.value = true
try {
const { data } = await api.get('/system/tls/nginx-preview')
nginxPreview.value = data
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.loadFailed')) }
finally { nginxLoading.value = false }
}
async function saveTLSDomain() {
tlsSaving.value = true
try {
const { data } = await api.put('/system/tls/domain', { domain: tlsDomain.value })
tlsStatus.value = data.status
nginxPreview.value = null
ElMessage.success(data.restart_required ? t('tls.restartRequired') : t('tls.saved'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.saveFailed')) }
finally { tlsSaving.value = false }
@@ -148,7 +298,7 @@ async function changePwd() {
}
onMounted(async () => {
if (auth.isAdmin() && !auth.mustChange) loadTLS()
if (auth.isAdmin() && !auth.mustChange) { loadTLS(); loadNginxSetup(); loadHostReadiness() }
try {
const status = await api.get('/mfa/status')
mfa.value.enabled = status.data.enabled
@@ -200,4 +350,5 @@ function copyBackup() {
background: var(--bg-card); border-radius: 4px;
}
.tls-hint { color: var(--text-secondary); line-height: 1.6; font-size: 13px; }
.nginx-preview { max-height: 300px; overflow: auto; background: #172239; color: #e7eefc; border-radius: 8px; padding: 14px; font-size: 12px; line-height: 1.5; }
</style>