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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user