feat(frontend): public entry scheme/port and certificate dialog
Add gateway scheme/host/port with derived Base URL, certificate list plus upload/HTTP-01 dialog, soften identity https-only gate, and let OIDC guide inherit public base from readiness when available.
This commit is contained in:
1 parent
871b94486f
commit
ef838ad68b
6 files changed
+449
-141
No files matched your search
@@ -133,8 +133,14 @@ async function loadReadiness() {
|
||||
}
|
||||
const suggestedBaseUrl = computed(() => {
|
||||
const status = readiness.value
|
||||
return status?.https_management_ready && /^[a-z0-9.-]+$/i.test(status.management_domain || '')
|
||||
? `https://${status.management_domain}` : ''
|
||||
if (status?.public_base_url) return String(status.public_base_url).replace(/\/+$/, '')
|
||||
const domain = status?.management_domain || ''
|
||||
if (!/^[a-z0-9.-]+$/i.test(domain)) return ''
|
||||
const scheme = status?.management_scheme === 'http' ? 'http' : 'https'
|
||||
const port = Number(status?.management_port)
|
||||
const def = scheme === 'http' ? 80 : 443
|
||||
const portPart = Number.isFinite(port) && port > 0 && port !== def ? `:${port}` : ''
|
||||
return `${scheme}://${domain}${portPart}`
|
||||
})
|
||||
async function requestPort(from, direction, previous = null) {
|
||||
portLoading.value = true
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"never": "Never",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
"logout": "Logout",
|
||||
"batchOperation": "Batch Operation"
|
||||
"batchOperation": "Batch Operation",
|
||||
"copyFailed": "Copy failed"
|
||||
},
|
||||
"login": {
|
||||
"title": "VPN Management Platform",
|
||||
@@ -630,9 +631,9 @@
|
||||
"tenantFailed": "Enterprise permission check failed",
|
||||
"tenantNotEnabled": "",
|
||||
"enabled": "Enable built-in issuer",
|
||||
"httpsOnly": "Use HTTPS to enter app credentials.",
|
||||
"httpsOnly": "You are on HTTP. You can still configure identity; prefer the HTTPS public Base URL in production.",
|
||||
"openHttps": "Open HTTPS management site",
|
||||
"httpsRequired": "Configure and activate your public HTTPS site and certificate first.",
|
||||
"httpsRequired": "Built-in issuer needs a configured public entry; HTTP is allowed when scheme/port are set.",
|
||||
"gatewayPending": "The identity provider is saved but Nginx does not yet route /id/; sync the gateway.",
|
||||
"addresses": "Addresses and client credentials",
|
||||
"issuer": "OIDC issuer",
|
||||
@@ -939,7 +940,39 @@
|
||||
"hostRoute": "Default route",
|
||||
"hostForward": "IPv4 forwarding",
|
||||
"enableForwarding": "Enable IPv4 forwarding",
|
||||
"forwardingEnabled": "IPv4 forwarding enabled"
|
||||
"forwardingEnabled": "IPv4 forwarding enabled",
|
||||
"publicEntryTitle": "Public entry",
|
||||
"publicEntryDesc": "Scheme, host, and port define the public Base URL used by the OIDC issuer, Feishu identity, and tunnel callbacks.",
|
||||
"scheme": "Scheme",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"publicBase": "Public Base URL",
|
||||
"publicBaseHint": "OIDC issuer is Base URL + /id; tunnel callbacks inherit the same base.",
|
||||
"httpRisk": "HTTP is plaintext. Prefer HTTPS in production; HTTP is for intranet or temporary ICP workarounds.",
|
||||
"saveEntry": "Save public entry",
|
||||
"hostRequired": "Host is required",
|
||||
"certListTitle": "Certificates",
|
||||
"addCert": "Add certificate",
|
||||
"replaceCert": "Replace",
|
||||
"certSource": "Source",
|
||||
"sourceUpload": "Upload",
|
||||
"sourceHttp01": "HTTP-01",
|
||||
"noCerts": "No certificates yet",
|
||||
"renewalFailed": "Renewal failed",
|
||||
"methodUpload": "Upload existing certificate",
|
||||
"methodHttp01": "Request free certificate",
|
||||
"certPem": "Certificate",
|
||||
"keyPem": "Private key",
|
||||
"certPemPlaceholder": "Paste PEM certificate or upload .crt / .pem",
|
||||
"keyPemPlaceholder": "Paste PEM private key or upload .key",
|
||||
"uploadCertFile": "Upload certificate file (.crt)",
|
||||
"uploadKeyFile": "Upload private key file (.key)",
|
||||
"applyEmail": "Email",
|
||||
"multiDomainHint": "Domain name",
|
||||
"http01Hint": "Let’s Encrypt HTTP-01. Port 80 must be reachable. Auto-renewal status appears in the list.",
|
||||
"certKeyRequired": "Certificate and private key are required",
|
||||
"certUploaded": "Certificate uploaded",
|
||||
"uploadApiPending": "Upload API is not ready yet (POST /system/tls/certificate). Use HTTP-01 for now."
|
||||
},
|
||||
"feishuSync": {
|
||||
"title": "Feishu directory",
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"never": "不",
|
||||
"copiedToClipboard": "已复制到剪贴板",
|
||||
"logout": "退出登录",
|
||||
"batchOperation": "批量操作"
|
||||
"batchOperation": "批量操作",
|
||||
"copyFailed": "复制失败"
|
||||
},
|
||||
"login": {
|
||||
"title": "VPN 专线管理平台",
|
||||
@@ -630,9 +631,9 @@
|
||||
"tenantFailed": "检测企业信息权限失败",
|
||||
"tenantNotEnabled": "",
|
||||
"enabled": "启用内置发行者",
|
||||
"httpsOnly": "请通过 HTTPS 管理入口填写密钥。",
|
||||
"httpsOnly": "当前为 HTTP 明文访问管理台;可继续配置,正式环境建议使用公网入口的 HTTPS Base URL。",
|
||||
"openHttps": "打开 HTTPS 管理入口",
|
||||
"httpsRequired": "请先完成公网域名 HTTPS 证书和站点配置,再启用身份服务。",
|
||||
"httpsRequired": "内置发行者依赖公网入口已配置;http 场景下仍可启用,请确认 Base URL 与端口正确。",
|
||||
"gatewayPending": "身份服务已保存,但 Nginx 尚未转发 /id/;请同步公网入口。",
|
||||
"addresses": "接入地址与客户端凭据",
|
||||
"issuer": "OIDC 发行者",
|
||||
@@ -939,7 +940,39 @@
|
||||
"hostRoute": "默认出口",
|
||||
"hostForward": "IPv4 转发",
|
||||
"enableForwarding": "开启 IPv4 转发",
|
||||
"forwardingEnabled": "IPv4 转发已开启"
|
||||
"forwardingEnabled": "IPv4 转发已开启",
|
||||
"publicEntryTitle": "公网入口",
|
||||
"publicEntryDesc": "协议、域名与端口决定 OIDC 发行者、飞书身份与专线对外地址;保存后各页只读继承,不再写死 https。",
|
||||
"scheme": "协议",
|
||||
"host": "域名",
|
||||
"port": "端口",
|
||||
"publicBase": "对外 Base URL",
|
||||
"publicBaseHint": "飞书 / OIDC 发行者为 Base URL + /id;专线回调亦由此派生。",
|
||||
"httpRisk": "HTTP 为明文传输,仅建议内网或未备案临时绕行场景;生产环境优先 HTTPS。",
|
||||
"saveEntry": "保存公网入口",
|
||||
"hostRequired": "请填写域名",
|
||||
"certListTitle": "证书",
|
||||
"addCert": "添加证书",
|
||||
"replaceCert": "更换",
|
||||
"certSource": "来源",
|
||||
"sourceUpload": "上传",
|
||||
"sourceHttp01": "HTTP-01",
|
||||
"noCerts": "暂无证书,点击右上角添加",
|
||||
"renewalFailed": "续期失败",
|
||||
"methodUpload": "上传已有证书",
|
||||
"methodHttp01": "申请免费证书",
|
||||
"certPem": "证书",
|
||||
"keyPem": "私钥",
|
||||
"certPemPlaceholder": "粘贴 PEM 证书内容,或上传 .crt / .pem",
|
||||
"keyPemPlaceholder": "粘贴 PEM 私钥内容,或上传 .key",
|
||||
"uploadCertFile": "点击上传证书文件 (.crt)",
|
||||
"uploadKeyFile": "点击上传私钥文件 (.key)",
|
||||
"applyEmail": "申请邮箱",
|
||||
"multiDomainHint": "支持输入域名(主域名)",
|
||||
"http01Hint": "使用 Let’s Encrypt HTTP-01 验证;需公网可访问 80 端口。过期前将尝试自动续期,失败时列表会显示状态。",
|
||||
"certKeyRequired": "请填写证书与私钥",
|
||||
"certUploaded": "证书已上传",
|
||||
"uploadApiPending": "上传证书接口尚未就绪,请先由后端落地 POST /system/tls/certificate,或改用 HTTP-01 申请。"
|
||||
},
|
||||
"feishuSync": {
|
||||
"title": "飞书组织与成员",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/** Build public base URL from gateway scheme/host/port. */
|
||||
export function defaultPort(scheme) {
|
||||
return scheme === 'http' ? 80 : 443
|
||||
}
|
||||
|
||||
export function buildPublicBase({ scheme = 'https', host = '', port } = {}) {
|
||||
const s = scheme === 'http' ? 'http' : 'https'
|
||||
const h = String(host || '').trim().toLowerCase()
|
||||
if (!h) return ''
|
||||
const p = Number(port)
|
||||
const def = defaultPort(s)
|
||||
const usePort = Number.isFinite(p) && p > 0 && p !== def
|
||||
return `${s}://${h}${usePort ? `:${p}` : ''}`
|
||||
}
|
||||
|
||||
export function issuerFromPublicBase(base) {
|
||||
if (!base) return ''
|
||||
return `${String(base).replace(/\/+$/, '')}/id`
|
||||
}
|
||||
@@ -1,88 +1,189 @@
|
||||
<template>
|
||||
<div class="settings-column settings-column-wide">
|
||||
<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" />
|
||||
<div class="settings-column settings-column-wide">
|
||||
<!-- 公网入口契约 -->
|
||||
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<b>{{ $t('tls.publicEntryTitle') }}</b>
|
||||
<el-button link :loading="tlsLoading" @click="refreshAll">{{ $t('tls.refresh') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-alert type="info" :closable="false" :title="$t('tls.publicEntryDesc')" style="margin-bottom:16px" />
|
||||
|
||||
<el-form label-position="top" @submit.prevent="savePublicEntry">
|
||||
<el-form-item :label="$t('tls.scheme')">
|
||||
<el-radio-group v-model="entry.scheme" class="scheme-group" @change="onSchemeChange">
|
||||
<el-radio-button value="https">HTTPS</el-radio-button>
|
||||
<el-radio-button value="http">HTTP</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
v-if="entry.scheme === 'http'"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="$t('tls.httpRisk')"
|
||||
style="margin-bottom:12px"
|
||||
/>
|
||||
<el-row :gutter="12">
|
||||
<el-col :xs="24" :sm="16">
|
||||
<el-form-item :label="$t('tls.host')" required>
|
||||
<el-input v-model.trim="entry.host" placeholder="vpn.example.com" maxlength="253" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('tls.port')">
|
||||
<el-input-number v-model="entry.port" :min="1" :max="65535" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('tls.publicBase')">
|
||||
<el-input :model-value="publicBase" readonly>
|
||||
<template #append>
|
||||
<el-button @click="copyBase">{{ $t('common.copy') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="tls-hint">{{ $t('tls.publicBaseHint') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('tls.mode')">
|
||||
<el-select v-model="entry.mode" 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 type="primary" :loading="tlsSaving" @click="savePublicEntry">{{ $t('tls.saveEntry') }}</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-alert v-if="tlsStatus?.restart_required" type="warning" :closable="false" :title="$t('tls.restartRequired')" style="margin-top:12px" />
|
||||
<el-alert v-if="tlsStatus?.management_direct_public" type="warning" :closable="false" :title="$t('tls.directPortWarning')" style="margin-top:12px" />
|
||||
</el-card>
|
||||
|
||||
<!-- 证书列表 -->
|
||||
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<b>{{ $t('tls.certListTitle') }}</b>
|
||||
<el-button type="primary" @click="openAddCert">{{ $t('tls.addCert') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="certRows" size="default" v-loading="tlsLoading">
|
||||
<el-table-column :label="$t('tls.host')" min-width="160">
|
||||
<template #default="{ row }">{{ row.host || '—' }}</template>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tls.certificate')" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.valid ? 'success' : 'warning'">{{ row.valid ? $t('tls.valid') : row.pending ? $t('tls.pending') : $t('tls.notConfigured') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tls.expires')" width="170">
|
||||
<template #default="{ row }">{{ row.expires ? new Date(row.expires).toLocaleString() : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tls.renewal')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.renewalLabel }}</span>
|
||||
<el-tag v-if="row.renewalFailed" type="danger" size="small" style="margin-left:6px">{{ $t('tls.renewalFailed') }}</el-tag>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :description="$t('tls.noCerts')" /></template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- Nginx 准备(折叠) -->
|
||||
<el-collapse v-if="entry.mode === 'nginx'">
|
||||
<el-collapse-item :title="$t('tls.setupTitle')" name="setup">
|
||||
<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')" />
|
||||
</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>
|
||||
<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" />
|
||||
<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?.management_direct_public" type="warning" :closable="false" :title="$t('tls.directPortWarning')" 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.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>
|
||||
</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') : 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 || (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="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>
|
||||
</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-button style="margin-top:12px" :loading="nginxLoading" :disabled="!entry.host" @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-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<!-- 添加证书弹窗 -->
|
||||
<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'">
|
||||
<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'">
|
||||
<el-radio v-model="certForm.method" value="http01">{{ $t('tls.methodHttp01') }}</el-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="certForm.method === 'upload'">
|
||||
<el-form label-position="top" style="margin-top:16px">
|
||||
<el-form-item :label="$t('tls.certPem')" required>
|
||||
<el-input v-model="certForm.certPem" type="textarea" :rows="6" :placeholder="$t('tls.certPemPlaceholder')" />
|
||||
<el-upload :show-file-list="false" accept=".crt,.pem,.cer" :before-upload="(f) => readPemFile(f, 'certPem')">
|
||||
<el-button class="upload-btn" style="width:100%;margin-top:8px">{{ $t('tls.uploadCertFile') }}</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('tls.keyPem')" required>
|
||||
<el-input v-model="certForm.keyPem" type="textarea" :rows="6" :placeholder="$t('tls.keyPemPlaceholder')" />
|
||||
<el-upload :show-file-list="false" accept=".key,.pem" :before-upload="(f) => readPemFile(f, 'keyPem')">
|
||||
<el-button class="upload-btn" style="width:100%;margin-top:8px">{{ $t('tls.uploadKeyFile') }}</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-form label-position="top" style="margin-top:16px">
|
||||
<el-form-item :label="$t('tls.host')" required>
|
||||
<el-input v-model.trim="certForm.host" :placeholder="$t('tls.multiDomainHint')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('tls.applyEmail')">
|
||||
<el-input v-model.trim="certForm.email" placeholder="admin@example.com" />
|
||||
</el-form-item>
|
||||
<el-alert type="info" :closable="false" :title="$t('tls.http01Hint')" />
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="certDialog = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="certSubmitting" @click="submitCert">{{ $t('common.submit') }}</el-button>
|
||||
</template>
|
||||
<pre v-if="nginxActivateLog" ref="nginxActivateLogView" class="nginx-preview" role="log" aria-live="polite" style="margin-top:12px">{{ nginxActivateLog }}</pre>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api, { auth } from '../../../api'
|
||||
import { buildPublicBase, defaultPort } from '../../../utils/publicBase'
|
||||
|
||||
const { t } = useI18n()
|
||||
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 nginxSetup = ref(null)
|
||||
@@ -90,51 +191,80 @@ 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)
|
||||
const certDialog = ref(false)
|
||||
const certSubmitting = ref(false)
|
||||
|
||||
async function activateNginx() {
|
||||
nginxActivating.value = true
|
||||
const entry = reactive({
|
||||
scheme: 'https',
|
||||
host: '',
|
||||
port: 443,
|
||||
mode: 'autocert',
|
||||
})
|
||||
|
||||
const certForm = reactive({
|
||||
method: 'upload',
|
||||
certPem: '',
|
||||
keyPem: '',
|
||||
host: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const publicBase = computed(() => buildPublicBase(entry))
|
||||
|
||||
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'))
|
||||
return [{
|
||||
host,
|
||||
source,
|
||||
valid: !!s.certificate_valid,
|
||||
pending: !!s.enabled && !s.certificate_valid,
|
||||
expires: s.not_after || null,
|
||||
renewalFailed: !!s.renewal_failed,
|
||||
renewalLabel: s.enabled ? t('tls.automatic') : s.mode === 'nginx' ? t('tls.externalRenewal') : '—',
|
||||
}]
|
||||
})
|
||||
|
||||
function onSchemeChange() {
|
||||
entry.port = defaultPort(entry.scheme)
|
||||
}
|
||||
|
||||
function resetCertForm() {
|
||||
certForm.method = 'upload'
|
||||
certForm.certPem = ''
|
||||
certForm.keyPem = ''
|
||||
certForm.host = entry.host
|
||||
certForm.email = ''
|
||||
nginxActivateLog.value = ''
|
||||
let completed = false
|
||||
}
|
||||
|
||||
function openAddCert() {
|
||||
resetCertForm()
|
||||
certDialog.value = true
|
||||
}
|
||||
|
||||
function readPemFile(file, field) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => { certForm[field] = String(reader.result || '') }
|
||||
reader.readAsText(file)
|
||||
return false
|
||||
}
|
||||
|
||||
async function copyBase() {
|
||||
if (!publicBase.value) return
|
||||
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()])
|
||||
}
|
||||
await navigator.clipboard.writeText(publicBase.value)
|
||||
ElMessage.success(t('common.copied'))
|
||||
} catch { ElMessage.error(t('common.copyFailed')) }
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([loadTLS(), loadNginxSetup()])
|
||||
}
|
||||
|
||||
async function loadNginxSetup() {
|
||||
@@ -152,7 +282,7 @@ async function initializeNginx() {
|
||||
let completed = false
|
||||
try {
|
||||
const response = await fetch('/api/system/tls/nginx-setup', {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${auth.token}` }
|
||||
method: 'POST', headers: { Authorization: `Bearer ${auth.token}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}))
|
||||
@@ -194,21 +324,32 @@ async function loadTLS() {
|
||||
try {
|
||||
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'
|
||||
entry.host = data.configured_domain || data.domain || data.host || ''
|
||||
entry.scheme = data.scheme === 'http' ? 'http' : 'https'
|
||||
entry.port = Number(data.port) > 0 ? Number(data.port) : defaultPort(entry.scheme)
|
||||
entry.mode = 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
|
||||
async function savePublicEntry() {
|
||||
if (!entry.host) return ElMessage.warning(t('tls.hostRequired'))
|
||||
tlsSaving.value = true
|
||||
try {
|
||||
const { data } = await api.put('/system/tls/mode', { mode: tlsMode.value })
|
||||
tlsStatus.value = data.status
|
||||
// Forward-compatible: backend may only honor domain until scheme/port land.
|
||||
const { data: domainRes } = await api.put('/system/tls/domain', {
|
||||
domain: entry.host,
|
||||
host: entry.host,
|
||||
scheme: entry.scheme,
|
||||
port: entry.port,
|
||||
})
|
||||
tlsStatus.value = domainRes.status
|
||||
const { data: modeRes } = await api.put('/system/tls/mode', { mode: entry.mode })
|
||||
tlsStatus.value = modeRes.status || domainRes.status
|
||||
nginxPreview.value = null
|
||||
ElMessage.success(data.restart_required ? t('tls.restartRequired') : t('tls.saved'))
|
||||
ElMessage.success((domainRes.restart_required || modeRes.restart_required) ? t('tls.restartRequired') : t('tls.saved'))
|
||||
} catch (e) { ElMessage.error(e.response?.data?.error || t('tls.saveFailed')) }
|
||||
finally { tlsSavingMode.value = false }
|
||||
finally { tlsSaving.value = false }
|
||||
}
|
||||
|
||||
async function loadNginxPreview() {
|
||||
@@ -220,24 +361,100 @@ async function loadNginxPreview() {
|
||||
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 }
|
||||
async function streamActivate() {
|
||||
nginxActivateLog.value = ''
|
||||
let completed = false
|
||||
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'))
|
||||
}
|
||||
|
||||
onMounted(() => { if (!auth.mustChange) { loadTLS(); loadNginxSetup() } })
|
||||
async function submitCert() {
|
||||
certSubmitting.value = true
|
||||
try {
|
||||
if (certForm.method === 'upload') {
|
||||
if (!certForm.certPem.trim() || !certForm.keyPem.trim()) {
|
||||
ElMessage.warning(t('tls.certKeyRequired'))
|
||||
return
|
||||
}
|
||||
await api.post('/system/tls/certificate', {
|
||||
certificate_pem: certForm.certPem,
|
||||
private_key_pem: certForm.keyPem,
|
||||
host: entry.host || certForm.host,
|
||||
})
|
||||
ElMessage.success(t('tls.certUploaded'))
|
||||
certDialog.value = false
|
||||
await loadTLS()
|
||||
return
|
||||
}
|
||||
// HTTP-01: ensure domain saved, nginx mode, then activate
|
||||
const host = (certForm.host || entry.host || '').trim()
|
||||
if (!host) return ElMessage.warning(t('tls.hostRequired'))
|
||||
entry.host = host.split(/[\s,]+/)[0]
|
||||
await api.put('/system/tls/domain', {
|
||||
domain: entry.host, host: entry.host, scheme: entry.scheme, port: entry.port,
|
||||
})
|
||||
if (entry.mode !== 'nginx') {
|
||||
await api.put('/system/tls/mode', { mode: 'nginx' })
|
||||
entry.mode = 'nginx'
|
||||
}
|
||||
await streamActivate()
|
||||
ElMessage.success(t('tls.siteReady'))
|
||||
certDialog.value = false
|
||||
await refreshAll()
|
||||
} catch (e) {
|
||||
const msg = e.response?.data?.error || e.message || t('tls.siteFailed')
|
||||
if (certForm.method === 'upload' && (e.response?.status === 404 || e.response?.status === 405)) {
|
||||
ElMessage.error(t('tls.uploadApiPending'))
|
||||
} else {
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
} finally { certSubmitting.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { if (!auth.mustChange) refreshAll() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-column { max-width: 850px; }
|
||||
.settings-column { max-width: 920px; }
|
||||
.settings-column :deep(.page-card) { border-color: #e6ecf3; box-shadow: none; }
|
||||
.settings-column :deep(.el-card__header) { background: #f9fbfe; color: #21344e; }
|
||||
.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; }
|
||||
.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; }
|
||||
.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-card {
|
||||
border: 1px solid var(--el-border-color); border-radius: 10px; padding: 14px 16px; cursor: pointer;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.method-card.active { border-color: var(--el-color-primary); box-shadow: 0 0 0 1px var(--el-color-primary) inset; }
|
||||
.upload-btn { color: var(--el-color-primary); border-style: dashed; }
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
<el-alert type="error" :closable="false" :title="$t('feishuSync.publishAll')" show-icon style="margin-bottom:16px" />
|
||||
<el-alert v-if="!identitySecure" type="warning" :closable="false" :title="$t('identity.httpsOnly')" style="margin-bottom:16px" />
|
||||
<el-button v-if="!identitySecure && identityHttpsUrl" type="primary" tag="a" :href="identityHttpsUrl" style="margin-bottom:16px">{{ $t('identity.openHttps') }}</el-button>
|
||||
<el-form v-if="identitySecure" label-position="top" @submit.prevent="saveIdentity">
|
||||
<el-form label-position="top" @submit.prevent="saveIdentity">
|
||||
<el-form-item :label="$t('identity.appId')"><el-input v-model.trim="identityForm.app_id" placeholder="cli_..." maxlength="128" /></el-form-item>
|
||||
<el-form-item :label="$t('identity.appSecret')"><el-input v-model="identityForm.app_secret" type="password" show-password autocomplete="new-password" :placeholder="$t('identity.secretPlaceholder')" /></el-form-item>
|
||||
<div class="identity-checks" aria-live="polite">
|
||||
@@ -27,7 +27,7 @@
|
||||
<span v-if="tenantLoading" class="tenant-progress" role="status" aria-live="polite"><el-icon class="is-loading"><Loading /></el-icon>{{ $t('identity.fetchingTenant', { seconds: tenantSeconds }) }}</span>
|
||||
</el-form-item>
|
||||
<el-alert v-if="tenantError" type="warning" :closable="false" :title="tenantError" style="margin-bottom:16px" />
|
||||
<el-form-item :label="$t('identity.enabled')"><el-switch v-model="identityForm.enabled" :disabled="!identityStatus?.tls_ready" /></el-form-item>
|
||||
<el-form-item :label="$t('identity.enabled')"><el-switch v-model="identityForm.enabled" /></el-form-item>
|
||||
<el-button type="primary" :loading="identitySaving" @click="saveIdentity">{{ $t('common.save') }}</el-button>
|
||||
<el-button :loading="identityLoading" @click="loadIdentity">{{ $t('tls.refresh') }}</el-button>
|
||||
</el-form>
|
||||
@@ -43,9 +43,9 @@
|
||||
<el-descriptions-item :label="$t('identity.clientId')">{{ identityStatus?.client_id || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<p class="tls-hint">{{ $t('identity.routeHint') }}</p>
|
||||
<el-button :loading="identityGatewayLoading" :disabled="!identitySecure || !identityStatus?.tls_ready" @click="syncIdentityGateway">{{ $t('identity.syncGateway') }}</el-button>
|
||||
<el-button :loading="identityGatewayLoading" :disabled="!identityStatus?.enabled" @click="syncIdentityGateway">{{ $t('identity.syncGateway') }}</el-button>
|
||||
</el-card>
|
||||
<el-card v-if="identityStatus?.enabled && identitySecure" class="page-card" shadow="never" style="margin-top:16px">
|
||||
<el-card v-if="identityStatus?.enabled" class="page-card" shadow="never" style="margin-top:16px">
|
||||
<template #header><b>{{ $t('feishuDirectory.title') }}</b></template>
|
||||
<p class="tls-hint">{{ $t('feishuDirectory.hint') }}</p>
|
||||
<div class="directory-check">
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<el-alert v-if="directoryResult" :title="directoryResult" :type="directoryAvailable ? 'success' : 'warning'" :closable="false" style="margin-top:12px" />
|
||||
</el-card>
|
||||
<el-card v-if="identityStatus?.enabled && identitySecure" class="page-card" shadow="never" style="margin-top:16px">
|
||||
<el-card v-if="identityStatus?.enabled" class="page-card" shadow="never" style="margin-top:16px">
|
||||
<template #header><b>{{ $t('feishuSync.title') }}</b></template>
|
||||
<p class="tls-hint">{{ $t('feishuSync.hint') }}</p>
|
||||
<p class="tls-hint">{{ $t('feishuSync.permissions') }}</p>
|
||||
@@ -175,7 +175,7 @@ watch(() => [identityForm.value.app_id, identityForm.value.app_secret], ([appId,
|
||||
})
|
||||
watch(selectedProfile, () => { basicProfileVerified.value = false; directoryResult.value = '' })
|
||||
async function fetchIdentityTenant(silent = false) {
|
||||
if (!identitySecure) return ElMessage.warning(t('identity.httpsOnly'))
|
||||
if (!identitySecure) ElMessage.warning(t('identity.httpsOnly'))
|
||||
const request = ++tenantRequest
|
||||
clearInterval(tenantTimer)
|
||||
tenantError.value = ''
|
||||
@@ -204,7 +204,7 @@ async function loadIdentity() {
|
||||
try {
|
||||
const { data } = await api.get('/system/identity')
|
||||
identityStatus.value = data
|
||||
if (data.enabled && identitySecure) { loadProfiles(); loadSync(); checkScope() }
|
||||
if (data.enabled) { loadProfiles(); loadSync(); checkScope() }
|
||||
else { scopeRequest++; scopeResult.value = null; scopeError.value = '' }
|
||||
identityForm.value = { enabled: data.enabled, app_id: data.app_id || '', app_secret: '', tenant_key: data.tenant_key || '' }
|
||||
tenantName.value = ''
|
||||
@@ -278,7 +278,7 @@ function memberDepartments(value) {
|
||||
try { return JSON.parse(value || '[]').join(', ') || '—' } catch { return '—' }
|
||||
}
|
||||
async function saveIdentity() {
|
||||
if (!identitySecure) return ElMessage.warning(t('identity.httpsOnly'))
|
||||
if (!identitySecure) ElMessage.warning(t('identity.httpsOnly'))
|
||||
identitySaving.value = true
|
||||
try {
|
||||
await api.put('/system/identity', identityForm.value)
|
||||
|
||||
Reference in new issue
Block a user