feat: LDAP-User-Sync in Mandanten-Benutzerliste

Neuer Endpoint POST /api/admin/tenants/{id}/ldap/sync importiert alle
LDAP-User (source=ldap) per UpsertLDAPUser in die Tenant-Benutzerliste.
Im Nutzer-Dialog erscheint ein "LDAP-Benutzer synchronisieren"-Button
wenn LDAP für den Mandanten aktiv ist. Unterstützt Univention UCS
(mailPrimaryAddress, inetOrgPerson). Benutzertabelle zeigt jetzt auch
die Quelle (local/ldap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-20 11:35:02 +01:00
parent 2da61689ea
commit 72b023b598
4 changed files with 218 additions and 1 deletions
+50 -1
View File
@@ -39,6 +39,8 @@ import {
saveAdminTenantLDAPConfig,
deleteAdminTenantLDAPConfig,
testAdminTenantLDAPConfig,
syncAdminTenantLDAP,
type LDAPSyncResult,
getAdminLabels,
createAdminLabel,
deleteAdminLabel,
@@ -266,6 +268,8 @@ export default function AdminPage() {
const [tenantUsers, setTenantUsers] = useState<User[]>([]);
const [tenantUsersLoading, setTenantUsersLoading] = useState(false);
const [tenantUsersError, setTenantUsersError] = useState("");
const [tenantUsersSyncing, setTenantUsersSyncing] = useState(false);
const [tenantUsersSyncResult, setTenantUsersSyncResult] = useState<LDAPSyncResult | null>(null);
// Labels state
const [adminLabels, setAdminLabels] = useState<MailLabel[]>([]);
@@ -786,6 +790,7 @@ export default function AdminPage() {
setTenantUsersLoading(true);
setTenantUsers([]);
setTenantUsersError("");
setTenantUsersSyncResult(null);
try {
const users = await getTenantUsers(t.id);
setTenantUsers(users || []);
@@ -796,6 +801,23 @@ export default function AdminPage() {
}
}
async function handleSyncLDAPUsers() {
if (!tenantUsersDialogId) return;
setTenantUsersSyncing(true);
setTenantUsersSyncResult(null);
try {
const result = await syncAdminTenantLDAP(tenantUsersDialogId);
setTenantUsersSyncResult(result);
// Reload user list after sync
const users = await getTenantUsers(tenantUsersDialogId);
setTenantUsers(users || []);
} catch (err: unknown) {
setTenantUsersSyncResult({ synced: 0, errors: [err instanceof Error ? err.message : "Sync fehlgeschlagen"] });
} finally {
setTenantUsersSyncing(false);
}
}
async function handleAddDomain() {
if (!domainDialogTenant || !newDomain) return;
setAddDomainLoading(true);
@@ -2889,7 +2911,7 @@ export default function AdminPage() {
<div className="py-4 text-center space-y-2">
<p className="text-sm text-muted-foreground">Keine lokalen Benutzer diesem Mandanten zugewiesen.</p>
{tenantUsersDialogLdap && (
<p className="text-xs text-muted-foreground">LDAP ist aktiv Benutzer erscheinen hier nach ihrem ersten Login.</p>
<p className="text-xs text-muted-foreground">LDAP ist aktiv Benutzer erscheinen hier nach ihrem ersten Login oder nach der Synchronisation.</p>
)}
</div>
) : (
@@ -2899,6 +2921,7 @@ export default function AdminPage() {
<TableHead>Benutzername</TableHead>
<TableHead>E-Mail</TableHead>
<TableHead>Rolle</TableHead>
<TableHead>Quelle</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
@@ -2908,6 +2931,7 @@ export default function AdminPage() {
<TableCell className="font-medium">{u.username}</TableCell>
<TableCell className="text-sm text-muted-foreground">{u.email}</TableCell>
<TableCell><Badge variant="outline">{u.role}</Badge></TableCell>
<TableCell><Badge variant="secondary">{u.source || "local"}</Badge></TableCell>
<TableCell>
<Badge variant={u.active ? "default" : "secondary"}>
{u.active ? "Aktiv" : "Inaktiv"}
@@ -2918,6 +2942,31 @@ export default function AdminPage() {
</TableBody>
</Table>
)}
{tenantUsersDialogLdap && (
<div className="border-t pt-3 space-y-2">
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={handleSyncLDAPUsers}
disabled={tenantUsersSyncing}
>
{tenantUsersSyncing ? "Synchronisiere..." : "LDAP-Benutzer synchronisieren"}
</Button>
{tenantUsersSyncResult && (
<span className="text-sm text-muted-foreground">
{tenantUsersSyncResult.synced} Benutzer synchronisiert
{tenantUsersSyncResult.errors.length > 0 && (
<span className="text-destructive ml-1">({tenantUsersSyncResult.errors.length} Fehler)</span>
)}
</span>
)}
</div>
{tenantUsersSyncResult?.errors?.length > 0 && (
<p className="text-xs text-destructive font-mono">{tenantUsersSyncResult.errors.join(", ")}</p>
)}
</div>
)}
</DialogContent>
</Dialog>
+10
View File
@@ -48,6 +48,7 @@ export interface User {
username: string;
email: string;
role: string;
source?: string;
active: boolean;
tenant_id?: number;
}
@@ -879,6 +880,15 @@ export async function testAdminTenantLDAPConfig(
});
}
export interface LDAPSyncResult {
synced: number;
errors: string[];
}
export async function syncAdminTenantLDAP(tenantID: number): Promise<LDAPSyncResult> {
return request<LDAPSyncResult>(`/api/admin/tenants/${tenantID}/ldap/sync`, { method: "POST", body: "{}" });
}
// ── Profil-Einstellungen ──────────────────────────────────────────────────
export async function changePassword(