Check Feishu directory scope and highlight app availability

This commit is contained in:
Codex committed 2026-10-23 14:35:09 +08:00
1 parent bf9e1c3c2c
commit e2597719c8
7 files changed
+147 -4

No files matched your search

+1 -1
View File
@@ -794,7 +794,7 @@
"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"
},
"feishuSync": {
"title": "Feishu directory", "hint": "Import manually or every 30 minutes when enabled. Imported members do not receive VPN access or tunnel grants. Names are shown exactly as returned by Feishu.", "permissions": "Grant app identity access to department and user lists, publish the app, and set its directory visibility to the intended departments and members.", "auto": "Auto sync (30 min)", "run": "Sync now", "lastSuccess": "Last success", "counts": "Departments {departments} · members {members}", "visibility": "App-visible scope: {departments} department roots · {users} individually visible users (not the enterprise total)", "noDepartments": "Only individually visible members were returned. Grant department read access and expand the app's directory visibility in Feishu, publish, then sync again.", "name": "Name", "departments": "Department IDs", "firstMembers": "Showing the first 500 members", "saved": "Sync settings saved", "started": "Sync started; watch progress below", "done": "Directory sync complete", "failed": "Directory sync failed"
"title": "Feishu directory", "publishAll": "Publish the app in Feishu and set its availability to All Employees. Also grant directory visibility for the departments to import.", "hint": "Import manually or every 30 minutes when enabled. Imported members do not receive VPN access or tunnel grants. Names are shown exactly as returned by Feishu.", "permissions": "Grant app identity access to department and user lists, publish the app, and set its directory visibility to the intended departments and members.", "liveCheck": "Live permission check", "recheck": "Check again", "checking": "Checking Feishu access and visibility…", "checkFailed": "Could not check Feishu permissions", "liveVisibility": "App API visibility: {departments} department roots, {users} individual users (not the enterprise total).", "scopeMissing": "No visible departments. Include departments in the app directory visibility and publish. Department and user-list permissions cannot yet be verified.", "permissionMissing": "Department endpoint: {department}; user list endpoint: {users}. Grant the required app identity scopes and publish.", "apiReady": "Department and user-list endpoints work. Verify All Employees and publication in the Feishu console.", "ok": "Available", "auto": "Auto sync (30 min)", "run": "Sync now", "lastSuccess": "Last success", "counts": "Departments {departments} · members {members}", "visibility": "Last sync scope: {departments} department roots · {users} individual users", "noDepartments": "Only individually visible members were returned. Grant department read access and expand the app's directory visibility in Feishu, publish, then sync again.", "name": "Name", "departments": "Department IDs", "firstMembers": "Showing the first 500 members", "saved": "Sync settings saved", "started": "Sync started; watch progress below", "done": "Directory sync complete", "failed": "Directory sync failed"
},
"feishuDirectory": {
"title": "Feishu directory lookup",
+1 -1
View File
@@ -794,7 +794,7 @@
"commonSetupTitle": "首次引导 · 通用网络环境", "commonSetupHint": "检查服务权限、默认出口和 IPv4 转发。仅按需开启通用转发;各专线协议的依赖在“创建专线”中分别安装。", "hostRoot": "运行权限", "hostRoute": "默认出口", "hostForward": "IPv4 转发", "enableForwarding": "开启 IPv4 转发", "forwardingEnabled": "IPv4 转发已开启"
},
"feishuSync": {
"title": "飞书组织与成员", "hint": "手动导入,或开启后每 30 分钟同步一次。导入不会自动开通 VPN 或授予专线权限;仅显示飞书返回的姓名。", "permissions": "需应用身份读取部门及直属成员(如 contact:department.base:readonly、contact:user.department:readonly);发布应用,并设置通讯录可见范围。具体权限以飞书接口报错为准。", "auto": "自动同步(30 分钟)", "run": "立即同步", "lastSuccess": "上次成功", "counts": "部门 {departments} · 成员 {members}", "visibility": "飞书应用当前可见范围:部门入口 {departments} · 单独成员 {users}(与企业通讯录总人数不同)", "noDepartments": "当前只读到单独授权的成员,没有可见部门。请在飞书配置应用的部门读取权限和通讯录可见范围,发布后重新同步。", "name": "姓名", "departments": "部门 ID", "firstMembers": "仅显示前 500 位成员", "saved": "同步设置已保存", "started": "已开始同步,可查看下方进度", "done": "通讯录同步完成", "failed": "通讯录同步失败"
"title": "飞书组织与成员", "publishAll": "必须在飞书开放平台发布应用,并将应用可用范围改为“全体员工”;同时确保通讯录权限和可见范围覆盖需要同步的部门。", "hint": "手动导入,或开启后每 30 分钟同步一次。导入不会自动开通 VPN 或授予专线权限;仅显示飞书返回的姓名。", "permissions": "需应用身份读取部门及直属成员(如 contact:department.base:readonly、contact:user.department:readonly);发布应用,并设置通讯录可见范围。具体权限以飞书接口报错为准。", "liveCheck": "实时权限检查", "recheck": "重新检测", "checking": "正在检查飞书权限与可见范围…", "checkFailed": "飞书权限检查失败", "liveVisibility": "应用 API 可见:部门入口 {departments} 个,单独成员 {users} 位(不是企业总人数)。", "scopeMissing": "没有可见部门;请在飞书应用的通讯录权限管理中开放部门范围并发布。此时无法确认部门及直属成员接口是否有权限。", "permissionMissing": "部门接口:{department};成员列表接口:{users}。请按错误码补充应用身份权限并发布。", "apiReady": "部门与成员列表接口可用;“全体员工”和发布状态仍需在飞书开放平台核对。", "ok": "可用", "auto": "自动同步(30 分钟)", "run": "立即同步", "lastSuccess": "上次成功", "counts": "部门 {departments} · 成员 {members}", "visibility": "上次同步可见范围:部门入口 {departments} · 单独成员 {users}", "noDepartments": "当前只读到单独授权的成员,没有可见部门。请在飞书配置应用的部门读取权限和通讯录可见范围,发布后重新同步。", "name": "姓名", "departments": "部门 ID", "firstMembers": "仅显示前 500 位成员", "saved": "同步设置已保存", "started": "已开始同步,可查看下方进度", "done": "通讯录同步完成", "failed": "通讯录同步失败"
},
"feishuDirectory": {
"title": "飞书姓名查询",
@@ -9,6 +9,7 @@
<p>{{ $t('identity.guidePublish') }}</p>
<p>{{ $t('identity.guideCallback') }} <code v-if="identityStatus?.feishu_callback">{{ identityStatus.feishu_callback }}</code><span v-else>{{ $t('identity.guideCallbackPending') }}</span></p>
</div>
<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">
@@ -60,6 +61,20 @@
<template #header><b>{{ $t('feishuSync.title') }}</b></template>
<p class="tls-hint">{{ $t('feishuSync.hint') }}</p>
<p class="tls-hint">{{ $t('feishuSync.permissions') }}</p>
<div class="directory-check" style="align-items:center;margin:12px 0">
<b>{{ $t('feishuSync.liveCheck') }}</b>
<el-button :loading="scopeChecking" size="small" @click="checkScope">{{ $t('feishuSync.recheck') }}</el-button>
</div>
<el-alert v-if="scopeError" type="error" :closable="false" :title="scopeError" style="margin-bottom:12px" />
<el-alert v-else-if="scopeChecking" type="info" :closable="false" :title="$t('feishuSync.checking')" style="margin-bottom:12px" />
<template v-else-if="scopeResult">
<el-alert :type="scopeResult.visible_departments ? 'info' : 'error'" :closable="false"
:title="$t('feishuSync.liveVisibility', { departments: scopeResult.visible_departments, users: scopeResult.visible_users })" style="margin-bottom:12px" />
<el-alert v-if="!scopeResult.visible_departments" type="error" :closable="false" :title="$t('feishuSync.scopeMissing')" style="margin-bottom:12px" />
<el-alert v-else-if="scopeResult.department_access !== 'ok' || scopeResult.user_list_access !== 'ok'" type="error" :closable="false"
:title="$t('feishuSync.permissionMissing', { department: scopeResult.department_error || $t('feishuSync.ok'), users: scopeResult.user_list_error || $t('feishuSync.ok') })" style="margin-bottom:12px" />
<el-alert v-else type="success" :closable="false" :title="$t('feishuSync.apiReady')" style="margin-bottom:12px" />
</template>
<div class="directory-check">
<el-switch :model-value="syncStatus.enabled || false" :loading="syncSaving" :active-text="$t('feishuSync.auto')" @change="setSyncEnabled" />
<el-button :loading="syncRunning" @click="runSync">{{ $t('feishuSync.run') }}</el-button>
@@ -128,6 +143,10 @@ const syncError = ref('')
const syncLoading = ref(false)
const syncSaving = ref(false)
const syncRunning = ref(false)
const scopeResult = ref(null)
const scopeError = ref('')
const scopeChecking = ref(false)
let scopeRequest = 0
let syncPollTimer
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)
@@ -135,7 +154,7 @@ const tenantSeconds = ref(0)
let tenantTimer
let identityCheckTimer
let tenantRequest = 0
onUnmounted(() => { clearInterval(tenantTimer); clearTimeout(identityCheckTimer); clearTimeout(syncPollTimer); tenantRequest++ })
onUnmounted(() => { clearInterval(tenantTimer); clearTimeout(identityCheckTimer); clearTimeout(syncPollTimer); tenantRequest++; scopeRequest++ })
watch(() => [identityForm.value.app_id, identityForm.value.app_secret], ([appId, secret], [oldId, oldSecret] = []) => {
if (appId === oldId && secret === oldSecret) return
tenantRequest++
@@ -184,7 +203,8 @@ async function loadIdentity() {
try {
const { data } = await api.get('/system/identity')
identityStatus.value = data
if (data.enabled && identitySecure) { loadProfiles(); loadSync() }
if (data.enabled && identitySecure) { 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 = ''
tenantError.value = ''
@@ -226,6 +246,18 @@ async function loadSync() {
} catch (e) { syncError.value = e.response?.data?.error || t('feishuSync.failed') }
finally { syncLoading.value = false }
}
async function checkScope() {
const request = ++scopeRequest
scopeChecking.value = true
scopeError.value = ''
scopeResult.value = null
try {
const { data } = await api.get('/system/identity/directory-sync/check')
if (request === scopeRequest) scopeResult.value = data
} catch (e) {
if (request === scopeRequest) scopeError.value = e.response?.data?.error || t('feishuSync.checkFailed')
} finally { if (request === scopeRequest) scopeChecking.value = false }
}
async function setSyncEnabled(enabled) {
syncSaving.value = true
try {