import { useState, useEffect } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Shield, Lock, Unlock, AlertTriangle, CheckCircle, Loader2, Send } from 'lucide-react'; import { api } from '../api/client'; import type { AppSettings } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { Collapsible } from './Collapsible'; import { useToast } from '../contexts/ToastContext'; import { useAuth } from '../contexts/AuthContext'; const SECURITY_PORT_MAP: Record = { starttls: '389', ldaps: '636', }; interface LDAPFormState { ldap_server_url: string; ldap_bind_dn: string; ldap_bind_password: string; ldap_search_base: string; ldap_user_filter: string; ldap_security: string; ldap_group_mapping: string; ldap_auto_provision: boolean; ldap_default_group: string; } export function LDAPSettings() { const { t } = useTranslation(); const { showToast } = useToast(); const queryClient = useQueryClient(); const { authEnabled } = useAuth(); const [form, setForm] = useState({ ldap_server_url: '', ldap_bind_dn: '', ldap_bind_password: '', ldap_search_base: '', ldap_user_filter: '(sAMAccountName={username})', ldap_security: 'starttls', ldap_group_mapping: '', ldap_auto_provision: false, ldap_default_group: '', }); // Fetch settings const { data: settings, isLoading } = useQuery({ queryKey: ['settings'], queryFn: () => api.getSettings(), }); // Fetch LDAP status const { data: ldapStatus } = useQuery({ queryKey: ['ldapStatus'], queryFn: () => api.getLDAPStatus(), }); // Fetch groups for mapping display const { data: groups = [] } = useQuery({ queryKey: ['groups'], queryFn: () => api.getGroups(), }); // Load settings into form useEffect(() => { if (settings) { setForm({ ldap_server_url: settings.ldap_server_url || '', ldap_bind_dn: settings.ldap_bind_dn || '', ldap_bind_password: '', // Never show password ldap_search_base: settings.ldap_search_base || '', ldap_user_filter: settings.ldap_user_filter || '(sAMAccountName={username})', ldap_security: settings.ldap_security || 'starttls', ldap_group_mapping: settings.ldap_group_mapping || '', ldap_auto_provision: settings.ldap_auto_provision ?? false, ldap_default_group: settings.ldap_default_group || '', }); } }, [settings]); // Save settings const saveMutation = useMutation({ mutationFn: (data: Partial) => api.updateSettings(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }); queryClient.invalidateQueries({ queryKey: ['ldapStatus'] }); showToast(t('settings.ldap.settingsSaved') || 'LDAP settings saved', 'success'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); // Toggle LDAP const toggleMutation = useMutation({ mutationFn: (enabled: boolean) => api.updateSettings({ ldap_enabled: enabled }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }); queryClient.invalidateQueries({ queryKey: ['ldapStatus'] }); showToast( ldapStatus?.ldap_enabled ? (t('settings.ldap.disabled') || 'LDAP authentication disabled') : (t('settings.ldap.enabled') || 'LDAP authentication enabled'), 'success' ); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); // Test connection const testMutation = useMutation({ mutationFn: () => api.testLDAP(), onSuccess: (data: { success: boolean; message: string }) => { showToast(data.message, data.success ? 'success' : 'error'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const handleSave = () => { if (!form.ldap_server_url) { showToast(t('settings.ldap.errors.serverRequired') || 'LDAP server URL is required', 'error'); return; } if (!form.ldap_search_base) { showToast(t('settings.ldap.errors.searchBaseRequired') || 'Search base DN is required', 'error'); return; } // Build the update payload — only include password if user entered one const update: Record = { ldap_server_url: form.ldap_server_url, ldap_bind_dn: form.ldap_bind_dn, ldap_search_base: form.ldap_search_base, ldap_user_filter: form.ldap_user_filter, ldap_security: form.ldap_security, ldap_group_mapping: form.ldap_group_mapping, ldap_auto_provision: form.ldap_auto_provision, ldap_default_group: form.ldap_default_group, }; if (form.ldap_bind_password) { update.ldap_bind_password = form.ldap_bind_password; } saveMutation.mutate(update as Partial); }; const handleToggle = () => { if (!authEnabled) { showToast(t('settings.ldap.errors.enableAuthFirst') || 'Enable authentication first', 'error'); return; } if (!ldapStatus?.ldap_enabled && !ldapStatus?.ldap_configured) { showToast(t('settings.ldap.errors.configureLdapFirst') || 'Save LDAP settings first', 'error'); return; } toggleMutation.mutate(!ldapStatus?.ldap_enabled); }; if (isLoading) { return (
); } const ldapEnabled = ldapStatus?.ldap_enabled ?? false; const inputClasses = "w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"; return (
{/* LDAP Toggle */}

{t('settings.ldap.title') || 'LDAP Authentication'}

{ldapEnabled ? (

{t('settings.ldap.enabledDesc') || 'LDAP authentication is enabled'}

  • {t('settings.ldap.feature1') || 'Users can login with LDAP credentials'}
  • {t('settings.ldap.feature2') || 'Local admin account remains as fallback'}
  • {t('settings.ldap.feature3') || 'LDAP groups are mapped to BamBuddy groups on login'}
) : (

{t('settings.ldap.disabledDesc') || 'LDAP authentication is disabled'}

{t('settings.ldap.disabledHint') || 'Configure and save LDAP settings below, then enable.'}

)}
{/* LDAP Server Configuration */}

{t('settings.ldap.serverConfig') || 'LDAP Server Configuration'}

{/* Server URL + Security (side by side) */}
setForm({ ...form, ldap_server_url: e.target.value })} />

{t('settings.ldap.serverUrlHint') || 'Use ldaps:// for SSL or ldap:// with StartTLS'}

{(['starttls', 'ldaps'] as const).map(sec => ( ))}

{t('settings.ldap.securityHint') || `Default port: ${SECURITY_PORT_MAP[form.ldap_security]}`}

{/* Bind DN + Password (side by side) */}
setForm({ ...form, ldap_bind_dn: e.target.value })} />
setForm({ ...form, ldap_bind_password: e.target.value })} />
{/* Search Base + User Filter (side by side) */}
setForm({ ...form, ldap_search_base: e.target.value })} />
setForm({ ...form, ldap_user_filter: e.target.value })} />
{/* Advanced (collapsed by default) */} {t('settings.ldap.advanced') || 'Advanced'} } className="border-t border-bambu-dark-tertiary pt-3" summaryClassName="py-1" >
{/* Auto Provision */}

{t('settings.ldap.autoProvisionHint') || 'Automatically create a BamBuddy account on first LDAP login'}

{/* Default Group (fallback for users with no mapped groups) */}

{t('settings.ldap.defaultGroupHint') || 'Fallback group assigned when an LDAP user authenticates but is not listed in any mapped group. Leave empty to leave unmapped users without permissions.'}

{/* Group Mapping */}