From 4096b93da39016a80dc36fc0f6656a14ba4b9f81 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 23 Oct 2026 02:25:15 +0800 Subject: [PATCH] Clarify Feishu permissions and verify name lookup scope --- frontend/src/locales/en-US.json | 4 ++-- frontend/src/locales/zh-CN.json | 4 ++-- .../settings/components/IdentitySettings.vue | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index ecf9754..615bde3 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -577,8 +577,8 @@ }, "identity": { "tab": "Feishu identity", "title": "Built-in Feishu identity service", "description": "Issue OIDC identities for VPN using your existing HTTPS domain. Feishu app credentials stay upstream; this platform creates separate VPN client credentials.", - "guideTitle": "Feishu app setup", "guideApp": "Enter the enterprise app ID and secret; enable both permissions below.", "guidePermission": "", "guidePublish": "Publish the app and include VPN users in its availability scope.", "guideCallback": "Security Settings → Redirect URL:", "guideCallbackPending": "Configure an HTTPS domain first.", "guideVerify": "", "guideManual": "", "guideScope": "", - "tenantPermission": "tenant:tenant:readonly · enterprise", "userPermission": "contact:user.base:readonly · user", "checking": "Checking", "checked": "Verified", "checkFailed": "Check failed", "checkPending": "Not checked", "checkedByLogin": "Login verified", "loginPending": "Verify on first login", + "guideTitle": "Feishu app setup", "guideApp": "Enter the enterprise app ID and secret; grant the three scopes below as needed.", "guidePermission": "", "guidePublish": "Publish the app and include VPN users in its availability scope.", "guideCallback": "Security Settings → Redirect URL:", "guideCallbackPending": "Configure an HTTPS domain first.", "guideVerify": "", "guideManual": "", "guideScope": "", + "tenantPermission": "tenant:tenant:readonly · enterprise lookup", "userPermission": "contact:user.base:readonly · browser sign-in", "basicProfilePermission": "contact:user.basic_profile:readonly · name lookup by open_id (app identity)", "permissionSteps": "Grant the app identity scope and publish the app; after a member signs in, check their name below.", "openFeishuPermission": "Open Feishu permissions ↗", "checkNamePending": "Check name below", "checking": "Checking", "checked": "Verified", "checkFailed": "Check failed", "checkPending": "Not checked", "checkedByLogin": "Login verified", "loginPending": "Verify on first login", "appId": "Feishu App ID", "appSecret": "Feishu App Secret", "secretPlaceholder": "Leave blank if saved; enter a new secret to rotate", "tenant": "Allowed Feishu tenant_key", "tenantPlaceholder": "Fetched automatically or enter manually", "fetchTenant": "Check again", "fetchingTenant": "Checking ({seconds}s)…", "tenantFetched": "Enterprise permission verified", "tenantFailed": "Enterprise permission check failed", "tenantNotEnabled": "", "enabled": "Enable built-in issuer", "httpsOnly": "Use HTTPS to enter app credentials.", "openHttps": "Open HTTPS management site", "httpsRequired": "Configure and activate your public HTTPS site and certificate first.", "gatewayPending": "The identity provider is saved but Nginx does not yet route /id/; sync the gateway.", "addresses": "Addresses and client credentials", "issuer": "OIDC issuer", "feishuCallback": "Callback registered in Feishu", "vpnCallback": "VPN client callback", "clientId": "VPN Client ID", diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index 0306fc8..117909b 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -577,8 +577,8 @@ }, "identity": { "tab": "飞书身份", "title": "内置飞书身份服务", "description": "使用现有 HTTPS 域名签发 VPN 所需 OIDC 身份令牌。飞书 App 凭据仅用于飞书上游,VPN 客户端凭据由本平台生成。", - "guideTitle": "飞书应用准备", "guideApp": "填入企业自建应用的 App ID 和 App Secret;在权限管理开通下方两项权限。", "guidePermission": "", "guidePublish": "发布应用,并将 VPN 用户加入应用可用范围。", "guideCallback": "安全设置 → 重定向 URL:", "guideCallbackPending": "请先配置 HTTPS 域名。", "guideVerify": "", "guideManual": "", "guideScope": "", - "tenantPermission": "tenant:tenant:readonly · 企业信息", "userPermission": "contact:user.base:readonly · 用户信息", "checking": "检测中", "checked": "已验证", "checkFailed": "检测失败", "checkPending": "待检测", "checkedByLogin": "登录已验证", "loginPending": "首次登录后验证", + "guideTitle": "飞书应用准备", "guideApp": "填入企业自建应用的 App ID 和 App Secret;按需开通下方三项权限。", "guidePermission": "", "guidePublish": "发布应用,并将 VPN 用户加入应用可用范围。", "guideCallback": "安全设置 → 重定向 URL:", "guideCallbackPending": "请先配置 HTTPS 域名。", "guideVerify": "", "guideManual": "", "guideScope": "", + "tenantPermission": "tenant:tenant:readonly · 查询企业信息", "userPermission": "contact:user.base:readonly · 浏览器登录用户", "basicProfilePermission": "contact:user.basic_profile:readonly · 按 open_id 查询姓名(应用身份)", "permissionSteps": "在飞书开通应用身份权限并发布应用;成员登录后,到下方“飞书姓名查询”检查。", "openFeishuPermission": "打开飞书权限管理 ↗", "checkNamePending": "待检查姓名", "checking": "检测中", "checked": "已验证", "checkFailed": "检测失败", "checkPending": "待检测", "checkedByLogin": "登录已验证", "loginPending": "首次登录后验证", "appId": "飞书 App ID", "appSecret": "飞书 App Secret", "secretPlaceholder": "已保存可留空;更换密钥请填写", "tenant": "允许的飞书 tenant_key", "tenantPlaceholder": "自动读取,也可手填", "fetchTenant": "重新检测", "fetchingTenant": "正在检测({seconds} 秒)…", "tenantFetched": "企业信息权限已验证", "tenantFailed": "检测企业信息权限失败", "tenantNotEnabled": "", "enabled": "启用内置发行者", "httpsOnly": "请通过 HTTPS 管理入口填写密钥。", "openHttps": "打开 HTTPS 管理入口", "httpsRequired": "请先完成公网域名 HTTPS 证书和站点配置,再启用身份服务。", "gatewayPending": "身份服务已保存,但 Nginx 尚未转发 /id/;请同步公网入口。", "addresses": "接入地址与客户端凭据", "issuer": "OIDC 发行者", "feishuCallback": "登记到飞书应用的回调", "vpnCallback": "VPN 客户端回调", "clientId": "VPN Client ID", diff --git a/frontend/src/views/settings/components/IdentitySettings.vue b/frontend/src/views/settings/components/IdentitySettings.vue index c615234..7a8fc96 100644 --- a/frontend/src/views/settings/components/IdentitySettings.vue +++ b/frontend/src/views/settings/components/IdentitySettings.vue @@ -17,7 +17,9 @@
{{ $t('identity.tenantPermission') }}{{ tenantError ? $t('identity.checkFailed') : tenantLoading ? $t('identity.checking') : tenantChecked ? $t('identity.checked') : $t('identity.checkPending') }}
{{ $t('identity.userPermission') }}{{ userScopeVerified ? $t('identity.checkedByLogin') : $t('identity.loginPending') }}
+
{{ $t('identity.basicProfilePermission') }}{{ basicProfileVerified ? $t('identity.checked') : $t('identity.checkNamePending') }}
+

{{ $t('identity.permissionSteps') }} {{ $t('identity.openFeishuPermission') }}

{{ $t('identity.fetchTenant') }} @@ -69,6 +71,9 @@ const identityStatus = ref(null) const identityForm = ref({ enabled: false, app_id: '', app_secret: '', tenant_key: '' }) const identitySecure = window.location.protocol === 'https:' const identityHttpsUrl = computed(() => identityStatus.value?.issuer ? `${identityStatus.value.issuer.replace(/\/id$/, '')}/settings?tab=identity` : '') +const feishuPermissionUrl = computed(() => /^cli_[a-zA-Z0-9]+$/.test(identityForm.value.app_id) + ? `https://open.feishu.cn/app/${identityForm.value.app_id}/auth?q=contact:user.basic_profile:readonly&op_from=openapi&token_type=tenant` + : '') const identityLoading = ref(false) const identitySaving = ref(false) const identityGatewayLoading = ref(false) @@ -82,6 +87,7 @@ const profilesLoading = ref(false) const directoryLoading = ref(false) const directoryResult = ref('') const directoryAvailable = ref(false) +const basicProfileVerified = ref(false) const userScopeVerified = computed(() => !!identityStatus.value?.user_scope_verified && identityForm.value.app_id === identityStatus.value?.app_id && !identityForm.value.app_secret && identityForm.value.tenant_key === identityStatus.value?.tenant_key) const tenantSeconds = ref(0) let tenantTimer @@ -95,6 +101,7 @@ watch(() => [identityForm.value.app_id, identityForm.value.app_secret], ([appId, tenantLoading.value = false tenantChecked.value = false tenantError.value = '' + basicProfileVerified.value = false clearTimeout(identityCheckTimer) if ((appId !== oldId && appId !== identityStatus.value?.app_id) || (secret && secret !== oldSecret)) { identityForm.value.tenant_key = '' @@ -104,6 +111,7 @@ watch(() => [identityForm.value.app_id, identityForm.value.app_secret], ([appId, identityCheckTimer = setTimeout(() => fetchIdentityTenant(true), 700) } }) +watch(selectedProfile, () => { basicProfileVerified.value = false; directoryResult.value = '' }) async function fetchIdentityTenant(silent = false) { if (!identitySecure) return ElMessage.warning(t('identity.httpsOnly')) const request = ++tenantRequest @@ -139,6 +147,7 @@ async function loadIdentity() { tenantName.value = '' tenantError.value = '' tenantChecked.value = false + basicProfileVerified.value = false } catch (e) { ElMessage.error(e.response?.data?.error || t('identity.loadFailed')) } finally { identityLoading.value = false } } @@ -151,11 +160,14 @@ async function loadProfiles() { async function checkDirectory() { directoryLoading.value = true directoryResult.value = '' + const subject = selectedProfile.value try { - const { data } = await api.post('/system/identity/directory-check', { subject: selectedProfile.value }) + const { data } = await api.post('/system/identity/directory-check', { subject }) + if (subject !== selectedProfile.value) return directoryAvailable.value = data.available + basicProfileVerified.value = data.available && data.source === 'basic_batch' directoryResult.value = data.available ? t('feishuDirectory.found', { name: data.name }) + (data.source === 'contact_user' ? ` ${t('feishuDirectory.basicUnavailable')}` : '') : data.hint - } catch (e) { directoryAvailable.value = false; directoryResult.value = e.response?.data?.error || t('feishuDirectory.failed') } + } catch (e) { if (subject === selectedProfile.value) { directoryAvailable.value = false; basicProfileVerified.value = false; directoryResult.value = e.response?.data?.error || t('feishuDirectory.failed') } } finally { directoryLoading.value = false } } async function saveIdentity() {