mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-11 20:18:30 +08:00
feat: 添加 LDAP 认证支持
- 新增 LDAP 服务和 API 接口 - 添加 LDAP 配置管理页面 - 登录页面支持 LDAP/本地认证切换 - 数据库迁移支持 LDAP 相关字段
This commit is contained in:
@@ -106,23 +106,23 @@
|
||||
type="text"
|
||||
:placeholder="smtpPasswordIsSet ? '已设置(留空保持不变)' : '请输入密码'"
|
||||
class="-webkit-text-security-disc"
|
||||
:class="smtpPasswordIsSet ? 'pr-8' : ''"
|
||||
:class="(smtpPasswordIsSet || emailConfig.smtp_password) ? 'pr-10' : ''"
|
||||
autocomplete="one-time-code"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
data-form-type="other"
|
||||
/>
|
||||
<button
|
||||
v-if="smtpPasswordIsSet"
|
||||
v-if="smtpPasswordIsSet || emailConfig.smtp_password"
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="清除已保存的密码"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50 transition-colors"
|
||||
title="清除密码"
|
||||
@click="handleClearSmtpPassword"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -498,6 +498,7 @@ const smtpEncryptionSelectOpen = ref(false)
|
||||
const emailSuffixModeSelectOpen = ref(false)
|
||||
const testSmtpLoading = ref(false)
|
||||
const smtpPasswordIsSet = ref(false)
|
||||
const clearSmtpPassword = ref(false) // 标记是否要清除密码
|
||||
|
||||
// 邮件模板相关状态
|
||||
const templateLoading = ref(false)
|
||||
@@ -710,6 +711,7 @@ async function loadEmailConfig() {
|
||||
// 配置不存在时使用默认值,无需处理
|
||||
}
|
||||
}
|
||||
clearSmtpPassword.value = false
|
||||
} catch (err) {
|
||||
error('加载邮件配置失败')
|
||||
log.error('加载邮件配置失败:', err)
|
||||
@@ -720,6 +722,12 @@ async function loadEmailConfig() {
|
||||
async function saveSmtpConfig() {
|
||||
smtpSaveLoading.value = true
|
||||
try {
|
||||
const passwordAction: 'unchanged' | 'updated' | 'cleared' = emailConfig.value.smtp_password
|
||||
? 'updated'
|
||||
: clearSmtpPassword.value
|
||||
? 'cleared'
|
||||
: 'unchanged'
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
key: 'smtp_host',
|
||||
@@ -737,7 +745,7 @@ async function saveSmtpConfig() {
|
||||
description: 'SMTP 用户名'
|
||||
},
|
||||
// 只有输入了新密码才提交(空值表示保持原密码)
|
||||
...(emailConfig.value.smtp_password
|
||||
...(passwordAction === 'updated'
|
||||
? [{
|
||||
key: 'smtp_password',
|
||||
value: emailConfig.value.smtp_password,
|
||||
@@ -770,8 +778,23 @@ async function saveSmtpConfig() {
|
||||
adminApi.updateSystemConfig(item.key, item.value, item.description)
|
||||
)
|
||||
|
||||
// 如果标记了清除密码,删除密码配置
|
||||
if (passwordAction === 'cleared') {
|
||||
promises.push(adminApi.deleteSystemConfig('smtp_password'))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
success('SMTP 配置已保存')
|
||||
|
||||
// 更新状态
|
||||
if (passwordAction === 'cleared') {
|
||||
clearSmtpPassword.value = false
|
||||
smtpPasswordIsSet.value = false
|
||||
} else if (passwordAction === 'updated') {
|
||||
clearSmtpPassword.value = false
|
||||
smtpPasswordIsSet.value = true
|
||||
}
|
||||
emailConfig.value.smtp_password = null
|
||||
} catch (err) {
|
||||
error('保存配置失败')
|
||||
log.error('保存 SMTP 配置失败:', err)
|
||||
@@ -812,15 +835,16 @@ async function saveEmailSuffixConfig() {
|
||||
}
|
||||
|
||||
// 清除 SMTP 密码
|
||||
async function handleClearSmtpPassword() {
|
||||
try {
|
||||
await adminApi.deleteSystemConfig('smtp_password')
|
||||
smtpPasswordIsSet.value = false
|
||||
function handleClearSmtpPassword() {
|
||||
// 如果有输入内容,先清空输入框
|
||||
if (emailConfig.value.smtp_password) {
|
||||
emailConfig.value.smtp_password = null
|
||||
success('SMTP 密码已清除')
|
||||
} catch (err) {
|
||||
error('清除密码失败')
|
||||
log.error('清除 SMTP 密码失败:', err)
|
||||
return
|
||||
}
|
||||
// 标记要清除服务端密码(保存时生效)
|
||||
if (smtpPasswordIsSet.value) {
|
||||
clearSmtpPassword.value = true
|
||||
smtpPasswordIsSet.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
379
frontend/src/views/admin/LdapSettings.vue
Normal file
379
frontend/src/views/admin/LdapSettings.vue
Normal file
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="LDAP 配置"
|
||||
description="配置 LDAP 认证服务"
|
||||
/>
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
<CardSection
|
||||
title="LDAP 服务器配置"
|
||||
description="配置 LDAP 服务器连接参数"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="testLoading"
|
||||
@click="handleTestConnection"
|
||||
>
|
||||
{{ testLoading ? '测试中...' : '测试连接' }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
:disabled="saveLoading"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saveLoading ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="server-url" class="block text-sm font-medium">
|
||||
服务器地址
|
||||
</Label>
|
||||
<Input
|
||||
id="server-url"
|
||||
v-model="ldapConfig.server_url"
|
||||
type="text"
|
||||
placeholder="ldap://ldap.example.com:389"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
格式: ldap://host:389 或 ldaps://host:636
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="bind-dn" class="block text-sm font-medium">
|
||||
绑定 DN
|
||||
</Label>
|
||||
<Input
|
||||
id="bind-dn"
|
||||
v-model="ldapConfig.bind_dn"
|
||||
type="text"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
用于连接 LDAP 服务器的管理员 DN
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="bind-password" class="block text-sm font-medium">
|
||||
绑定密码
|
||||
</Label>
|
||||
<div class="relative mt-1">
|
||||
<Input
|
||||
id="bind-password"
|
||||
v-model="ldapConfig.bind_password"
|
||||
type="password"
|
||||
:placeholder="hasPassword ? '已设置(留空保持不变)' : '请输入密码'"
|
||||
:class="(hasPassword || ldapConfig.bind_password) ? 'pr-10' : ''"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<button
|
||||
v-if="hasPassword || ldapConfig.bind_password"
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50 transition-colors"
|
||||
@click="handleClearPassword"
|
||||
title="清除密码"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
绑定账号的密码
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="base-dn" class="block text-sm font-medium">
|
||||
基础 DN
|
||||
</Label>
|
||||
<Input
|
||||
id="base-dn"
|
||||
v-model="ldapConfig.base_dn"
|
||||
type="text"
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
用户搜索的基础 DN
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="user-search-filter" class="block text-sm font-medium">
|
||||
用户搜索过滤器
|
||||
</Label>
|
||||
<Input
|
||||
id="user-search-filter"
|
||||
v-model="ldapConfig.user_search_filter"
|
||||
type="text"
|
||||
placeholder="(uid={username})"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
{username} 会被替换为登录用户名
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="username-attr" class="block text-sm font-medium">
|
||||
用户名属性
|
||||
</Label>
|
||||
<Input
|
||||
id="username-attr"
|
||||
v-model="ldapConfig.username_attr"
|
||||
type="text"
|
||||
placeholder="uid"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
常用: uid (OpenLDAP), sAMAccountName (AD)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="email-attr" class="block text-sm font-medium">
|
||||
邮箱属性
|
||||
</Label>
|
||||
<Input
|
||||
id="email-attr"
|
||||
v-model="ldapConfig.email_attr"
|
||||
type="text"
|
||||
placeholder="mail"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="display-name-attr" class="block text-sm font-medium">
|
||||
显示名称属性
|
||||
</Label>
|
||||
<Input
|
||||
id="display-name-attr"
|
||||
v-model="ldapConfig.display_name_attr"
|
||||
type="text"
|
||||
placeholder="cn"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="connect-timeout" class="block text-sm font-medium">
|
||||
连接超时 (秒)
|
||||
</Label>
|
||||
<Input
|
||||
id="connect-timeout"
|
||||
v-model.number="ldapConfig.connect_timeout"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
placeholder="10"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
单次 LDAP 操作超时时间 (1-60秒),跨国网络建议 15-30 秒
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label class="text-sm font-medium">使用 STARTTLS</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
在非 SSL 连接上启用 TLS 加密
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model="ldapConfig.use_starttls" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label class="text-sm font-medium">启用 LDAP 认证</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
允许用户使用 LDAP 账号登录
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model="ldapConfig.is_enabled" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Label class="text-sm font-medium">仅允许 LDAP 登录</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
禁用本地账号登录,仅允许 LDAP 认证
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model="ldapConfig.is_exclusive" />
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { PageContainer, PageHeader, CardSection } from '@/components/layout'
|
||||
import { Button, Input, Label, Switch } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { adminApi, type LdapConfigUpdateRequest } from '@/api/admin'
|
||||
|
||||
const { success, error } = useToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const testLoading = ref(false)
|
||||
const hasPassword = ref(false)
|
||||
const clearPassword = ref(false) // 标记是否要清除密码
|
||||
|
||||
const ldapConfig = ref({
|
||||
server_url: '',
|
||||
bind_dn: '',
|
||||
bind_password: '',
|
||||
base_dn: '',
|
||||
user_search_filter: '(uid={username})',
|
||||
username_attr: 'uid',
|
||||
email_attr: 'mail',
|
||||
display_name_attr: 'cn',
|
||||
is_enabled: false,
|
||||
is_exclusive: false,
|
||||
use_starttls: false,
|
||||
connect_timeout: 10,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadConfig()
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await adminApi.getLdapConfig()
|
||||
ldapConfig.value = {
|
||||
server_url: response.server_url || '',
|
||||
bind_dn: response.bind_dn || '',
|
||||
bind_password: '',
|
||||
base_dn: response.base_dn || '',
|
||||
user_search_filter: response.user_search_filter || '(uid={username})',
|
||||
username_attr: response.username_attr || 'uid',
|
||||
email_attr: response.email_attr || 'mail',
|
||||
display_name_attr: response.display_name_attr || 'cn',
|
||||
is_enabled: response.is_enabled || false,
|
||||
is_exclusive: response.is_exclusive || false,
|
||||
use_starttls: response.use_starttls || false,
|
||||
connect_timeout: response.connect_timeout || 10,
|
||||
}
|
||||
hasPassword.value = !!response.has_bind_password
|
||||
clearPassword.value = false
|
||||
} catch (err) {
|
||||
error('加载 LDAP 配置失败')
|
||||
console.error('加载 LDAP 配置失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const payload: LdapConfigUpdateRequest = {
|
||||
server_url: ldapConfig.value.server_url,
|
||||
bind_dn: ldapConfig.value.bind_dn,
|
||||
base_dn: ldapConfig.value.base_dn,
|
||||
user_search_filter: ldapConfig.value.user_search_filter,
|
||||
username_attr: ldapConfig.value.username_attr,
|
||||
email_attr: ldapConfig.value.email_attr,
|
||||
display_name_attr: ldapConfig.value.display_name_attr,
|
||||
is_enabled: ldapConfig.value.is_enabled,
|
||||
is_exclusive: ldapConfig.value.is_exclusive,
|
||||
use_starttls: ldapConfig.value.use_starttls,
|
||||
connect_timeout: ldapConfig.value.connect_timeout,
|
||||
}
|
||||
|
||||
// 优先使用输入的新密码;否则如果标记清除则发送空字符串
|
||||
let passwordAction: 'unchanged' | 'updated' | 'cleared' = 'unchanged'
|
||||
if (ldapConfig.value.bind_password) {
|
||||
payload.bind_password = ldapConfig.value.bind_password
|
||||
passwordAction = 'updated'
|
||||
} else if (clearPassword.value) {
|
||||
payload.bind_password = ''
|
||||
passwordAction = 'cleared'
|
||||
}
|
||||
|
||||
await adminApi.updateLdapConfig(payload)
|
||||
success('LDAP 配置保存成功')
|
||||
|
||||
if (passwordAction === 'cleared') {
|
||||
hasPassword.value = false
|
||||
clearPassword.value = false
|
||||
} else if (passwordAction === 'updated') {
|
||||
hasPassword.value = true
|
||||
clearPassword.value = false
|
||||
}
|
||||
ldapConfig.value.bind_password = ''
|
||||
} catch (err) {
|
||||
error('保存 LDAP 配置失败')
|
||||
console.error('保存 LDAP 配置失败:', err)
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestConnection() {
|
||||
if (clearPassword.value && !ldapConfig.value.bind_password) {
|
||||
error('已标记清除绑定密码,请先保存或输入新的绑定密码再测试')
|
||||
return
|
||||
}
|
||||
|
||||
testLoading.value = true
|
||||
try {
|
||||
const payload: LdapConfigUpdateRequest = {
|
||||
server_url: ldapConfig.value.server_url,
|
||||
bind_dn: ldapConfig.value.bind_dn,
|
||||
base_dn: ldapConfig.value.base_dn,
|
||||
user_search_filter: ldapConfig.value.user_search_filter,
|
||||
username_attr: ldapConfig.value.username_attr,
|
||||
email_attr: ldapConfig.value.email_attr,
|
||||
display_name_attr: ldapConfig.value.display_name_attr,
|
||||
is_enabled: ldapConfig.value.is_enabled,
|
||||
is_exclusive: ldapConfig.value.is_exclusive,
|
||||
use_starttls: ldapConfig.value.use_starttls,
|
||||
connect_timeout: ldapConfig.value.connect_timeout,
|
||||
...(ldapConfig.value.bind_password && { bind_password: ldapConfig.value.bind_password }),
|
||||
}
|
||||
const response = await adminApi.testLdapConnection(payload)
|
||||
if (response.success) {
|
||||
success('LDAP 连接测试成功')
|
||||
} else {
|
||||
error(`LDAP 连接测试失败: ${response.message}`)
|
||||
}
|
||||
} catch (err) {
|
||||
error('LDAP 连接测试失败')
|
||||
console.error('LDAP 连接测试失败:', err)
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearPassword() {
|
||||
// 如果有输入内容,先清空输入框
|
||||
if (ldapConfig.value.bind_password) {
|
||||
ldapConfig.value.bind_password = ''
|
||||
return
|
||||
}
|
||||
// 标记要清除服务端密码(保存时生效)
|
||||
if (hasPassword.value) {
|
||||
clearPassword.value = true
|
||||
hasPassword.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user