diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1f96964..f3b824e 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -30,6 +30,14 @@ import { getTenantDomains, addTenantDomain, removeTenantDomain, + getTenantLDAPConfig, + saveTenantLDAPConfig, + deleteTenantLDAPConfig, + testTenantLDAPConfig, + getAdminTenantLDAPConfig, + saveAdminTenantLDAPConfig, + deleteAdminTenantLDAPConfig, + testAdminTenantLDAPConfig, type User, type AuditEntry, type SMTPStatus, @@ -41,6 +49,7 @@ import { type SecurityAuditResult, type LDAPConfig, type LDAPTestResult, + type TenantLDAPConfig, type Tenant, type TenantDomain, } from "@/lib/api"; @@ -192,6 +201,30 @@ export default function AdminPage() { const [addDomainLoading, setAddDomainLoading] = useState(false); const [domainError, setDomainError] = useState(""); + // Tenant LDAP state (domain_admin own tenant) + const [tenantLdapConfig, setTenantLdapConfig] = useState(null); + const [tenantLdapLoading, setTenantLdapLoading] = useState(false); + const [tenantLdapSaving, setTenantLdapSaving] = useState(false); + const [tenantLdapTesting, setTenantLdapTesting] = useState(false); + const [tenantLdapError, setTenantLdapError] = useState(""); + const [tenantLdapTestResult, setTenantLdapTestResult] = useState(null); + const [tenantLdapForm, setTenantLdapForm] = useState({ + enabled: false, + url: "ldap://", + bind_dn: "", + bind_password: "", + base_dn: "", + user_filter: "(sAMAccountName=%s)", + tls: false, + tls_skip_verify: false, + default_role: "user", + group_mappings: [], + }); + const [tenantLdapChangePassword, setTenantLdapChangePassword] = useState(false); + + // Superadmin: tenant LDAP dialog + const [tenantLdapDialogId, setTenantLdapDialogId] = useState(null); + const loadDashboard = useCallback(async () => { setDashLoading(true); try { @@ -599,6 +632,78 @@ export default function AdminPage() { } } + // Tenant LDAP handlers (domain_admin) + const loadTenantLDAP = useCallback(async () => { + setTenantLdapLoading(true); + setTenantLdapError(""); + try { + const cfg = await getTenantLDAPConfig(); + if (cfg) { + setTenantLdapConfig(cfg); + setTenantLdapForm({ ...cfg, bind_password: "" }); + setTenantLdapChangePassword(false); + } + } catch { + setTenantLdapError("LDAP-Konfiguration konnte nicht geladen werden."); + } finally { + setTenantLdapLoading(false); + } + }, []); + + async function handleSaveTenantLDAP(e: React.FormEvent) { + e.preventDefault(); + setTenantLdapSaving(true); + setTenantLdapError(""); + try { + const payload: Partial = { ...tenantLdapForm }; + if (!tenantLdapChangePassword) { + delete payload.bind_password; + } + await saveTenantLDAPConfig(payload); + await loadTenantLDAP(); + } catch (err: unknown) { + setTenantLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen."); + } finally { + setTenantLdapSaving(false); + } + } + + async function handleTestTenantLDAP() { + setTenantLdapTesting(true); + setTenantLdapError(""); + setTenantLdapTestResult(null); + try { + const payload = tenantLdapConfig + ? { use_saved: true } + : { use_saved: false, ...tenantLdapForm }; + const result = await testTenantLDAPConfig(payload as Parameters[0]); + setTenantLdapTestResult(result); + } catch (err: unknown) { + setTenantLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen."); + } finally { + setTenantLdapTesting(false); + } + } + + async function handleDeleteTenantLDAP() { + setTenantLdapSaving(true); + setTenantLdapError(""); + try { + await deleteTenantLDAPConfig(); + setTenantLdapConfig(null); + setTenantLdapForm({ + enabled: false, url: "ldap://", bind_dn: "", bind_password: "", + base_dn: "", user_filter: "(sAMAccountName=%s)", tls: false, + tls_skip_verify: false, default_role: "user", group_mappings: [], + }); + setTenantLdapTestResult(null); + } catch (err: unknown) { + setTenantLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); + } finally { + setTenantLdapSaving(false); + } + } + const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE); return ( @@ -621,7 +726,10 @@ export default function AdminPage() { Benutzer Audit-Log Import - {isSuperAdmin && LDAP} + {isSuperAdmin && LDAP (Global)} + {!isSuperAdmin && user?.role === "domain_admin" && ( + LDAP + )} {isSuperAdmin && Security} {isSuperAdmin && Mandanten} {isSuperAdmin && Module} @@ -1850,6 +1958,266 @@ export default function AdminPage() { )} + {/* ── Tenant LDAP (domain_admin) ── */} + +
+
+

LDAP / Active Directory — Mandantenkonfiguration

+

Konfiguriere den LDAP-Server für deinen Mandanten.

+
+
+ LDAP aktiviert + setTenantLdapForm((f) => ({ ...f, enabled: e.target.checked }))} + /> +
+
+ + {tenantLdapError && ( + + {tenantLdapError} + + )} + + {tenantLdapLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : ( +
+ + +
+
+ + setTenantLdapForm((f) => ({ ...f, url: e.target.value }))} + /> +
+
+ + setTenantLdapForm((f) => ({ ...f, bind_dn: e.target.value }))} + /> +
+
+ + {tenantLdapConfig && !tenantLdapChangePassword ? ( +
+ + +
+ ) : ( + setTenantLdapForm((f) => ({ ...f, bind_password: e.target.value }))} + /> + )} +
+
+ + setTenantLdapForm((f) => ({ ...f, base_dn: e.target.value }))} + /> +
+
+ + setTenantLdapForm((f) => ({ ...f, user_filter: e.target.value }))} + /> +
+
+ + +
+
+ + + +
+ + +
+ + + + {/* Group mappings -- domain_admin: nur user + auditor */} +
+
+ + +
+ {tenantLdapForm.group_mappings.length === 0 ? ( +

Keine Gruppen-Zuordnungen definiert.

+ ) : ( +
+ {tenantLdapForm.group_mappings.map((gm, i) => ( +
+ { + const gms = [...tenantLdapForm.group_mappings]; + gms[i] = { ...gms[i], group_dn: e.target.value }; + setTenantLdapForm((f) => ({ ...f, group_mappings: gms })); + }} + /> + + +
+ ))} +
+ )} +
+
+
+ + {/* Test result */} + {tenantLdapTestResult && ( + + +
+ + {tenantLdapTestResult.ok ? "Verbunden" : "Fehler"} + + {tenantLdapTestResult.message} + {tenantLdapTestResult.latency_ms > 0 && ( + {tenantLdapTestResult.latency_ms} ms + )} +
+ {tenantLdapTestResult.server_info && ( +

{tenantLdapTestResult.server_info}

+ )} + {tenantLdapTestResult.users_found > 0 && ( +

{tenantLdapTestResult.users_found} Benutzer gefunden

+ )} + {tenantLdapTestResult.error_detail && ( +

{tenantLdapTestResult.error_detail}

+ )} +
+
+ )} + + {/* Action bar */} +
+ + + {tenantLdapConfig && ( + + )} +
+
+ )} + + {tenantLdapConfig && ( +

+ Zuletzt geändert: {tenantLdapConfig.updated_at ? new Date(tenantLdapConfig.updated_at).toLocaleString("de-DE") : "–"} + {tenantLdapConfig.updated_by ? ` von ${tenantLdapConfig.updated_by}` : ""} +

+ )} +
+ {/* ── Mandanten ── */}
@@ -1927,6 +2295,7 @@ export default function AdminPage() { Slug Domains Nutzer + LDAP Status Aktionen @@ -1938,6 +2307,15 @@ export default function AdminPage() { {t.slug} {t.domain_count ?? 0} {t.user_count ?? 0} + + {t.ldap_enabled === true ? ( + Aktiv + ) : t.ldap_url ? ( + Deaktiviert + ) : ( + + )} + {t.active ? "Aktiv" : "Inaktiv"} @@ -1948,6 +2326,9 @@ export default function AdminPage() { + @@ -2031,6 +2412,14 @@ export default function AdminPage() {
+ + {/* Tenant LDAP dialog (superadmin) */} + {tenantLdapDialogId !== null && ( + { setTenantLdapDialogId(null); loadTenants(); }} + /> + )}
@@ -2226,3 +2615,362 @@ function ModulesTab() { ); } + +// ── Tenant LDAP Dialog (superadmin) ───────────────────────────────────────── + +function TenantLDAPDialog({ tenantID, onClose }: { tenantID: number; onClose: () => void }) { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [error, setError] = useState(""); + const [testResult, setTestResult] = useState(null); + const [changePassword, setChangePassword] = useState(false); + const [form, setForm] = useState({ + enabled: false, + url: "ldap://", + bind_dn: "", + bind_password: "", + base_dn: "", + user_filter: "(sAMAccountName=%s)", + tls: false, + tls_skip_verify: false, + default_role: "user", + group_mappings: [], + }); + + const loadConfig = useCallback(async () => { + setLoading(true); + setError(""); + try { + const cfg = await getAdminTenantLDAPConfig(tenantID); + if (cfg) { + setConfig(cfg); + setForm({ ...cfg, bind_password: "" }); + setChangePassword(false); + } + } catch { + setError("LDAP-Konfiguration konnte nicht geladen werden."); + } finally { + setLoading(false); + } + }, [tenantID]); + + useEffect(() => { + loadConfig(); + }, [loadConfig]); + + async function handleSave(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + setError(""); + try { + const payload: Partial = { ...form }; + if (!changePassword) { + delete payload.bind_password; + } + await saveAdminTenantLDAPConfig(tenantID, payload); + await loadConfig(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Speichern fehlgeschlagen."); + } finally { + setSaving(false); + } + } + + async function handleTest() { + setTesting(true); + setError(""); + setTestResult(null); + try { + const payload = config + ? { use_saved: true } + : { use_saved: false, ...form }; + const result = await testAdminTenantLDAPConfig(tenantID, payload as Parameters[1]); + setTestResult(result); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Test fehlgeschlagen."); + } finally { + setTesting(false); + } + } + + async function handleDelete() { + setSaving(true); + setError(""); + try { + await deleteAdminTenantLDAPConfig(tenantID); + setConfig(null); + setForm({ + enabled: false, url: "ldap://", bind_dn: "", bind_password: "", + base_dn: "", user_filter: "(sAMAccountName=%s)", tls: false, + tls_skip_verify: false, default_role: "user", group_mappings: [], + }); + setTestResult(null); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); + } finally { + setSaving(false); + } + } + + return ( + { if (!open) onClose(); }}> + + + LDAP-Konfiguration (Mandant #{tenantID}) + LDAP-Server für diesen Mandanten konfigurieren. + + + {error && ( + + {error} + + )} + + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : ( +
+
+ LDAP aktiviert + setForm((f) => ({ ...f, enabled: e.target.checked }))} + /> +
+ +
+
+ + setForm((f) => ({ ...f, url: e.target.value }))} + /> +
+
+ + setForm((f) => ({ ...f, bind_dn: e.target.value }))} + /> +
+
+ + {config && !changePassword ? ( +
+ + +
+ ) : ( + setForm((f) => ({ ...f, bind_password: e.target.value }))} + /> + )} +
+
+ + setForm((f) => ({ ...f, base_dn: e.target.value }))} + /> +
+
+ + setForm((f) => ({ ...f, user_filter: e.target.value }))} + /> +
+
+ + +
+
+ + + +
+ + +
+ + + + {/* Group mappings -- superadmin per tenant: bis domain_admin */} +
+
+ + +
+ {form.group_mappings.length === 0 ? ( +

Keine Gruppen-Zuordnungen definiert.

+ ) : ( +
+ {form.group_mappings.map((gm, i) => ( +
+ { + const gms = [...form.group_mappings]; + gms[i] = { ...gms[i], group_dn: e.target.value }; + setForm((f) => ({ ...f, group_mappings: gms })); + }} + /> + + +
+ ))} +
+ )} +
+ + {/* Test result */} + {testResult && ( + + +
+ + {testResult.ok ? "Verbunden" : "Fehler"} + + {testResult.message} + {testResult.latency_ms > 0 && ( + {testResult.latency_ms} ms + )} +
+ {testResult.server_info && ( +

{testResult.server_info}

+ )} + {testResult.users_found > 0 && ( +

{testResult.users_found} Benutzer gefunden

+ )} + {testResult.error_detail && ( +

{testResult.error_detail}

+ )} +
+
+ )} + + {/* Action bar */} +
+ + + {config && ( + + )} +
+ + {config && ( +

+ Zuletzt geändert: {config.updated_at ? new Date(config.updated_at).toLocaleString("de-DE") : "–"} + {config.updated_by ? ` von ${config.updated_by}` : ""} +

+ )} + + )} +
+
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 0376d30..04bd476 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -678,6 +678,8 @@ export interface Tenant { created_at: string; domain_count?: number; user_count?: number; + ldap_enabled?: boolean; + ldap_url?: string; } export interface TenantDomain { @@ -734,3 +736,66 @@ export async function removeTenantDomain( method: "DELETE", }); } + +// ── PROJ-23: Pro-Mandant LDAP (tenant_ldap) ────────────────────────────── + +export interface TenantLDAPConfig extends LDAPConfig { + tenant_id?: number; +} + +// domain_admin -- eigener Mandant +export async function getTenantLDAPConfig(): Promise { + try { + return await request("/api/tenant/ldap"); + } catch (e: unknown) { + if (e instanceof Error && e.message.includes("404")) return null; + if (e instanceof Error && e.message.includes("no ldap config")) return null; + throw e; + } +} + +export async function saveTenantLDAPConfig(cfg: Partial): Promise { + await request("/api/tenant/ldap", { method: "PUT", body: JSON.stringify(cfg) }); +} + +export async function deleteTenantLDAPConfig(): Promise { + await request("/api/tenant/ldap", { method: "DELETE" }); +} + +export async function testTenantLDAPConfig( + payload: { use_saved: boolean } & Partial +): Promise { + return request("/api/tenant/ldap/test", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +// superadmin -- beliebiger Mandant per ID +export async function getAdminTenantLDAPConfig(tenantID: number): Promise { + try { + return await request(`/api/admin/tenants/${tenantID}/ldap`); + } catch (e: unknown) { + if (e instanceof Error && e.message.includes("404")) return null; + if (e instanceof Error && e.message.includes("no ldap config")) return null; + throw e; + } +} + +export async function saveAdminTenantLDAPConfig(tenantID: number, cfg: Partial): Promise { + await request(`/api/admin/tenants/${tenantID}/ldap`, { method: "PUT", body: JSON.stringify(cfg) }); +} + +export async function deleteAdminTenantLDAPConfig(tenantID: number): Promise { + await request(`/api/admin/tenants/${tenantID}/ldap`, { method: "DELETE" }); +} + +export async function testAdminTenantLDAPConfig( + tenantID: number, + payload: { use_saved: boolean } & Partial +): Promise { + return request(`/api/admin/tenants/${tenantID}/ldap/test`, { + method: "POST", + body: JSON.stringify(payload), + }); +}