From 75b58491e9d2c7a101c6aaebe91b266e74fc70fb Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 7 Jul 2026 09:39:58 +0200 Subject: [PATCH] feat(ldap): AD connection prefill + group/OU import filter UI New-config form now defaults to the CTL Active Directory connection values (balios.ctl.local:3268, dc=ctl,dc=local, AD person filter, down-level bind-DN hint) with the password left blank; editing an existing config still shows its real saved values. Adds a group/OU import filter section: discover AD groups/OUs via GET /ldap/groups, toggle selection or add DNs manually, persist via PATCH /ldap/config. Empty selection keeps today's "import everyone under base DN" behavior. Co-Authored-By: Claude Sonnet 5 --- apps/web/src/app/(portal)/admin/ldap/page.tsx | 193 +++++++++++++++++- apps/web/src/messages/de.json | 16 ++ apps/web/src/messages/en.json | 16 ++ 3 files changed, 220 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/(portal)/admin/ldap/page.tsx b/apps/web/src/app/(portal)/admin/ldap/page.tsx index d930f99..33d4540 100644 --- a/apps/web/src/app/(portal)/admin/ldap/page.tsx +++ b/apps/web/src/app/(portal)/admin/ldap/page.tsx @@ -23,10 +23,17 @@ interface LdapConfig { searchFilter: string; syncIntervalMin: number; isActive: boolean; + groupFilterDns: string[]; lastSyncAt: string | null; fieldMappings: FieldMapping[]; } +interface LdapDirectoryEntry { + dn: string; + name: string; + type: 'group' | 'ou'; +} + interface SyncResult { created: number; updated: number; @@ -53,13 +60,18 @@ export default function AdminLdapPage() { const [syncResult, setSyncResult] = useState(null); const [syncing, setSyncing] = useState(false); - // Form state for connection settings + // Form state for connection settings. + // Defaults are pre-filled with the known-good CTL Active Directory + // connection values (sourced from the working XWiki LDAP config) so a + // brand-new setup only needs the service-account password. fetchConfig() + // below overwrites these with the real saved values whenever a config + // already exists. const [formData, setFormData] = useState({ - serverUrl: '', - baseDn: '', + serverUrl: 'ldap://balios.ctl.local:3268', + baseDn: 'dc=ctl,dc=local', bindDn: '', bindPassword: '', - searchFilter: '(objectClass=person)', + searchFilter: '(&(objectClass=user)(objectCategory=person))', syncIntervalMin: 60, isActive: true, }); @@ -68,6 +80,13 @@ export default function AdminLdapPage() { const [newMapping, setNewMapping] = useState({ ldapField: '', tesseraField: '' }); const [showMappingForm, setShowMappingForm] = useState(false); + // Group/OU import filter (selective sync) + const [groupFilterDns, setGroupFilterDns] = useState([]); + const [discovered, setDiscovered] = useState(null); + const [discovering, setDiscovering] = useState(false); + const [manualDn, setManualDn] = useState(''); + const [savingFilter, setSavingFilter] = useState(false); + const hasAccess = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN'; @@ -89,6 +108,7 @@ export default function AdminLdapPage() { syncIntervalMin: data.syncIntervalMin ?? 60, isActive: data.isActive ?? true, }); + setGroupFilterDns(data.groupFilterDns ?? []); } } } catch { @@ -208,6 +228,59 @@ export default function AdminLdapPage() { } }; + const handleDiscoverGroups = async () => { + setDiscovering(true); + try { + const res = await fetch(`${API_URL}/ldap/groups`, { + credentials: 'include', + }); + if (res.ok) { + const data = await res.json(); + setDiscovered(data); + } + } catch { + // silently fail + } finally { + setDiscovering(false); + } + }; + + const toggleGroupFilterDn = (dn: string) => { + setGroupFilterDns((prev) => + prev.includes(dn) ? prev.filter((d) => d !== dn) : [...prev, dn], + ); + }; + + const handleAddManualDn = () => { + const dn = manualDn.trim(); + if (!dn || groupFilterDns.includes(dn)) return; + setGroupFilterDns((prev) => [...prev, dn]); + setManualDn(''); + }; + + const handleRemoveGroupFilterDn = (dn: string) => { + setGroupFilterDns((prev) => prev.filter((d) => d !== dn)); + }; + + const handleSaveGroupFilter = async () => { + setSavingFilter(true); + try { + const res = await fetch(`${API_URL}/ldap/config`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ groupFilterDns }), + }); + if (res.ok) { + await fetchConfig(); + } + } catch { + // silently fail + } finally { + setSavingFilter(false); + } + }; + if (!hasAccess) { return (
@@ -269,10 +342,11 @@ export default function AdminLdapPage() { type="text" value={formData.bindDn} onChange={(e) => setFormData({ ...formData, bindDn: e.target.value })} - placeholder="cn=admin,dc=example,dc=com" + placeholder="ctl\serviceaccount" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" required /> +

{t('bindDnHint')}