"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useAuth } from "@/hooks/useAuth"; import { features, type Feature } from "@/data/features"; import { getUsers, createUser, updateUser, deleteUser, getAuditLog, getSMTPStatus, getHealth, getStorageStats, getServices, serviceAction, getSystemStats, uploadMailFiles, getUploadProgress, getSecurityAudit, fixSecurityIssue, getLDAPConfig, saveLDAPConfig, deleteLDAPConfig, testLDAPConfig, getTenants, createTenant, updateTenant, deleteTenant, getTenantDomains, addTenantDomain, removeTenantDomain, type User, type AuditEntry, type SMTPStatus, type StorageStats, type ServiceStatus, type SystemStats, type UploadJob, type SecurityCheck, type SecurityAuditResult, type LDAPConfig, type LDAPTestResult, type Tenant, type TenantDomain, } from "@/lib/api"; import { Navbar } from "@/components/navbar"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; 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"; const AUDIT_PAGE_SIZE = 25; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } export default function AdminPage() { const { user, loading: authLoading } = useAuth("admin"); // Dashboard state const [smtpStatus, setSmtpStatus] = useState(null); const [storageStats, setStorageStats] = useState(null); const [systemStats, setSystemStats] = useState(null); const [apiOnline, setApiOnline] = useState(null); const [dashLoading, setDashLoading] = useState(true); const [dashRefreshed, setDashRefreshed] = useState(null); const [countdown, setCountdown] = useState(30); // Services state const [services, setServices] = useState([]); const [servicesLoading, setServicesLoading] = useState(false); const [serviceActionLoading, setServiceActionLoading] = useState(null); const [serviceError, setServiceError] = useState(""); // Users state const [users, setUsers] = useState([]); const [usersLoading, setUsersLoading] = useState(true); const [usersError, setUsersError] = useState(""); // Create user dialog const [dialogOpen, setDialogOpen] = useState(false); const [newUsername, setNewUsername] = useState(""); const [newEmail, setNewEmail] = useState(""); const [newPassword, setNewPassword] = useState(""); const [newRole, setNewRole] = useState("user"); const [createLoading, setCreateLoading] = useState(false); const [createError, setCreateError] = useState(""); // User action state const [userActionLoading, setUserActionLoading] = useState(null); const [resetPasswordUserId, setResetPasswordUserId] = useState(null); const [resetPasswordValue, setResetPasswordValue] = useState(""); const [resetPasswordError, setResetPasswordError] = useState(""); const [resetPasswordLoading, setResetPasswordLoading] = useState(false); // Delete confirmation dialog const [deleteDialogUser, setDeleteDialogUser] = useState(null); const [deleteActionLoading, setDeleteActionLoading] = useState<"deactivate" | "delete" | null>(null); const [deleteDialogError, setDeleteDialogError] = useState(""); // Audit state const [auditEntries, setAuditEntries] = useState([]); const [auditTotal, setAuditTotal] = useState(0); const [auditPage, setAuditPage] = useState(1); const [auditLoading, setAuditLoading] = useState(false); // Security audit state const [securityAudit, setSecurityAudit] = useState(null); const [securityLoading, setSecurityLoading] = useState(false); const [securityError, setSecurityError] = useState(""); const [fixLoading, setFixLoading] = useState(null); const [fixMessage, setFixMessage] = useState(""); // Upload state const [uploadDragging, setUploadDragging] = useState(false); const [uploadJob, setUploadJob] = useState(null); const [uploadError, setUploadError] = useState(""); const [uploadLoading, setUploadLoading] = useState(false); const uploadPollRef = useRef | null>(null); // LDAP state const [ldapConfig, setLdapConfig] = useState(null); const [ldapLoading, setLdapLoading] = useState(false); const [ldapSaving, setLdapSaving] = useState(false); const [ldapTesting, setLdapTesting] = useState(false); const [ldapError, setLdapError] = useState(""); const [ldapTestResult, setLdapTestResult] = useState(null); const [ldapForm, setLdapForm] = 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 [ldapChangePassword, setLdapChangePassword] = useState(false); // Tenants state const [tenants, setTenants] = useState([]); const [tenantsLoading, setTenantsLoading] = useState(false); const [tenantsError, setTenantsError] = useState(""); const [tenantDialogOpen, setTenantDialogOpen] = useState(false); const [newTenantName, setNewTenantName] = useState(""); const [newTenantSlug, setNewTenantSlug] = useState(""); const [tenantCreateLoading, setTenantCreateLoading] = useState(false); const [tenantCreateError, setTenantCreateError] = useState(""); const [tenantDeleteId, setTenantDeleteId] = useState(null); const [tenantDeleteLoading, setTenantDeleteLoading] = useState(false); const [domainDialogTenant, setDomainDialogTenant] = useState(null); const [tenantDomains, setTenantDomains] = useState([]); const [domainsLoading, setDomainsLoading] = useState(false); const [newDomain, setNewDomain] = useState(""); const [addDomainLoading, setAddDomainLoading] = useState(false); const [domainError, setDomainError] = useState(""); const loadDashboard = useCallback(async () => { setDashLoading(true); try { const [smtp, health, storage, sysStats] = await Promise.allSettled([ getSMTPStatus(), getHealth(), getStorageStats(), getSystemStats(), ]); setSmtpStatus(smtp.status === "fulfilled" ? smtp.value : null); setApiOnline(health.status === "fulfilled" && health.value.status === "ok"); setStorageStats(storage.status === "fulfilled" ? storage.value : null); setSystemStats(sysStats.status === "fulfilled" ? sysStats.value : null); setDashRefreshed(new Date()); } finally { setDashLoading(false); } }, []); const loadUsers = useCallback(async () => { setUsersLoading(true); setUsersError(""); try { const data = await getUsers(); setUsers(data || []); } catch { setUsersError("Benutzer konnten nicht geladen werden."); } finally { setUsersLoading(false); } }, []); const loadAudit = useCallback(async (p: number) => { setAuditLoading(true); try { const data = await getAuditLog({ page: p, page_size: AUDIT_PAGE_SIZE }); setAuditEntries(data.entries || []); setAuditTotal(data.total); setAuditPage(p); } catch { setAuditEntries([]); } finally { setAuditLoading(false); } }, []); const loadServices = useCallback(async () => { setServicesLoading(true); setServiceError(""); try { const data = await getServices(); setServices(data || []); } catch { setServiceError("Dienste konnten nicht abgerufen werden."); } finally { setServicesLoading(false); } }, []); async function handleUploadFiles(files: File[]) { const valid = files.filter(f => f.name.toLowerCase().endsWith(".eml") || f.name.toLowerCase().endsWith(".mbox")); if (valid.length === 0) { setUploadError("Nur .eml und .mbox Dateien erlaubt."); return; } setUploadError(""); setUploadJob(null); setUploadLoading(true); try { const { job_id } = await uploadMailFiles(valid); // Start polling const poll = setInterval(async () => { try { const job = await getUploadProgress(job_id); setUploadJob(job); if (job.status !== "running") { clearInterval(poll); uploadPollRef.current = null; setUploadLoading(false); } } catch { clearInterval(poll); uploadPollRef.current = null; setUploadLoading(false); } }, 1500); uploadPollRef.current = poll; } catch (e: unknown) { setUploadError(e instanceof Error ? e.message : "Upload fehlgeschlagen."); setUploadLoading(false); } } async function handleServiceAction(name: string, action: "start" | "stop" | "restart" | "enable" | "disable" | "block_external" | "allow_external") { setServiceActionLoading(`${name}:${action}`); setServiceError(""); try { const updated = await serviceAction(name, action); setServices((prev) => prev.map((s) => (s.name === updated.name ? updated : s))); } catch (e: unknown) { setServiceError(e instanceof Error ? e.message : "Aktion fehlgeschlagen."); } finally { setServiceActionLoading(null); } } const dashIntervalRef = useRef | null>(null); useEffect(() => { if (!user) return; loadDashboard(); loadUsers(); loadAudit(1); loadServices(); // Auto-Refresh Dashboard alle 30 Sekunden setCountdown(30); dashIntervalRef.current = setInterval(() => { loadDashboard(); setCountdown(30); }, 30_000); // Countdown-Ticker const ticker = setInterval(() => { setCountdown((c) => (c > 0 ? c - 1 : 0)); }, 1_000); return () => { if (dashIntervalRef.current) clearInterval(dashIntervalRef.current); clearInterval(ticker); }; }, [user, loadDashboard, loadUsers, loadAudit, loadServices]); async function handleCreateUser(e: React.FormEvent) { e.preventDefault(); setCreateLoading(true); setCreateError(""); try { await createUser({ username: newUsername, email: newEmail, password: newPassword, role: newRole, }); setDialogOpen(false); setNewUsername(""); setNewEmail(""); setNewPassword(""); setNewRole("user"); loadUsers(); } catch { setCreateError("Benutzer konnte nicht erstellt werden."); } finally { setCreateLoading(false); } } async function handleToggleActive(u: User) { setUserActionLoading(u.id); try { await updateUser(u.id, { active: !u.active }); loadUsers(); } catch { // ignore } finally { setUserActionLoading(null); } } async function handleDeactivateConfirmed() { if (!deleteDialogUser) return; setDeleteActionLoading("deactivate"); setDeleteDialogError(""); try { await updateUser(deleteDialogUser.id, { active: false }); setDeleteDialogUser(null); loadUsers(); } catch { setDeleteDialogError("Deaktivierung fehlgeschlagen."); } finally { setDeleteActionLoading(null); } } async function handleDeleteConfirmed() { if (!deleteDialogUser) return; setDeleteActionLoading("delete"); setDeleteDialogError(""); try { await deleteUser(deleteDialogUser.id); setDeleteDialogUser(null); loadUsers(); } catch (err: unknown) { setDeleteDialogError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); } finally { setDeleteActionLoading(null); } } async function handleResetPassword(e: React.FormEvent) { e.preventDefault(); if (!resetPasswordUserId) return; setResetPasswordLoading(true); setResetPasswordError(""); try { await updateUser(resetPasswordUserId, { password: resetPasswordValue }); setResetPasswordUserId(null); setResetPasswordValue(""); } catch { setResetPasswordError("Passwort konnte nicht geändert werden."); } finally { setResetPasswordLoading(false); } } async function runSecurityAudit() { setSecurityLoading(true); setSecurityError(""); setFixMessage(""); try { const result = await getSecurityAudit(); setSecurityAudit(result); } catch { setSecurityError("Security-Audit konnte nicht ausgeführt werden."); } finally { setSecurityLoading(false); } } async function runFix(action: string) { setFixLoading(action); setFixMessage(""); setSecurityError(""); try { const res = await fixSecurityIssue(action); setFixMessage(res.message); // Re-run audit automatically to reflect new state await runSecurityAudit(); } catch (e: unknown) { setSecurityError(e instanceof Error ? e.message : "Fix fehlgeschlagen."); } finally { setFixLoading(null); } } // LDAP handlers const loadLDAP = useCallback(async () => { setLdapLoading(true); setLdapError(""); try { const cfg = await getLDAPConfig(); if (cfg) { setLdapConfig(cfg); setLdapForm({ ...cfg, bind_password: "" }); setLdapChangePassword(false); } } catch { setLdapError("LDAP-Konfiguration konnte nicht geladen werden."); } finally { setLdapLoading(false); } }, []); async function handleSaveLDAP(e: React.FormEvent) { e.preventDefault(); setLdapSaving(true); setLdapError(""); try { const payload: Partial = { ...ldapForm }; if (!ldapChangePassword) { delete payload.bind_password; } await saveLDAPConfig(payload); await loadLDAP(); } catch (err: unknown) { setLdapError(err instanceof Error ? err.message : "Speichern fehlgeschlagen."); } finally { setLdapSaving(false); } } async function handleTestLDAP() { setLdapTesting(true); setLdapError(""); setLdapTestResult(null); try { const payload = ldapConfig ? { use_saved: true } : { use_saved: false, ...ldapForm }; const result = await testLDAPConfig(payload as Parameters[0]); setLdapTestResult(result); } catch (err: unknown) { setLdapError(err instanceof Error ? err.message : "Test fehlgeschlagen."); } finally { setLdapTesting(false); } } async function handleDeleteLDAP() { setLdapSaving(true); setLdapError(""); try { await deleteLDAPConfig(); setLdapConfig(null); setLdapForm({ 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: [], }); setLdapTestResult(null); } catch (err: unknown) { setLdapError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); } finally { setLdapSaving(false); } } // Tenants handlers const loadTenants = useCallback(async () => { setTenantsLoading(true); setTenantsError(""); try { const data = await getTenants(); setTenants(data || []); } catch { setTenantsError("Mandanten konnten nicht geladen werden."); } finally { setTenantsLoading(false); } }, []); async function handleCreateTenant(e: React.FormEvent) { e.preventDefault(); setTenantCreateLoading(true); setTenantCreateError(""); try { await createTenant(newTenantName, newTenantSlug); setTenantDialogOpen(false); setNewTenantName(""); setNewTenantSlug(""); await loadTenants(); } catch (err: unknown) { setTenantCreateError(err instanceof Error ? err.message : "Erstellen fehlgeschlagen."); } finally { setTenantCreateLoading(false); } } async function handleToggleTenant(t: Tenant) { try { await updateTenant(t.id, { active: !t.active }); await loadTenants(); } catch { /* ignore */ } } async function handleDeleteTenant() { if (!tenantDeleteId) return; setTenantDeleteLoading(true); try { await deleteTenant(tenantDeleteId); setTenantDeleteId(null); await loadTenants(); } catch { /* ignore */ } finally { setTenantDeleteLoading(false); } } async function openDomainDialog(t: Tenant) { setDomainDialogTenant(t); setDomainsLoading(true); setDomainError(""); setNewDomain(""); try { const domains = await getTenantDomains(t.id); setTenantDomains(domains || []); } catch { setDomainError("Domains konnten nicht geladen werden."); } finally { setDomainsLoading(false); } } async function handleAddDomain() { if (!domainDialogTenant || !newDomain) return; setAddDomainLoading(true); setDomainError(""); try { await addTenantDomain(domainDialogTenant.id, newDomain); setNewDomain(""); const domains = await getTenantDomains(domainDialogTenant.id); setTenantDomains(domains || []); } catch (err: unknown) { setDomainError(err instanceof Error ? err.message : "Domain konnte nicht hinzugefügt werden."); } finally { setAddDomainLoading(false); } } async function handleRemoveDomain(domainId: number) { if (!domainDialogTenant) return; setDomainError(""); try { await removeTenantDomain(domainDialogTenant.id, domainId); const domains = await getTenantDomains(domainDialogTenant.id); setTenantDomains(domains || []); } catch (err: unknown) { setDomainError(err instanceof Error ? err.message : "Domain konnte nicht entfernt werden."); } } const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE); return (
{(authLoading || !user) ? (
) : (<>

Administration

Dashboard Dienste Benutzer Audit-Log Import LDAP Security Mandanten Module {/* ── Dashboard ── */}

Systemstatus

{dashRefreshed && ( {dashRefreshed.toLocaleTimeString("de-DE")} · nächste Aktualisierung in {countdown}s )}
{dashLoading ? (
{Array.from({ length: 3 }).map((_, i) => ( ))}
) : ( <> {/* Status-Kacheln */}
{/* API */}
REST API {apiOnline ? "Online" : "Offline"}
Adresse :8080 Protokoll HTTP
{/* SMTP */}
SMTP-Daemon {smtpStatus?.running ? "Aktiv" : smtpStatus?.enabled === false ? "Deaktiviert" : "Gestoppt"}
{smtpStatus ? (
Adresse {smtpStatus.bind} Domain {smtpStatus.domain || "–"} TLS {smtpStatus.tls ? "Ja" : "Nein"} Max. Größe {smtpStatus.max_size_mb > 0 ? `${smtpStatus.max_size_mb} MB` : "50 MB"}
) : (

Nicht erreichbar

)}
{/* SMTP Statistik (nur live via SMTP-Daemon) */}
SMTP Statistik seit letztem Start
{smtpStatus ? (
Empfangen {smtpStatus.received} Abgelehnt {smtpStatus.rejected} Letzte Mail {smtpStatus.last_mail_at ? new Date(smtpStatus.last_mail_at).toLocaleString("de-DE") : "–"}
) : (

Keine Daten

)}
{/* Archiv-Speicher */}
Archiv gesamt {storageStats && ( {storageStats.total_mails} Mails )}
{storageStats ? (
E-Mails {storageStats.total_mails.toLocaleString("de-DE")} Speicher {formatBytes(storageStats.total_bytes)}
) : (

Keine Daten

)}
{/* System Stats: CPU, RAM, Disks, Archivzeitraum */}

Systemauslastung

{!systemStats ? ( Systemdaten konnten nicht abgerufen werden. Prüfe ob der Backend-Dienst läuft und der Endpunkt /api/admin/system/stats erreichbar ist. ) : ( <>
{/* CPU */}
CPU Load Average {systemStats.cpu.num_cpu} CPU(s)
1 min {systemStats.cpu.load1.toFixed(2)} 5 min {systemStats.cpu.load5.toFixed(2)} 15 min {systemStats.cpu.load15.toFixed(2)}
{/* RAM */}
Arbeitsspeicher 90 ? "destructive" : systemStats.ram.used_pct > 70 ? "secondary" : "default"}> {systemStats.ram.used_pct.toFixed(1)}%
90 ? "bg-destructive" : systemStats.ram.used_pct > 70 ? "bg-yellow-500" : "bg-primary"}`} style={{ width: `${Math.min(systemStats.ram.used_pct, 100)}%` }} />
Belegt {formatBytes(systemStats.ram.used_bytes)} Gesamt {formatBytes(systemStats.ram.total_bytes)} Frei {formatBytes(systemStats.ram.free_bytes)}
{/* Archivzeitraum */}
Archivzeitraum
{systemStats.archive.first_mail || systemStats.archive.last_mail ? (
{systemStats.archive.first_mail && (
Älteste Mail {new Date(systemStats.archive.first_mail.date).toLocaleDateString("de-DE")} {systemStats.archive.first_mail.from || "–"} {systemStats.archive.first_mail.subject || "(kein Betreff)"}
)} {systemStats.archive.last_mail && (
Neueste Mail {new Date(systemStats.archive.last_mail.date).toLocaleDateString("de-DE")} {systemStats.archive.last_mail.from || "–"} {systemStats.archive.last_mail.subject || "(kein Betreff)"}
)}
) : (

Archiv leer

)}
{/* Festplatten */} {systemStats.disks.length > 0 && (

Festplatten

{systemStats.disks.map((disk) => (
{disk.mount} 90 ? "destructive" : disk.used_pct > 75 ? "secondary" : "outline"}> {disk.used_pct.toFixed(1)}%
90 ? "bg-destructive" : disk.used_pct > 75 ? "bg-yellow-500" : "bg-primary"}`} style={{ width: `${Math.min(disk.used_pct, 100)}%` }} />
Belegt {formatBytes(disk.used_bytes)} Gesamt {formatBytes(disk.total_bytes)} Frei {formatBytes(disk.free_bytes)} Dateisystem {disk.fstype}
))}
)} )}
{/* IP-Allowlist */} {smtpStatus && smtpStatus.allowed_ips?.length > 0 && ( SMTP IP-Allowlist
{smtpStatus.allowed_ips.map((ip) => ( {ip} ))}
)} {/* Benutzerübersicht */} Benutzer {usersLoading ? ( ) : (
{users.filter(u => u.active).length} aktiv {users.filter(u => u.role === "admin").length} Admin {users.filter(u => u.role === "auditor").length} Auditor {users.filter(u => u.role === "user").length} User
)}
{!smtpStatus && ( SMTP-Status konnte nicht abgerufen werden. Prüfe ob der Backend-Dienst läuft. )} )} {/* ── Dienste ── */}

Systemdienste

{serviceError && ( {serviceError} )} {servicesLoading && services.length === 0 ? ( {Array.from({ length: 5 }).map((_, i) => ( ))} ) : ( Dienst Status Autostart Externer Zugriff Beschreibung Aktionen {services.map((svc) => { const isActive = svc.active === "active"; const isFailed = svc.active === "failed"; const isEnabled = svc.enabled === "enabled" || svc.enabled === "static"; const busy = (key: string) => serviceActionLoading === `${svc.name}:${key}`; const anyBusy = serviceActionLoading?.startsWith(`${svc.name}:`) ?? false; return ( {svc.name} {svc.active === "active" ? `Aktiv (${svc.sub})` : svc.active === "failed" ? "Fehler" : svc.active === "inactive" ? "Gestoppt" : svc.active} {svc.enabled === "enabled" ? "Aktiviert" : svc.enabled === "disabled" ? "Deaktiviert" : svc.enabled === "static" ? "Statisch" : svc.enabled} {svc.external_blocked !== undefined ? ( {svc.external_blocked ? "Gesperrt" : "Offen"} ) : ( )} {svc.description || "–"}
{isActive ? ( <> ) : ( )} {svc.enabled === "enabled" ? ( ) : svc.enabled === "disabled" ? ( ) : null} {svc.external_blocked !== undefined && ( svc.external_blocked ? ( ) : ( ) )}
); })}
)}

Benutzerverwaltung

Neuen Benutzer anlegen Erstellen Sie einen neuen Benutzer-Account.
setNewUsername(e.target.value)} required aria-label="Neuer Benutzername" />
setNewEmail(e.target.value)} required aria-label="Neue E-Mail-Adresse" />
setNewPassword(e.target.value)} required aria-label="Neues Passwort" />
{createError && (

{createError}

)}
{usersLoading ? ( {Array.from({ length: 4 }).map((_, i) => ( ))} ) : usersError ? ( {usersError} ) : users.length === 0 ? ( Keine Benutzer vorhanden. ) : ( Benutzername E-Mail Rolle Status Aktionen {users.map((u) => ( {u.username} {u.email} {u.role} {u.active ? "Aktiv" : "Inaktiv"}
))}
)}

Audit-Log

{auditLoading ? ( {Array.from({ length: 5 }).map((_, i) => ( ))} ) : auditEntries.length === 0 ? ( Keine Audit-Eintraege vorhanden. ) : ( <> Zeitstempel Ereignis Benutzer Details {auditEntries.map((entry) => ( {new Date(entry.timestamp).toLocaleString("de-DE")} {entry.event_type} {entry.username} {entry.detail} ))}
{auditTotalPages > 1 && (
Seite {auditPage} von {auditTotalPages}
)} )}
{/* ── Import ── */}

EML / MBOX importieren

Lade .eml oder .mbox Dateien hoch um sie ins Archiv zu importieren. Duplikate werden automatisch übersprungen.

{/* Drop zone */}
{ e.preventDefault(); setUploadDragging(true); }} onDragLeave={() => setUploadDragging(false)} onDrop={(e) => { e.preventDefault(); setUploadDragging(false); const files = Array.from(e.dataTransfer.files); handleUploadFiles(files); }} className={`border-2 border-dashed rounded-lg p-10 text-center transition-colors cursor-pointer ${ uploadDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50" }`} onClick={() => document.getElementById("upload-file-input")?.click()} > { if (e.target.files) handleUploadFiles(Array.from(e.target.files)); e.target.value = ""; }} />

Dateien hierher ziehen oder klicken zum Auswählen

Akzeptiert: .eml, .mbox

{uploadError && ( {uploadError} )} {/* Progress */} {(uploadLoading || uploadJob) && uploadJob && (
{uploadJob.status === "running" ? "Import läuft..." : "Import abgeschlossen"} {uploadJob.status === "done" ? "Fertig" : "Läuft"}
{/* Progress bar */} {uploadJob.total > 0 && (

{uploadJob.imported + uploadJob.skipped + uploadJob.errors} / {uploadJob.total} verarbeitet

)} {uploadJob.status === "done" && (

{uploadJob.imported}

Importiert

{uploadJob.skipped}

Übersprungen

{uploadJob.errors}

Fehler

)} )} {uploadLoading && !uploadJob && (

Upload läuft...

)} {/* ── Security Audit ── */}

Security Audit

{securityAudit && (

Zuletzt geprüft: {new Date(securityAudit.run_at).toLocaleString("de-DE")}

)}
{securityError && ( {securityError} )} {fixMessage && ( {fixMessage} )} {!securityAudit && !securityLoading && !securityError && (
Klicke auf “Jetzt prüfen” um den Security-Audit zu starten.
)} {securityLoading && (
{Array.from({ length: 7 }).map((_, i) => ( ))}
)} {securityAudit && !securityLoading && (() => { // Map check names to fix actions const fixActions: Record = { "Fail2ban": { action: "install_fail2ban", label: "Installieren & aktivieren" }, "Firewall (nftables)": { action: "enable_firewall", label: "Firewall aktivieren" }, "SSH Passwort-Auth": { action: "fix_ssh_password_auth", label: "Deaktivieren" }, "SSH Root-Login": { action: "fix_ssh_root_login", label: "Auf prohibit-password setzen" }, }; return (
{securityAudit.checks.map((check: SecurityCheck, i: number) => { const fix = check.status !== "ok" ? fixActions[check.name] : undefined; return (
{check.name} {check.status === "ok" ? "OK" : check.status === "warning" ? "Warnung" : "Fehler"}

{check.message}

{fix && ( )} {check.name === "HTTPS (TLS)" && check.status !== "ok" && ( )}
); })} {/* Summary */}
{[ { label: "OK", color: "bg-green-50 text-green-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "ok").length }, { label: "Warnungen", color: "bg-yellow-50 text-yellow-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "warning").length }, { label: "Fehler", color: "bg-red-50 text-red-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "error").length }, ].map((s) => (

{s.count}

{s.label}

))}
); })()}
{/* ── LDAP ── */}

LDAP / Active Directory

LDAP aktiviert setLdapForm((f) => ({ ...f, enabled: e.target.checked }))} />
{ldapError && ( {ldapError} )} {ldapLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : (
setLdapForm((f) => ({ ...f, url: e.target.value }))} />
setLdapForm((f) => ({ ...f, bind_dn: e.target.value }))} />
{ldapConfig && !ldapChangePassword ? (
) : ( setLdapForm((f) => ({ ...f, bind_password: e.target.value }))} /> )}
setLdapForm((f) => ({ ...f, base_dn: e.target.value }))} />
setLdapForm((f) => ({ ...f, user_filter: e.target.value }))} />
{/* Group mappings */}
{ldapForm.group_mappings.length === 0 ? (

Keine Gruppen-Zuordnungen definiert.

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

{ldapTestResult.server_info}

)} {ldapTestResult.users_found > 0 && (

{ldapTestResult.users_found} Benutzer gefunden

)} {ldapTestResult.error_detail && (

{ldapTestResult.error_detail}

)}
)} {/* Action bar */}
{ldapConfig && ( )}
)} {ldapConfig && (

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

)}
{/* ── Mandanten ── */}

Mandantenverwaltung

Neuen Mandanten anlegen Name und URL-Slug für den neuen Mandanten eingeben.
{ setNewTenantName(e.target.value); setNewTenantSlug(e.target.value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")); }} />
setNewTenantSlug(e.target.value)} />
{tenantCreateError && (

{tenantCreateError}

)}
{tenantsError && ( {tenantsError} )} {tenantsLoading ? ( {Array.from({ length: 4 }).map((_, i) => ( ))} ) : tenants.length === 0 ? ( Keine Mandanten vorhanden. Klicke auf “+ Mandant anlegen” um den ersten Mandanten zu erstellen. ) : ( Name Slug Domains Nutzer Status Aktionen {tenants.map((t) => ( {t.name} {t.slug} {t.domain_count ?? 0} {t.user_count ?? 0} {t.active ? "Aktiv" : "Inaktiv"}
))}
)} {/* Tenant delete confirmation */} { if (!open) setTenantDeleteId(null); }}> Mandant löschen Alle Daten dieses Mandanten (Domains, LDAP-Konfiguration) werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. {/* Domain management dialog */} { if (!open) setDomainDialogTenant(null); }}> Domains: {domainDialogTenant?.name} E-Mail-Domains diesem Mandanten zuweisen. {domainError && ( {domainError} )} {domainsLoading ? ( ) : (
{tenantDomains.length === 0 ? (

Keine Domains zugewiesen.

) : ( tenantDomains.map((d) => (
{d.domain}
)) )}
)}
setNewDomain(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleAddDomain(); } }} />
)}
{/* Passwort-Reset Dialog */} { if (!open) setResetPasswordUserId(null); }}> Passwort zurücksetzen Neues Passwort für den Benutzer festlegen.
setResetPasswordValue(e.target.value)} required minLength={8} placeholder="Mindestens 8 Zeichen" />
{resetPasswordError && (

{resetPasswordError}

)}
{/* Nutzer-Löschung Dialog */} { if (!open) setDeleteDialogUser(null); }}> Benutzer entfernen Was soll mit dem Konto {deleteDialogUser?.username} passieren?
Hinweis (GoBD): E-Mails bleiben unabhängig von dieser Aktion im Archiv erhalten. Die gesetzliche Aufbewahrungspflicht besteht auch nach Ausscheiden des Mitarbeiters.

Konto deaktivieren (empfohlen)

Login wird gesperrt. Konto und IMAP-Verbindungen bleiben erhalten und können reaktiviert werden.

Konto endgültig löschen

Account und alle IMAP-Verbindungen werden dauerhaft entfernt. Nicht rückgängig zu machen.

{deleteDialogError && (

{deleteDialogError}

)}
); } // ── Module Tab ───────────────────────────────────────────────────────────── const statusColors: Record = { "Planned": "bg-gray-100 text-gray-700", "In Progress": "bg-yellow-100 text-yellow-800", "In Review": "bg-blue-100 text-blue-800", "Deployed": "bg-green-100 text-green-800", }; const statusCounts = (list: Feature[]) => ({ total: list.length, planned: list.filter((f) => f.status === "Planned").length, inProgress: list.filter((f) => f.status === "In Progress").length, inReview: list.filter((f) => f.status === "In Review").length, deployed: list.filter((f) => f.status === "Deployed").length, }); function ModulesTab() { const counts = statusCounts(features); return (

Modulübersicht

{/* Summary bar */}
{[ { label: "In Progress", value: counts.inProgress, color: "bg-yellow-100 text-yellow-800" }, { label: "In Review", value: counts.inReview, color: "bg-blue-100 text-blue-800" }, { label: "Deployed", value: counts.deployed, color: "bg-green-100 text-green-800" }, { label: "Geplant", value: counts.planned, color: "bg-gray-100 text-gray-700" }, ].map((s) => ( {s.label} {s.value} ))}
{/* Table */} ID Feature Status Frontend Backend Aktualisiert {features.map((f) => ( {f.id} {f.name} {f.status} {f.frontend ? : } {f.backend ? : } {f.lastUpdated} ))}
); }