diff --git a/frontend/src/components/OpenVPNOIDCGuide.vue b/frontend/src/components/OpenVPNOIDCGuide.vue index 6bb967d..779204a 100644 --- a/frontend/src/components/OpenVPNOIDCGuide.vue +++ b/frontend/src/components/OpenVPNOIDCGuide.vue @@ -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 diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 9108ae2..a90abad 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -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", diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index a6756f8..320607b 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -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": "飞书组织与成员", diff --git a/frontend/src/utils/publicBase.js b/frontend/src/utils/publicBase.js new file mode 100644 index 0000000..5d9b734 --- /dev/null +++ b/frontend/src/utils/publicBase.js @@ -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` +} diff --git a/frontend/src/views/settings/components/GatewaySettings.vue b/frontend/src/views/settings/components/GatewaySettings.vue index 9448b01..8bf62d5 100644 --- a/frontend/src/views/settings/components/GatewaySettings.vue +++ b/frontend/src/views/settings/components/GatewaySettings.vue @@ -1,88 +1,189 @@ diff --git a/frontend/src/views/settings/components/IdentitySettings.vue b/frontend/src/views/settings/components/IdentitySettings.vue index 1b685b4..2ebabef 100644 --- a/frontend/src/views/settings/components/IdentitySettings.vue +++ b/frontend/src/views/settings/components/IdentitySettings.vue @@ -11,7 +11,7 @@ {{ $t('identity.openHttps') }} - +
@@ -27,7 +27,7 @@ {{ $t('identity.fetchingTenant', { seconds: tenantSeconds }) }} - + {{ $t('common.save') }} {{ $t('tls.refresh') }} @@ -43,9 +43,9 @@ {{ identityStatus?.client_id || '—' }}

{{ $t('identity.routeHint') }}

- {{ $t('identity.syncGateway') }} + {{ $t('identity.syncGateway') }} - +

{{ $t('feishuDirectory.hint') }}

@@ -56,7 +56,7 @@
- +

{{ $t('feishuSync.hint') }}

{{ $t('feishuSync.permissions') }}

@@ -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)