mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-11 03:58:28 +08:00
feat: add ldap login
This commit is contained in:
@@ -473,5 +473,30 @@ export const adminApi = {
|
||||
`/api/admin/system/email/templates/${templateType}/reset`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// LDAP 配置相关
|
||||
// 获取 LDAP 配置
|
||||
async getLdapConfig(): Promise<any> {
|
||||
const response = await apiClient.get<any>('/api/admin/ldap/config')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 更新 LDAP 配置
|
||||
async updateLdapConfig(config: any): Promise<{ message: string }> {
|
||||
const response = await apiClient.put<{ message: string }>(
|
||||
'/api/admin/ldap/config',
|
||||
config
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 测试 LDAP 连接
|
||||
async testLdapConnection(config?: any): Promise<{ success: boolean; message: string }> {
|
||||
const response = await apiClient.post<{ success: boolean; message: string }>(
|
||||
'/api/admin/ldap/test',
|
||||
config || {}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { log } from '@/utils/logger'
|
||||
export interface LoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
auth_type?: 'local' | 'ldap'
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
@@ -81,6 +82,12 @@ export interface RegistrationSettingsResponse {
|
||||
require_email_verification: boolean
|
||||
}
|
||||
|
||||
export interface AuthSettingsResponse {
|
||||
local_enabled: boolean
|
||||
ldap_enabled: boolean
|
||||
ldap_exclusive: boolean
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string // UUID
|
||||
username: string
|
||||
@@ -173,5 +180,10 @@ export const authApi = {
|
||||
{ email }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getAuthSettings(): Promise<AuthSettingsResponse> {
|
||||
const response = await apiClient.get<AuthSettingsResponse>('/api/auth/settings')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,19 +66,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证方式切换 -->
|
||||
<Tabs
|
||||
v-if="showAuthTypeTabs"
|
||||
v-model="authType"
|
||||
class="w-full"
|
||||
>
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="local">
|
||||
本地登录
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ldap">
|
||||
LDAP 登录
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form
|
||||
class="space-y-4"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="login-email">邮箱</Label>
|
||||
<Label for="login-email">{{ emailLabel }}</Label>
|
||||
<Input
|
||||
id="login-email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
:type="authType === 'ldap' ? 'text' : 'email'"
|
||||
required
|
||||
placeholder="hello@example.com"
|
||||
:placeholder="authType === 'ldap' ? 'username 或 email' : 'hello@example.com'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
@@ -156,6 +172,9 @@ import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import Tabs from '@/components/ui/tabs.vue'
|
||||
import TabsList from '@/components/ui/tabs-list.vue'
|
||||
import TabsTrigger from '@/components/ui/tabs-trigger.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||
@@ -180,6 +199,20 @@ const showRegisterDialog = ref(false)
|
||||
const requireEmailVerification = ref(false)
|
||||
const allowRegistration = ref(false) // 由系统配置控制,默认关闭
|
||||
|
||||
// LDAP authentication settings
|
||||
const authType = ref<'local' | 'ldap'>('local')
|
||||
const localEnabled = ref(true)
|
||||
const ldapEnabled = ref(false)
|
||||
const ldapExclusive = ref(false)
|
||||
|
||||
const showAuthTypeTabs = computed(() => {
|
||||
return localEnabled.value && ldapEnabled.value && !ldapExclusive.value
|
||||
})
|
||||
|
||||
const emailLabel = computed(() => {
|
||||
return authType.value === 'ldap' ? '用户名/邮箱' : '邮箱'
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
isOpen.value = val
|
||||
// 打开对话框时重置表单
|
||||
@@ -212,7 +245,7 @@ async function handleLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
const success = await authStore.login(form.value.email, form.value.password)
|
||||
const success = await authStore.login(form.value.email, form.value.password, authType.value)
|
||||
if (success) {
|
||||
showSuccess('登录成功,正在跳转...')
|
||||
|
||||
@@ -246,16 +279,36 @@ function handleSwitchToLogin() {
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
// Load registration settings on mount
|
||||
// Load authentication and registration settings on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const settings = await authApi.getRegistrationSettings()
|
||||
allowRegistration.value = !!settings.enable_registration
|
||||
requireEmailVerification.value = !!settings.require_email_verification
|
||||
// Load registration settings
|
||||
const regSettings = await authApi.getRegistrationSettings()
|
||||
allowRegistration.value = !!regSettings.enable_registration
|
||||
requireEmailVerification.value = !!regSettings.require_email_verification
|
||||
|
||||
// Load authentication settings
|
||||
const authSettings = await authApi.getAuthSettings()
|
||||
localEnabled.value = authSettings.local_enabled
|
||||
ldapEnabled.value = authSettings.ldap_enabled
|
||||
ldapExclusive.value = authSettings.ldap_exclusive
|
||||
|
||||
// Set default auth type based on settings
|
||||
if (authSettings.ldap_exclusive) {
|
||||
authType.value = 'ldap'
|
||||
} else if (!authSettings.local_enabled && authSettings.ldap_enabled) {
|
||||
authType.value = 'ldap'
|
||||
} else {
|
||||
authType.value = 'local'
|
||||
}
|
||||
} catch (error) {
|
||||
// If获取失败,保持默认:关闭注册 & 关闭邮箱验证
|
||||
// If获取失败,保持默认:关闭注册 & 关闭邮箱验证 & 使用本地认证
|
||||
allowRegistration.value = false
|
||||
requireEmailVerification.value = false
|
||||
localEnabled.value = true
|
||||
ldapEnabled.value = false
|
||||
ldapExclusive.value = false
|
||||
authType.value = 'local'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -423,6 +423,7 @@ const navigation = computed(() => {
|
||||
{ name: 'IP 安全', href: '/admin/ip-security', icon: Shield },
|
||||
{ name: '审计日志', href: '/admin/audit-logs', icon: AlertTriangle },
|
||||
{ name: '邮件配置', href: '/admin/email', icon: Mail },
|
||||
{ name: 'LDAP 配置', href: '/admin/ldap', icon: Shield },
|
||||
{ name: '系统设置', href: '/admin/system', icon: Cog },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -111,6 +111,11 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'EmailSettings',
|
||||
component: () => importWithRetry(() => import('@/views/admin/EmailSettings.vue'))
|
||||
},
|
||||
{
|
||||
path: 'ldap',
|
||||
name: 'LdapSettings',
|
||||
component: () => importWithRetry(() => import('@/views/admin/LdapSettings.vue'))
|
||||
},
|
||||
{
|
||||
path: 'audit-logs',
|
||||
name: 'AuditLogs',
|
||||
|
||||
@@ -31,12 +31,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
async function login(email: string, password: string, authType: 'local' | 'ldap' = 'local') {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authApi.login({ email, password })
|
||||
const response = await authApi.login({ email, password, auth_type: authType })
|
||||
token.value = response.access_token
|
||||
|
||||
// 获取用户信息
|
||||
|
||||
303
frontend/src/views/admin/LdapSettings.vue
Normal file
303
frontend/src/views/admin/LdapSettings.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<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>
|
||||
<Input
|
||||
id="bind-password"
|
||||
v-model="ldapConfig.bind_password"
|
||||
type="password"
|
||||
:placeholder="hasPassword ? '已设置(留空保持不变)' : '请输入密码'"
|
||||
class="mt-1"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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:checked="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:checked="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:checked="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 } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useLogger } from '@/composables/useLogger'
|
||||
import { adminApi } from '@/api/admin'
|
||||
|
||||
const { success, error } = useToast()
|
||||
const log = useLogger('LdapSettings')
|
||||
|
||||
const loading = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const testLoading = ref(false)
|
||||
const hasPassword = 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,
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
hasPassword.value = !!response.server_url
|
||||
} catch (err) {
|
||||
error('加载 LDAP 配置失败')
|
||||
log.error('加载 LDAP 配置失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
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,
|
||||
}
|
||||
if (ldapConfig.value.bind_password) {
|
||||
payload.bind_password = ldapConfig.value.bind_password
|
||||
}
|
||||
await adminApi.updateLdapConfig(payload)
|
||||
success('LDAP 配置保存成功')
|
||||
hasPassword.value = true
|
||||
ldapConfig.value.bind_password = ''
|
||||
} catch (err) {
|
||||
error('保存 LDAP 配置失败')
|
||||
log.error('保存 LDAP 配置失败:', err)
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestConnection() {
|
||||
testLoading.value = true
|
||||
try {
|
||||
const response = await adminApi.testLdapConnection()
|
||||
if (response.success) {
|
||||
success('LDAP 连接测试成功')
|
||||
} else {
|
||||
error(`LDAP 连接测试失败: ${response.message}`)
|
||||
}
|
||||
} catch (err) {
|
||||
error('LDAP 连接测试失败')
|
||||
log.error('LDAP 连接测试失败:', err)
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user