Add opt-in built-in Feishu OIDC issuer for OpenVPN

This commit is contained in:
opsx committed 2026-10-22 09:47:54 +08:00
1 parent 6ca609fde4
commit 63cdda4ae1
23 files changed
+1244 -17

No files matched your search

+63 -2
View File
@@ -37,6 +37,35 @@
</el-card>
</div>
</el-tab-pane>
<el-tab-pane v-if="auth.isAdmin()" :label="$t('identity.tab')" name="identity">
<div class="settings-column settings-column-wide">
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('identity.title') }}</b></template>
<el-alert type="info" :closable="false" :title="$t('identity.description')" style="margin-bottom:16px" />
<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>
<el-form-item :label="$t('identity.tenant')"><el-input v-model.trim="identityForm.tenant_key" :placeholder="$t('identity.tenantPlaceholder')" maxlength="128" /></el-form-item>
<el-form-item :label="$t('identity.enabled')"><el-switch v-model="identityForm.enabled" :disabled="!identityStatus?.tls_ready" /></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>
<el-alert v-if="!identityStatus?.tls_ready" type="warning" :closable="false" :title="$t('identity.httpsRequired')" style="margin-top:16px" />
<el-alert v-if="identityStatus?.enabled && !identityStatus?.gateway_ready" type="warning" :closable="false" :title="$t('identity.gatewayPending')" style="margin-top:16px" />
</el-card>
<el-card class="page-card" shadow="never" style="margin-top:16px">
<template #header><b>{{ $t('identity.addresses') }}</b></template>
<el-descriptions :column="1" border>
<el-descriptions-item :label="$t('identity.issuer')">{{ identityStatus?.issuer || '—' }}</el-descriptions-item>
<el-descriptions-item :label="$t('identity.feishuCallback')">{{ identityStatus?.feishu_callback || '—' }}</el-descriptions-item>
<el-descriptions-item :label="$t('identity.vpnCallback')">{{ identityStatus?.vpn_callback || '—' }}</el-descriptions-item>
<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="!identityStatus?.tls_ready" @click="syncIdentityGateway">{{ $t('identity.syncGateway') }}</el-button>
</el-card>
</div>
</el-tab-pane>
<el-tab-pane v-if="auth.isAdmin()" :label="$t('settings.gatewayTab')" name="gateway">
<div class="settings-column settings-column-wide">
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
@@ -162,12 +191,44 @@ const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const activeTab = ref('security')
const visibleTab = (name) => auth.isAdmin() && ['gateway', 'host'].includes(name) ? name : 'security'
const visibleTab = (name) => auth.isAdmin() && ['identity', 'gateway', 'host'].includes(name) ? name : 'security'
watch(() => route.query.tab, name => { activeTab.value = visibleTab(name) }, { immediate: true })
function changeTab(name) {
if (route.query.tab !== name) router.replace({ path: '/settings', query: name === 'security' ? {} : { tab: name } })
}
const pwd = ref({ old_password: '', new_password: '', confirm: '' })
const identityStatus = ref(null)
const identityForm = ref({ enabled: false, app_id: '', app_secret: '', tenant_key: '' })
const identityLoading = ref(false)
const identitySaving = ref(false)
const identityGatewayLoading = ref(false)
async function loadIdentity() {
identityLoading.value = true
try {
const { data } = await api.get('/system/identity')
identityStatus.value = data
identityForm.value = { enabled: data.enabled, app_id: data.app_id || '', app_secret: '', tenant_key: data.tenant_key || '' }
} catch (e) { ElMessage.error(e.response?.data?.error || t('identity.loadFailed')) }
finally { identityLoading.value = false }
}
async function saveIdentity() {
identitySaving.value = true
try {
await api.put('/system/identity', identityForm.value)
await loadIdentity()
ElMessage.success(t('identity.saved'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('identity.saveFailed')) }
finally { identitySaving.value = false }
}
async function syncIdentityGateway() {
identityGatewayLoading.value = true
try {
await api.post('/system/identity/gateway')
await loadIdentity()
ElMessage.success(t('identity.gatewaySynced'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('identity.gatewayFailed')) }
finally { identityGatewayLoading.value = false }
}
const pwdLoading = ref(false)
const mfa = ref({ enabled: false, uri: '', code: '' })
const mfaCanvas = ref(null)
@@ -365,7 +426,7 @@ async function changePwd() {
}
onMounted(async () => {
if (auth.isAdmin() && !auth.mustChange) { loadTLS(); loadNginxSetup(); loadHostReadiness() }
if (auth.isAdmin() && !auth.mustChange) { loadTLS(); loadNginxSetup(); loadHostReadiness(); loadIdentity() }
try {
const status = await api.get('/mfa/status')
mfa.value.enabled = status.data.enabled