feat(PROJ-23): Pro-Mandant LDAP Frontend
- domain_admin: neuer LDAP-Tab mit Tenant-Konfiguration (eigener Mandant) - Gruppen-Mapping auf user/auditor beschränkt (keine Eskalation) - Nutzt /api/tenant/ldap Endpunkte - superadmin: globaler LDAP-Tab umbenannt zu "LDAP (Global)" - superadmin: Mandantentabelle um LDAP-Statusspalte erweitert - Badge: Aktiv / Deaktiviert / --- - TenantLDAPDialog pro Mandant (alle Rollen bis domain_admin) - api.ts: TenantLDAPConfig Interface + 8 neue API-Funktionen - getTenantLDAPConfig/save/delete/test (domain_admin) - getAdminTenantLDAPConfig/save/delete/test (superadmin) - Tenant Interface: ldap_enabled + ldap_url Felder ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+749
-1
@@ -30,6 +30,14 @@ import {
|
|||||||
getTenantDomains,
|
getTenantDomains,
|
||||||
addTenantDomain,
|
addTenantDomain,
|
||||||
removeTenantDomain,
|
removeTenantDomain,
|
||||||
|
getTenantLDAPConfig,
|
||||||
|
saveTenantLDAPConfig,
|
||||||
|
deleteTenantLDAPConfig,
|
||||||
|
testTenantLDAPConfig,
|
||||||
|
getAdminTenantLDAPConfig,
|
||||||
|
saveAdminTenantLDAPConfig,
|
||||||
|
deleteAdminTenantLDAPConfig,
|
||||||
|
testAdminTenantLDAPConfig,
|
||||||
type User,
|
type User,
|
||||||
type AuditEntry,
|
type AuditEntry,
|
||||||
type SMTPStatus,
|
type SMTPStatus,
|
||||||
@@ -41,6 +49,7 @@ import {
|
|||||||
type SecurityAuditResult,
|
type SecurityAuditResult,
|
||||||
type LDAPConfig,
|
type LDAPConfig,
|
||||||
type LDAPTestResult,
|
type LDAPTestResult,
|
||||||
|
type TenantLDAPConfig,
|
||||||
type Tenant,
|
type Tenant,
|
||||||
type TenantDomain,
|
type TenantDomain,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
@@ -192,6 +201,30 @@ export default function AdminPage() {
|
|||||||
const [addDomainLoading, setAddDomainLoading] = useState(false);
|
const [addDomainLoading, setAddDomainLoading] = useState(false);
|
||||||
const [domainError, setDomainError] = useState("");
|
const [domainError, setDomainError] = useState("");
|
||||||
|
|
||||||
|
// Tenant LDAP state (domain_admin own tenant)
|
||||||
|
const [tenantLdapConfig, setTenantLdapConfig] = useState<TenantLDAPConfig | null>(null);
|
||||||
|
const [tenantLdapLoading, setTenantLdapLoading] = useState(false);
|
||||||
|
const [tenantLdapSaving, setTenantLdapSaving] = useState(false);
|
||||||
|
const [tenantLdapTesting, setTenantLdapTesting] = useState(false);
|
||||||
|
const [tenantLdapError, setTenantLdapError] = useState("");
|
||||||
|
const [tenantLdapTestResult, setTenantLdapTestResult] = useState<LDAPTestResult | null>(null);
|
||||||
|
const [tenantLdapForm, setTenantLdapForm] = useState<TenantLDAPConfig>({
|
||||||
|
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<number | null>(null);
|
||||||
|
|
||||||
const loadDashboard = useCallback(async () => {
|
const loadDashboard = useCallback(async () => {
|
||||||
setDashLoading(true);
|
setDashLoading(true);
|
||||||
try {
|
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<TenantLDAPConfig> = { ...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<typeof testTenantLDAPConfig>[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);
|
const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -621,7 +726,10 @@ export default function AdminPage() {
|
|||||||
<TabsTrigger value="users">Benutzer</TabsTrigger>
|
<TabsTrigger value="users">Benutzer</TabsTrigger>
|
||||||
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
|
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
|
||||||
<TabsTrigger value="import">Import</TabsTrigger>
|
<TabsTrigger value="import">Import</TabsTrigger>
|
||||||
{isSuperAdmin && <TabsTrigger value="ldap" onClick={loadLDAP}>LDAP</TabsTrigger>}
|
{isSuperAdmin && <TabsTrigger value="ldap" onClick={loadLDAP}>LDAP (Global)</TabsTrigger>}
|
||||||
|
{!isSuperAdmin && user?.role === "domain_admin" && (
|
||||||
|
<TabsTrigger value="tenant-ldap" onClick={loadTenantLDAP}>LDAP</TabsTrigger>
|
||||||
|
)}
|
||||||
{isSuperAdmin && <TabsTrigger value="security">Security</TabsTrigger>}
|
{isSuperAdmin && <TabsTrigger value="security">Security</TabsTrigger>}
|
||||||
{isSuperAdmin && <TabsTrigger value="tenants" onClick={loadTenants}>Mandanten</TabsTrigger>}
|
{isSuperAdmin && <TabsTrigger value="tenants" onClick={loadTenants}>Mandanten</TabsTrigger>}
|
||||||
{isSuperAdmin && <TabsTrigger value="modules">Module</TabsTrigger>}
|
{isSuperAdmin && <TabsTrigger value="modules">Module</TabsTrigger>}
|
||||||
@@ -1850,6 +1958,266 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ── Tenant LDAP (domain_admin) ── */}
|
||||||
|
<TabsContent value="tenant-ldap" className="mt-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">LDAP / Active Directory — Mandantenkonfiguration</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Konfiguriere den LDAP-Server für deinen Mandanten.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">LDAP aktiviert</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={tenantLdapForm.enabled}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, enabled: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tenantLdapError && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>{tenantLdapError}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tenantLdapLoading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-10 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSaveTenantLDAP} className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-url">Server-URL</Label>
|
||||||
|
<Input
|
||||||
|
id="tldap-url"
|
||||||
|
placeholder="ldap://dc.example.com:389"
|
||||||
|
value={tenantLdapForm.url}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, url: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-bind-dn">Bind-DN (Service-Account)</Label>
|
||||||
|
<Input
|
||||||
|
id="tldap-bind-dn"
|
||||||
|
placeholder="CN=archivmail,OU=Service,DC=example,DC=com"
|
||||||
|
value={tenantLdapForm.bind_dn}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, bind_dn: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-pw">Bind-Passwort</Label>
|
||||||
|
{tenantLdapConfig && !tenantLdapChangePassword ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input id="tldap-pw" type="password" value="••••••" readOnly className="flex-1" />
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={() => setTenantLdapChangePassword(true)}>
|
||||||
|
Ändern
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id="tldap-pw"
|
||||||
|
type="password"
|
||||||
|
placeholder="Neues Passwort eingeben"
|
||||||
|
value={tenantLdapForm.bind_password}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, bind_password: e.target.value }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-base-dn">Base-DN</Label>
|
||||||
|
<Input
|
||||||
|
id="tldap-base-dn"
|
||||||
|
placeholder="DC=example,DC=com"
|
||||||
|
value={tenantLdapForm.base_dn}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, base_dn: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-filter">User-Filter</Label>
|
||||||
|
<Input
|
||||||
|
id="tldap-filter"
|
||||||
|
placeholder="(sAMAccountName=%s)"
|
||||||
|
value={tenantLdapForm.user_filter}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, user_filter: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tldap-role">Standard-Rolle</Label>
|
||||||
|
<Select
|
||||||
|
value={tenantLdapForm.default_role}
|
||||||
|
onValueChange={(v) => setTenantLdapForm((f) => ({ ...f, default_role: v }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="tldap-role">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="auditor">Auditor</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-6">
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={tenantLdapForm.tls}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, tls: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
STARTTLS verwenden
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={tenantLdapForm.tls_skip_verify}
|
||||||
|
onChange={(e) => setTenantLdapForm((f) => ({ ...f, tls_skip_verify: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
TLS-Zertifikat ignorieren
|
||||||
|
{tenantLdapForm.tls_skip_verify && (
|
||||||
|
<Badge variant="destructive" className="text-xs ml-1">Unsicher</Badge>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Group mappings -- domain_admin: nur user + auditor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Gruppen-Rollenzuordnung</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setTenantLdapForm((f) => ({
|
||||||
|
...f,
|
||||||
|
group_mappings: [...f.group_mappings, { group_dn: "", role: "user" }],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Hinzufuegen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{tenantLdapForm.group_mappings.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Keine Gruppen-Zuordnungen definiert.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tenantLdapForm.group_mappings.map((gm, i) => (
|
||||||
|
<div key={i} className="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
placeholder="CN=Archivadmins,OU=Groups,DC=example,DC=com"
|
||||||
|
value={gm.group_dn}
|
||||||
|
onChange={(e) => {
|
||||||
|
const gms = [...tenantLdapForm.group_mappings];
|
||||||
|
gms[i] = { ...gms[i], group_dn: e.target.value };
|
||||||
|
setTenantLdapForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={gm.role}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const gms = [...tenantLdapForm.group_mappings];
|
||||||
|
gms[i] = { ...gms[i], role: v };
|
||||||
|
setTenantLdapForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="auditor">Auditor</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const gms = tenantLdapForm.group_mappings.filter((_, j) => j !== i);
|
||||||
|
setTenantLdapForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Test result */}
|
||||||
|
{tenantLdapTestResult && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={tenantLdapTestResult.ok ? "default" : "destructive"}>
|
||||||
|
{tenantLdapTestResult.ok ? "Verbunden" : "Fehler"}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm">{tenantLdapTestResult.message}</span>
|
||||||
|
{tenantLdapTestResult.latency_ms > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">{tenantLdapTestResult.latency_ms} ms</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{tenantLdapTestResult.server_info && (
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">{tenantLdapTestResult.server_info}</p>
|
||||||
|
)}
|
||||||
|
{tenantLdapTestResult.users_found > 0 && (
|
||||||
|
<p className="text-sm">{tenantLdapTestResult.users_found} Benutzer gefunden</p>
|
||||||
|
)}
|
||||||
|
{tenantLdapTestResult.error_detail && (
|
||||||
|
<p className="text-xs text-destructive font-mono">{tenantLdapTestResult.error_detail}</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action bar */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={handleTestTenantLDAP} disabled={tenantLdapTesting || tenantLdapSaving}>
|
||||||
|
{tenantLdapTesting ? "Teste..." : "Verbindung testen"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={tenantLdapSaving || tenantLdapTesting}>
|
||||||
|
{tenantLdapSaving ? "Speichern..." : "Speichern"}
|
||||||
|
</Button>
|
||||||
|
{tenantLdapConfig && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={tenantLdapSaving}
|
||||||
|
onClick={handleDeleteTenantLDAP}
|
||||||
|
>
|
||||||
|
Konfiguration löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tenantLdapConfig && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Zuletzt geändert: {tenantLdapConfig.updated_at ? new Date(tenantLdapConfig.updated_at).toLocaleString("de-DE") : "–"}
|
||||||
|
{tenantLdapConfig.updated_by ? ` von ${tenantLdapConfig.updated_by}` : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* ── Mandanten ── */}
|
{/* ── Mandanten ── */}
|
||||||
<TabsContent value="tenants" className="mt-4 space-y-4">
|
<TabsContent value="tenants" className="mt-4 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -1927,6 +2295,7 @@ export default function AdminPage() {
|
|||||||
<TableHead>Slug</TableHead>
|
<TableHead>Slug</TableHead>
|
||||||
<TableHead className="text-center">Domains</TableHead>
|
<TableHead className="text-center">Domains</TableHead>
|
||||||
<TableHead className="text-center">Nutzer</TableHead>
|
<TableHead className="text-center">Nutzer</TableHead>
|
||||||
|
<TableHead>LDAP</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Aktionen</TableHead>
|
<TableHead className="text-right">Aktionen</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -1938,6 +2307,15 @@ export default function AdminPage() {
|
|||||||
<TableCell className="font-mono text-xs text-muted-foreground">{t.slug}</TableCell>
|
<TableCell className="font-mono text-xs text-muted-foreground">{t.slug}</TableCell>
|
||||||
<TableCell className="text-center">{t.domain_count ?? 0}</TableCell>
|
<TableCell className="text-center">{t.domain_count ?? 0}</TableCell>
|
||||||
<TableCell className="text-center">{t.user_count ?? 0}</TableCell>
|
<TableCell className="text-center">{t.user_count ?? 0}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{t.ldap_enabled === true ? (
|
||||||
|
<Badge variant="default" className="bg-green-100 text-green-800 hover:bg-green-100">Aktiv</Badge>
|
||||||
|
) : t.ldap_url ? (
|
||||||
|
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">Deaktiviert</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">—</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={t.active ? "default" : "secondary"}>
|
<Badge variant={t.active ? "default" : "secondary"}>
|
||||||
{t.active ? "Aktiv" : "Inaktiv"}
|
{t.active ? "Aktiv" : "Inaktiv"}
|
||||||
@@ -1948,6 +2326,9 @@ export default function AdminPage() {
|
|||||||
<Button size="sm" variant="outline" onClick={() => openDomainDialog(t)}>
|
<Button size="sm" variant="outline" onClick={() => openDomainDialog(t)}>
|
||||||
Domains
|
Domains
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => setTenantLdapDialogId(t.id)}>
|
||||||
|
LDAP
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => handleToggleTenant(t)}>
|
<Button size="sm" variant="outline" onClick={() => handleToggleTenant(t)}>
|
||||||
{t.active ? "Deaktivieren" : "Aktivieren"}
|
{t.active ? "Deaktivieren" : "Aktivieren"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -2031,6 +2412,14 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Tenant LDAP dialog (superadmin) */}
|
||||||
|
{tenantLdapDialogId !== null && (
|
||||||
|
<TenantLDAPDialog
|
||||||
|
tenantID={tenantLdapDialogId}
|
||||||
|
onClose={() => { setTenantLdapDialogId(null); loadTenants(); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="modules" className="mt-4">
|
<TabsContent value="modules" className="mt-4">
|
||||||
@@ -2226,3 +2615,362 @@ function ModulesTab() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tenant LDAP Dialog (superadmin) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
function TenantLDAPDialog({ tenantID, onClose }: { tenantID: number; onClose: () => void }) {
|
||||||
|
const [config, setConfig] = useState<TenantLDAPConfig | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [testResult, setTestResult] = useState<LDAPTestResult | null>(null);
|
||||||
|
const [changePassword, setChangePassword] = useState(false);
|
||||||
|
const [form, setForm] = useState<TenantLDAPConfig>({
|
||||||
|
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<TenantLDAPConfig> = { ...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<typeof testAdminTenantLDAPConfig>[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 (
|
||||||
|
<Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>LDAP-Konfiguration (Mandant #{tenantID})</DialogTitle>
|
||||||
|
<DialogDescription>LDAP-Server für diesen Mandanten konfigurieren.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-10 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSave} className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">LDAP aktiviert</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={form.enabled}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, enabled: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-url-${tenantID}`}>Server-URL</Label>
|
||||||
|
<Input
|
||||||
|
id={`atldap-url-${tenantID}`}
|
||||||
|
placeholder="ldap://dc.example.com:389"
|
||||||
|
value={form.url}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, url: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-bind-dn-${tenantID}`}>Bind-DN (Service-Account)</Label>
|
||||||
|
<Input
|
||||||
|
id={`atldap-bind-dn-${tenantID}`}
|
||||||
|
placeholder="CN=archivmail,OU=Service,DC=example,DC=com"
|
||||||
|
value={form.bind_dn}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, bind_dn: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-pw-${tenantID}`}>Bind-Passwort</Label>
|
||||||
|
{config && !changePassword ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input id={`atldap-pw-${tenantID}`} type="password" value="••••••" readOnly className="flex-1" />
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={() => setChangePassword(true)}>
|
||||||
|
Ändern
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={`atldap-pw-${tenantID}`}
|
||||||
|
type="password"
|
||||||
|
placeholder="Neues Passwort eingeben"
|
||||||
|
value={form.bind_password}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, bind_password: e.target.value }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-base-dn-${tenantID}`}>Base-DN</Label>
|
||||||
|
<Input
|
||||||
|
id={`atldap-base-dn-${tenantID}`}
|
||||||
|
placeholder="DC=example,DC=com"
|
||||||
|
value={form.base_dn}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, base_dn: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-filter-${tenantID}`}>User-Filter</Label>
|
||||||
|
<Input
|
||||||
|
id={`atldap-filter-${tenantID}`}
|
||||||
|
placeholder="(sAMAccountName=%s)"
|
||||||
|
value={form.user_filter}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, user_filter: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`atldap-role-${tenantID}`}>Standard-Rolle</Label>
|
||||||
|
<Select
|
||||||
|
value={form.default_role}
|
||||||
|
onValueChange={(v) => setForm((f) => ({ ...f, default_role: v }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`atldap-role-${tenantID}`}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="auditor">Auditor</SelectItem>
|
||||||
|
<SelectItem value="domain_admin">Domain Admin</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-6">
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={form.tls}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, tls: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
STARTTLS verwenden
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
checked={form.tls_skip_verify}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, tls_skip_verify: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
TLS-Zertifikat ignorieren
|
||||||
|
{form.tls_skip_verify && (
|
||||||
|
<Badge variant="destructive" className="text-xs ml-1">Unsicher</Badge>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Group mappings -- superadmin per tenant: bis domain_admin */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Gruppen-Rollenzuordnung</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
group_mappings: [...f.group_mappings, { group_dn: "", role: "user" }],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Hinzufuegen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{form.group_mappings.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Keine Gruppen-Zuordnungen definiert.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{form.group_mappings.map((gm, i) => (
|
||||||
|
<div key={i} className="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
placeholder="CN=Archivadmins,OU=Groups,DC=example,DC=com"
|
||||||
|
value={gm.group_dn}
|
||||||
|
onChange={(e) => {
|
||||||
|
const gms = [...form.group_mappings];
|
||||||
|
gms[i] = { ...gms[i], group_dn: e.target.value };
|
||||||
|
setForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={gm.role}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const gms = [...form.group_mappings];
|
||||||
|
gms[i] = { ...gms[i], role: v };
|
||||||
|
setForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="auditor">Auditor</SelectItem>
|
||||||
|
<SelectItem value="domain_admin">Domain Admin</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const gms = form.group_mappings.filter((_, j) => j !== i);
|
||||||
|
setForm((f) => ({ ...f, group_mappings: gms }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test result */}
|
||||||
|
{testResult && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={testResult.ok ? "default" : "destructive"}>
|
||||||
|
{testResult.ok ? "Verbunden" : "Fehler"}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm">{testResult.message}</span>
|
||||||
|
{testResult.latency_ms > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">{testResult.latency_ms} ms</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{testResult.server_info && (
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">{testResult.server_info}</p>
|
||||||
|
)}
|
||||||
|
{testResult.users_found > 0 && (
|
||||||
|
<p className="text-sm">{testResult.users_found} Benutzer gefunden</p>
|
||||||
|
)}
|
||||||
|
{testResult.error_detail && (
|
||||||
|
<p className="text-xs text-destructive font-mono">{testResult.error_detail}</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action bar */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={handleTest} disabled={testing || saving}>
|
||||||
|
{testing ? "Teste..." : "Verbindung testen"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={saving || testing}>
|
||||||
|
{saving ? "Speichern..." : "Speichern"}
|
||||||
|
</Button>
|
||||||
|
{config && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Konfiguration löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Zuletzt geändert: {config.updated_at ? new Date(config.updated_at).toLocaleString("de-DE") : "–"}
|
||||||
|
{config.updated_by ? ` von ${config.updated_by}` : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -678,6 +678,8 @@ export interface Tenant {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
domain_count?: number;
|
domain_count?: number;
|
||||||
user_count?: number;
|
user_count?: number;
|
||||||
|
ldap_enabled?: boolean;
|
||||||
|
ldap_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantDomain {
|
export interface TenantDomain {
|
||||||
@@ -734,3 +736,66 @@ export async function removeTenantDomain(
|
|||||||
method: "DELETE",
|
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<TenantLDAPConfig | null> {
|
||||||
|
try {
|
||||||
|
return await request<TenantLDAPConfig>("/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<TenantLDAPConfig>): Promise<void> {
|
||||||
|
await request<void>("/api/tenant/ldap", { method: "PUT", body: JSON.stringify(cfg) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTenantLDAPConfig(): Promise<void> {
|
||||||
|
await request<void>("/api/tenant/ldap", { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testTenantLDAPConfig(
|
||||||
|
payload: { use_saved: boolean } & Partial<TenantLDAPConfig>
|
||||||
|
): Promise<LDAPTestResult> {
|
||||||
|
return request<LDAPTestResult>("/api/tenant/ldap/test", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// superadmin -- beliebiger Mandant per ID
|
||||||
|
export async function getAdminTenantLDAPConfig(tenantID: number): Promise<TenantLDAPConfig | null> {
|
||||||
|
try {
|
||||||
|
return await request<TenantLDAPConfig>(`/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<TenantLDAPConfig>): Promise<void> {
|
||||||
|
await request<void>(`/api/admin/tenants/${tenantID}/ldap`, { method: "PUT", body: JSON.stringify(cfg) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminTenantLDAPConfig(tenantID: number): Promise<void> {
|
||||||
|
await request<void>(`/api/admin/tenants/${tenantID}/ldap`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testAdminTenantLDAPConfig(
|
||||||
|
tenantID: number,
|
||||||
|
payload: { use_saved: boolean } & Partial<TenantLDAPConfig>
|
||||||
|
): Promise<LDAPTestResult> {
|
||||||
|
return request<LDAPTestResult>(`/api/admin/tenants/${tenantID}/ldap/test`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user