chore: admin/page.tsx in Einzelkomponenten aufteilen (3917 → 1304 Zeilen)

- Tab-Sektionen → src/components/admin/tabs/ (11 Dateien)
- Dialoge → src/components/admin/ (TenantLDAPDialog, UserDialogs)
- Keine Verhaltensänderungen, TypeScript fehlerfrei

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-20 12:30:16 +01:00
parent 38f8cdddc7
commit bc4a98de0d
15 changed files with 3940 additions and 2905 deletions
+393
View File
@@ -0,0 +1,393 @@
"use client";
import { type TenantLDAPConfig, type LDAPTestResult } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface TenantLDAPTabProps {
tenantLdapConfig: TenantLDAPConfig | null;
tenantLdapLoading: boolean;
tenantLdapSaving: boolean;
tenantLdapTesting: boolean;
tenantLdapError: string;
tenantLdapTestResult: LDAPTestResult | null;
tenantLdapForm: TenantLDAPConfig;
setTenantLdapForm: React.Dispatch<React.SetStateAction<TenantLDAPConfig>>;
tenantLdapChangePassword: boolean;
setTenantLdapChangePassword: (v: boolean) => void;
ownLogoPreviewUrl: string | null;
ownLogoUploading: boolean;
ownLogoError: string;
onSave: (e: React.FormEvent) => void;
onTest: () => void;
onDelete: () => void;
onOwnLogoUpload: (file: File) => void;
onOwnLogoDelete: () => void;
}
export function TenantLDAPTab({
tenantLdapConfig,
tenantLdapLoading,
tenantLdapSaving,
tenantLdapTesting,
tenantLdapError,
tenantLdapTestResult,
tenantLdapForm,
setTenantLdapForm,
tenantLdapChangePassword,
setTenantLdapChangePassword,
ownLogoPreviewUrl,
ownLogoUploading,
ownLogoError,
onSave,
onTest,
onDelete,
onOwnLogoUpload,
onOwnLogoDelete,
}: TenantLDAPTabProps) {
return (
<div 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={onSave} 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://server:389 oder ldaps://server:636"
value={tenantLdapForm.url}
onChange={(e) => setTenantLdapForm((f) => ({ ...f, url: e.target.value }))}
/>
<p className="text-xs text-muted-foreground">
Port frei wählbar · Standard: 389 (LDAP), 636 (LDAPS) · Univention UCS: 7389 / 7636
</p>
</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-3">
<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.error_detail && (
<p className="text-xs text-destructive font-mono">{tenantLdapTestResult.error_detail}</p>
)}
{tenantLdapTestResult.ok && tenantLdapTestResult.users_found > 0 && (
<div className="space-y-1">
<p className="text-sm font-medium">
{tenantLdapTestResult.users_found} Benutzer gefunden
{tenantLdapTestResult.users?.length < tenantLdapTestResult.users_found && (
<span className="text-muted-foreground font-normal"> (Vorschau: {tenantLdapTestResult.users?.length})</span>
)}
</p>
<div className="rounded border overflow-auto max-h-64">
<table className="w-full text-xs">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left px-2 py-1 font-medium">UID</th>
<th className="text-left px-2 py-1 font-medium">Name</th>
<th className="text-left px-2 py-1 font-medium">E-Mail</th>
</tr>
</thead>
<tbody>
{tenantLdapTestResult.users?.map((u, i) => (
<tr key={i} className="border-t">
<td className="px-2 py-1 font-mono">{u.uid || ""}</td>
<td className="px-2 py-1">{u.display_name || ""}</td>
<td className="px-2 py-1 text-muted-foreground">{u.mail || ""}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Action bar */}
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" onClick={onTest} 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={onDelete}
>
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>
)}
{/* Logo section for domain_admin */}
<div className="space-y-3 pt-2">
<div>
<h3 className="text-base font-semibold">Mandanten-Logo</h3>
<p className="text-sm text-muted-foreground mt-1">Logo deines Mandanten hochladen (PNG, JPEG, GIF, WebP oder SVG, max. 2 MB).</p>
</div>
{ownLogoError && <p className="text-sm text-destructive">{ownLogoError}</p>}
{ownLogoPreviewUrl ? (
<div className="flex items-center gap-4">
<div className="flex items-center justify-center rounded border p-3 bg-muted/30">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={ownLogoPreviewUrl} alt="Logo" className="max-h-20 max-w-40 object-contain" />
</div>
<div className="flex flex-col gap-2">
<Input
type="file"
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml"
disabled={ownLogoUploading}
onChange={(e) => { const f = e.target.files?.[0]; if (f) onOwnLogoUpload(f); }}
className="w-auto"
/>
<Button variant="destructive" size="sm" disabled={ownLogoUploading} onClick={onOwnLogoDelete}>
{ownLogoUploading ? "Bitte warten..." : "Logo entfernen"}
</Button>
</div>
</div>
) : (
<div className="flex items-center gap-4">
<div className="flex items-center justify-center rounded border p-6 bg-muted/30 text-sm text-muted-foreground w-40">
Kein Logo
</div>
<Input
type="file"
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml"
disabled={ownLogoUploading}
onChange={(e) => { const f = e.target.files?.[0]; if (f) onOwnLogoUpload(f); }}
className="w-auto"
/>
</div>
)}
</div>
</div>
);
}