fix: 修复CSP白屏并优化LDAP配置

This commit is contained in:
Logan.Li committed 2026-07-25 22:28:16 +08:00
1 parent a14d9e62e6
commit c7f59f2cbf
12 files changed
+930 -70

No files matched your search

+20 -10
View File
@@ -73,7 +73,7 @@
"passwordLabel": "Password",
"passwordPlaceholder": "Enter password",
"loginButton": "Sign In",
"defaultAccount": "Default account: <strong>admin / admin123</strong> — please change password after first login",
"defaultAccount": "Default account: {account} — please change password after first login",
"errorEmpty": "Please enter username and password",
"loginSuccess": "Login successful",
"changePasswordFirst": "Please change your password first",
@@ -279,14 +279,22 @@
"roleOperator": "Operator",
"roleViewer": "Viewer",
"noUsers": "No users yet",
"ldapTitle": "LDAP Identity Source",
"ldapTitle": "LDAP / Active Directory",
"ldapHint": "LDAP users can be auto-synced on first login, or pre-imported using the \"LDAP Import\" on the left.",
"ldapUrl": "LDAP URL",
"bindDn": "Bind DN",
"bindPassword": "Bind Password",
"ldapConfigHint": "Configure an LDAP server for centralized identity authentication",
"ldapEnabled": "Enable LDAP",
"ldapServerAddress": "LDAP Server Address",
"ldapServerAddressPlaceholder": "e.g. 8.217.205.92 or ldap.example.com",
"ldapPort": "Port",
"ldapProtocol": "Protocol",
"ldapProtocolLDAPS": "LDAPS (Recommended)",
"ldapProtocolLDAP": "LDAP",
"bindDn": "Administrator Bind DN",
"bindPassword": "Administrator Password",
"keepOriginal": "Leave empty to keep original",
"baseDn": "Base DN",
"userFilter": "User Filter",
"baseDn": "Base DN (User Search Base)",
"userFilter": "Search Filter (optional; %s is the username)",
"enableLdaps": "Enable LDAPS (SSL encryption)",
"usernameAttr": "Username Attribute",
"testConnection": "Test Connection",
"editUser": "Edit User",
@@ -320,6 +328,7 @@
"ldapLoadFailed": "LDAP load failed",
"ldapConfigSaved": "LDAP configuration saved",
"ldapConnected": "LDAP connection successful",
"ldapTestRequired": "Test the LDAP connection successfully before saving",
"ldapConnectFailed": "Connection failed",
"emptyUsername": "(empty)",
"errorDetails": "Error details:"
@@ -502,8 +511,9 @@
"plugins": {
"title": "Custom Protocol Plugins",
"registerPlugin": "Register Plugin",
"pluginHint": "Plugins implement the unified protocol contract via <b>command hooks</b>: the platform pipes tunnel config JSON to hook commands via stdin.",
"pluginContract": "Contract: <code>Create / Start / Stop / Status / Config</code>; Status hook outputs <code>running</code> or <code>stopped</code> on stdout; Config hook outputs client config text.",
"commandHooks": "command hooks",
"pluginHint": "Plugins implement the unified protocol contract via {hooks}: the platform pipes tunnel config JSON to hook commands via stdin.",
"pluginContract": "Contract: {contract}; Status hook outputs {running} or {stopped} on stdout; Config hook outputs client config text.",
"protocolId": "Protocol ID",
"enabled": "Enabled",
"disabled": "Disabled",
@@ -1024,4 +1034,4 @@
}
}
}
}
}
+20 -10
View File
@@ -73,7 +73,7 @@
"passwordLabel": "密码",
"passwordPlaceholder": "请输入密码",
"loginButton": "登 录",
"defaultAccount": "默认账号 <strong>admin / admin123</strong>,登录后请立即修改",
"defaultAccount": "默认账号 {account},登录后请立即修改",
"errorEmpty": "请输入用户名和密码",
"loginSuccess": "登录成功",
"changePasswordFirst": "首次登录请先修改密码",
@@ -279,14 +279,22 @@
"roleOperator": "运维",
"roleViewer": "只读",
"noUsers": "还没有用户",
"ldapTitle": "LDAP 身份源",
"ldapTitle": "LDAP / Active Directory",
"ldapHint": "LDAP 用户可首次登录后自动同步,也可使用左侧「LDAP 导入」预先添加。",
"ldapUrl": "LDAP URL",
"bindDn": "Bind DN",
"bindPassword": "Bind 密码",
"ldapConfigHint": "配置 LDAP 服务器,实现统一身份认证",
"ldapEnabled": "启用 LDAP",
"ldapServerAddress": "LDAP 服务器地址",
"ldapServerAddressPlaceholder": "例如 8.217.205.92 或 ldap.example.com",
"ldapPort": "端口",
"ldapProtocol": "连接协议",
"ldapProtocolLDAPS": "LDAPS(推荐)",
"ldapProtocolLDAP": "LDAP",
"bindDn": "管理员 Bind DN",
"bindPassword": "管理员密码",
"keepOriginal": "留空保持原值",
"baseDn": "Base DN",
"userFilter": "用户过滤",
"baseDn": "Base DN(用户搜索基础)",
"userFilter": "搜索过滤器(可选,%s 表示用户名)",
"enableLdaps": "启用 LDAPS(SSL 加密)",
"usernameAttr": "用户名属性",
"testConnection": "测试连接",
"editUser": "编辑用户",
@@ -320,6 +328,7 @@
"ldapLoadFailed": "LDAP 加载失败",
"ldapConfigSaved": "LDAP 配置已保存",
"ldapConnected": "LDAP 连接成功",
"ldapTestRequired": "请先测试 LDAP 连接,通过后才能保存",
"ldapConnectFailed": "连接失败",
"emptyUsername": "(空)",
"errorDetails": "失败详情:"
@@ -502,8 +511,9 @@
"plugins": {
"title": "自定义协议插件",
"registerPlugin": "注册插件",
"pluginHint": "插件通过<b>命令钩子</b>实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
"pluginContract": "契约:<code>Create / Start / Stop / Status / Config</code>;Status 钩子 stdout 输出 <code>running</code> 或 <code>stopped</code>;Config 钩子 stdout 输出客户端配置文本。",
"commandHooks": "命令钩子",
"pluginHint": "插件通过 {hooks} 实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
"pluginContract": "契约:{contract};Status 钩子 stdout 输出 {running} 或 {stopped};Config 钩子 stdout 输出客户端配置文本。",
"protocolId": "协议标识",
"enabled": "启用",
"disabled": "停用",
@@ -1024,4 +1034,4 @@
}
}
}
}
}
+3 -1
View File
@@ -72,7 +72,9 @@
</form>
<div class="login-footer">
<p v-html="$t('login.defaultAccount')"></p>
<i18n-t keypath="login.defaultAccount" tag="p">
<template #account><strong>admin / admin123</strong></template>
</i18n-t>
</div>
</div>
</div>
+8 -2
View File
@@ -8,8 +8,14 @@
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:14px">
<p style="margin:0" v-html="$t('plugins.pluginHint')"></p>
<p style="margin:4px 0 0;font-size:12px" v-html="$t('plugins.pluginContract')"></p>
<i18n-t keypath="plugins.pluginHint" tag="p" style="margin:0">
<template #hooks><b>{{ $t('plugins.commandHooks') }}</b></template>
</i18n-t>
<i18n-t keypath="plugins.pluginContract" tag="p" style="margin:4px 0 0;font-size:12px">
<template #contract><code>Create / Start / Stop / Status / Config</code></template>
<template #running><code>running</code></template>
<template #stopped><code>stopped</code></template>
</i18n-t>
</el-alert>
<el-table :data="plugins" size="default" v-loading="loading">
<el-table-column prop="display_name" :label="$t('common.name')" width="160" />
+55 -16
View File
@@ -46,20 +46,28 @@
<el-col :xs="24" :lg="9">
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('users.ldapTitle') }}</b></template>
<el-alert type="info" :closable="false"
:title="$t('users.ldapHint')" style="margin-bottom:12px" />
<el-form label-width="90px" size="small">
<el-form-item :label="$t('common.enabled')"><el-switch v-model="ldap.enabled" /></el-form-item>
<el-form-item :label="$t('users.ldapUrl')"><el-input v-model="ldap.url" placeholder="ldaps://ldap.example.com:636" /></el-form-item>
<el-form-item :label="$t('users.bindDn')"><el-input v-model="ldap.bind_dn" /></el-form-item>
<template #header>
<div>
<b>{{ $t('users.ldapTitle') }}</b>
<div style="margin-top:4px;font-size:12px;color:var(--text-secondary)">{{ $t('users.ldapConfigHint') }}</div>
</div>
</template>
<el-form label-width="112px" size="small">
<el-form-item :label="$t('users.ldapEnabled')"><el-switch v-model="ldap.enabled" /></el-form-item>
<el-form-item :label="$t('users.ldapServerAddress')">
<el-input v-model.trim="ldap.url" :placeholder="$t('users.ldapServerAddressPlaceholder')" />
</el-form-item>
<el-form-item :label="$t('users.ldapPort')">
<el-input-number v-model="ldapPort" :min="1" :max="65535" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item :label="$t('users.baseDn')"><el-input v-model.trim="ldap.base_dn" placeholder="dc=opsx,dc=vip" /></el-form-item>
<el-form-item :label="$t('users.bindDn')"><el-input v-model.trim="ldap.bind_dn" placeholder="cn=admin,dc=opsx,dc=vip" /></el-form-item>
<el-form-item :label="$t('users.bindPassword')"><el-input v-model="ldap.bind_password" type="password" show-password :placeholder="$t('users.keepOriginal')" /></el-form-item>
<el-form-item :label="$t('users.baseDn')"><el-input v-model="ldap.base_dn" placeholder="dc=example,dc=com" /></el-form-item>
<el-form-item :label="$t('users.userFilter')"><el-input v-model="ldap.user_filter" placeholder="(objectClass=inetOrgPerson)" /></el-form-item>
<el-form-item :label="$t('users.usernameAttr')"><el-input v-model="ldap.username_attribute" placeholder="uid" /></el-form-item>
<el-form-item :label="$t('users.userFilter')"><el-input v-model.trim="ldap.user_filter" placeholder="(uid=%s)" /></el-form-item>
<el-form-item :label="$t('users.enableLdaps')"><el-switch v-model="ldapUseLDAPS" /></el-form-item>
<div style="text-align:right">
<el-button @click="testLDAP">{{ $t('users.testConnection') }}</el-button>
<el-button type="primary" @click="saveLDAP">{{ $t('common.save') }}</el-button>
<el-button :loading="ldapTesting" @click="testLDAP">{{ $t('users.testConnection') }}</el-button>
<el-button type="primary" :disabled="ldap.enabled && !ldapTested" :title="ldap.enabled && !ldapTested ? $t('users.ldapTestRequired') : ''" @click="saveLDAP">{{ $t('common.save') }}</el-button>
</div>
</el-form>
</el-card>
@@ -147,7 +155,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api, { auth } from '../api'
@@ -163,6 +171,10 @@ const ldap = ref({
base_dn: '', user_filter: '(objectClass=inetOrgPerson)', username_attribute: 'uid',
start_tls: false,
})
const ldapUseLDAPS = ref(false)
const ldapPort = ref(389)
const ldapTested = ref(false)
const ldapTesting = ref(false)
// 批量导入
const batchVisible = ref(false)
@@ -183,6 +195,13 @@ async function load() {
const [u, l] = await Promise.all([api.get('/users'), api.get('/ldap')])
users.value = u.data || []
if (l.data) {
const savedAddress = String(l.data.url || '').trim()
const protocolMatch = savedAddress.match(/^(ldap|ldaps):\/\/(\[[^\]]+\]|[^/:]+)(?::(\d+))?$/i)
if (protocolMatch) {
ldapUseLDAPS.value = protocolMatch[1].toLowerCase() === 'ldaps'
l.data.url = protocolMatch[2].replace(/^\[|\]$/g, '')
ldapPort.value = Number(protocolMatch[3]) || (ldapUseLDAPS.value ? 636 : 389)
}
ldap.value = { ...ldap.value, ...l.data, bind_password: '' }
}
} catch (e) {
@@ -190,6 +209,20 @@ async function load() {
} finally { loading.value = false }
}
function ldapPayload() {
const address = ldap.value.url.trim()
const host = address.includes(':') && !address.startsWith('[') ? `[${address}]` : address
const port = Number(ldapPort.value)
const protocol = ldapUseLDAPS.value ? 'ldaps' : 'ldap'
return { ...ldap.value, url: address && port >= 1 && port <= 65535 ? `${protocol}://${host}:${port}` : '' }
}
watch(ldapUseLDAPS, (useLDAPS, previousUseLDAPS) => {
const previousPort = previousUseLDAPS ? 636 : 389
if (ldapPort.value === previousPort) ldapPort.value = useLDAPS ? 636 : 389
})
watch([ldap, ldapUseLDAPS, ldapPort], () => { ldapTested.value = false }, { deep: true })
// ---- 单用户 CRUD ----
function openCreate() {
editing.value = null
@@ -290,16 +323,22 @@ async function doLDAPImport() {
// ---- LDAP 配置 ----
async function saveLDAP() {
try {
await api.put('/ldap', ldap.value)
if (ldap.value.enabled && !ldapTested.value) return ElMessage.warning(t('users.ldapTestRequired'))
await api.put('/ldap', ldapPayload())
ldap.value.bind_password = ''
ElMessage.success(t('users.ldapConfigSaved'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('common.saveFailed')) }
}
async function testLDAP() {
ldapTesting.value = true
try {
await api.post('/ldap/test')
await api.post('/ldap/test', ldapPayload())
ldapTested.value = true
ElMessage.success(t('users.ldapConnected'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('users.ldapConnectFailed')) }
} catch (e) {
ldapTested.value = false
ElMessage.error(e.response?.data?.error || t('users.ldapConnectFailed'))
} finally { ldapTesting.value = false }
}
onMounted(load)