feat(ldap): Active Directory 双目录支持与 Docker 构建优化

- 新增 ldap_ad.go,支持 AD 密码编码、字段映射与目录类型判断
- 重构 ldap/ldap_users/ldap_ou,兼容 OpenLDAP 与 AD 登录、导入、OU 浏览
- 前端系统设置增加目录类型选择与 AD 配置项
- Dockerfile 按 TARGETARCH 单架构编译 Agent,限制 Node 内存防 OOM
- run.sh 新增 docker fast/upgrade,README 补充 LDAP/AD 与常见问题
This commit is contained in:
Logan committed 2026-07-02 22:45:49 +08:00
1 parent 0f0f2770e1
commit 78c27037e3
12 files changed
+613 -226

No files matched your search

@@ -24,7 +24,10 @@ const SETTINGS_TABS = [
] as const;
type SettingsSubTab = (typeof SETTINGS_TABS)[number]['id'];
export type LdapServerType = 'openldap' | 'ad';
export interface LdapFormData {
server_type: LdapServerType;
ldap_host: string;
ldap_port: number;
use_ldaps: boolean;
@@ -32,6 +35,11 @@ export interface LdapFormData {
admin_password: string;
base_dn: string;
search_filter: string;
user_mapping: {
username: string;
email: string;
display_name: string;
};
}
interface SystemSettingsTabProps {
@@ -325,6 +333,32 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">目录类型</label>
<Select
value={ldapForm.server_type}
onValueChange={(v: LdapServerType) => {
const isAD = v === 'ad';
setLdapForm({
...ldapForm,
server_type: v,
ldap_port: isAD && ldapForm.ldap_port === 389 ? 389 : ldapForm.ldap_port,
search_filter: ldapForm.search_filter || (isAD ? '(sAMAccountName=%s)' : '(uid=%s)'),
user_mapping: isAD
? { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' }
: { username: 'uid', email: 'mail', display_name: 'cn' },
});
}}
>
<SelectTrigger className="dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="openldap">OpenLDAP</SelectItem>
<SelectItem value="ad">Active Directory</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">LDAP 服务器地址</label>
<Input
@@ -350,7 +384,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
value={ldapForm.base_dn}
onChange={(e) => setLdapForm({ ...ldapForm, base_dn: e.target.value })}
className="dg-input"
placeholder="ou=users,dc=example,dc=com"
placeholder={ldapForm.server_type === 'ad' ? 'DC=example,DC=com' : 'ou=users,dc=example,dc=com'}
/>
</div>
<div className="space-y-1.5">
@@ -359,7 +393,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
value={ldapForm.admin_dn}
onChange={(e) => setLdapForm({ ...ldapForm, admin_dn: e.target.value })}
className="dg-input"
placeholder="cn=admin,dc=example,dc=com"
placeholder={ldapForm.server_type === 'ad' ? 'CN=admin,CN=Users,DC=example,DC=com' : 'cn=admin,dc=example,dc=com'}
/>
</div>
<div className="space-y-1.5">
@@ -378,7 +412,43 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
value={ldapForm.search_filter}
onChange={(e) => setLdapForm({ ...ldapForm, search_filter: e.target.value })}
className="dg-input"
placeholder="(uid=%s)"
placeholder={ldapForm.server_type === 'ad' ? '(sAMAccountName=%s)' : '(uid=%s)'}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">用户名字段</label>
<Input
value={ldapForm.user_mapping.username}
onChange={(e) => setLdapForm({
...ldapForm,
user_mapping: { ...ldapForm.user_mapping, username: e.target.value },
})}
className="dg-input"
placeholder={ldapForm.server_type === 'ad' ? 'sAMAccountName' : 'uid'}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">邮箱字段</label>
<Input
value={ldapForm.user_mapping.email}
onChange={(e) => setLdapForm({
...ldapForm,
user_mapping: { ...ldapForm.user_mapping, email: e.target.value },
})}
className="dg-input"
placeholder="mail"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs dg-text-muted">显示名字段</label>
<Input
value={ldapForm.user_mapping.display_name}
onChange={(e) => setLdapForm({
...ldapForm,
user_mapping: { ...ldapForm.user_mapping, display_name: e.target.value },
})}
className="dg-input"
placeholder={ldapForm.server_type === 'ad' ? 'displayName' : 'cn'}
/>
</div>
<div className="flex items-center gap-2 col-span-2">
@@ -388,6 +458,9 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
className="data-[state=checked]:bg-blue-600"
/>
<span className="text-sm text-[var(--dg-text-secondary)]">启用 LDAPS(SSL 加密)</span>
{ldapForm.server_type === 'ad' && (
<span className="text-xs dg-text-muted">AD 创建用户必须启用 LDAPS</span>
)}
</div>
</div>
@@ -30,10 +30,13 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
// ===== LDAP 配置 =====
const [ldapConfig, setLdapConfig] = useState<JsonData>(null);
const [ldapForm, setLdapForm] = useState<LdapFormData>({
const defaultLdapForm = (): LdapFormData => ({
server_type: 'openldap',
ldap_host: '', ldap_port: 389, use_ldaps: false,
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
user_mapping: { username: 'uid', email: 'mail', display_name: 'cn' },
});
const [ldapForm, setLdapForm] = useState<LdapFormData>(defaultLdapForm());
const [ldapSaving, setLdapSaving] = useState(false);
const [ldapTesting, setLdapTesting] = useState(false);
const [ldapTestResult, setLdapTestResult] = useState<string | null>(null);
@@ -68,7 +71,26 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
const res = await ldapApi.getConfig();
if (res && res.ldap_host) {
setLdapConfig(res);
const serverType = (res.server_type === 'ad' ? 'ad' : 'openldap') as LdapFormData['server_type'];
let mapping = { username: 'uid', email: 'mail', display_name: 'cn' };
if (serverType === 'ad') {
mapping = { username: 'sAMAccountName', email: 'mail', display_name: 'displayName' };
}
if (res.user_mapping) {
let um = res.user_mapping;
if (typeof um === 'string') {
try { um = JSON.parse(um); } catch { um = null; }
}
if (um && typeof um === 'object') {
mapping = {
username: um.username || mapping.username,
email: um.email || mapping.email,
display_name: um.display_name || mapping.display_name,
};
}
}
setLdapForm({
server_type: serverType,
ldap_host: res.ldap_host || '',
ldap_port: res.ldap_port || 389,
use_ldaps: res.use_ldaps || false,
@@ -76,6 +98,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
admin_password: '',
base_dn: res.base_dn || '',
search_filter: res.search_filter || '',
user_mapping: mapping,
});
}
} catch {
@@ -87,7 +110,10 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
const handleLdapSave = async () => {
setLdapSaving(true);
try {
await ldapApi.updateConfig(ldapForm);
await ldapApi.updateConfig({
...ldapForm,
user_mapping: ldapForm.user_mapping,
});
await loadLdapConfig();
} catch (error) {
console.error('Failed to save LDAP config:', error);
@@ -101,7 +127,10 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
setLdapTesting(true);
setLdapTestResult(null);
try {
await ldapApi.updateConfig(ldapForm);
await ldapApi.updateConfig({
...ldapForm,
user_mapping: ldapForm.user_mapping,
});
const res = await ldapApi.testConnection();
setLdapTestResult(res?.message || (res?.success ? '连接成功' : '连接失败'));
} catch (error) {