"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, getTenantLDAPConfig, saveTenantLDAPConfig, deleteTenantLDAPConfig, testTenantLDAPConfig, getAdminTenantLDAPConfig, saveAdminTenantLDAPConfig, deleteAdminTenantLDAPConfig, testAdminTenantLDAPConfig, getAdminLabels, createAdminLabel, deleteAdminLabel, getLabelRules, createLabelRule, deleteLabelRule, type User, type AuditEntry, type SMTPStatus, type StorageStats, type ServiceStatus, type SystemStats, type UploadJob, type SecurityCheck, type SecurityAuditResult, type LDAPConfig, type LDAPTestResult, type TenantLDAPConfig, type Tenant, type TenantDomain, type MailLabel, type LabelRule, } 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("domain_admin"); const isSuperAdmin = user?.role === "superadmin"; // 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(""); // Tenant LDAP state (domain_admin own tenant) const [tenantLdapConfig, setTenantLdapConfig] = useState(null); const [tenantLdapLoading, setTenantLdapLoading] = useState(false); const [tenantLdapSaving, setTenantLdapSaving] = useState(false); const [tenantLdapTesting, setTenantLdapTesting] = useState(false); const [tenantLdapError, setTenantLdapError] = useState(""); const [tenantLdapTestResult, setTenantLdapTestResult] = useState(null); const [tenantLdapForm, setTenantLdapForm] = 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 [tenantLdapChangePassword, setTenantLdapChangePassword] = useState(false); // Superadmin: tenant LDAP dialog const [tenantLdapDialogId, setTenantLdapDialogId] = useState(null); // Labels state const [adminLabels, setAdminLabels] = useState([]); const [adminLabelsLoading, setAdminLabelsLoading] = useState(false); const [adminLabelsError, setAdminLabelsError] = useState(""); const [newLabelName, setNewLabelName] = useState(""); const [newLabelColor, setNewLabelColor] = useState("#ef4444"); const [labelCreating, setLabelCreating] = useState(false); const [labelRules, setLabelRules] = useState([]); const [labelRulesLoading, setLabelRulesLoading] = useState(false); const [newRuleField, setNewRuleField] = useState("from_domain"); const [newRuleValue, setNewRuleValue] = useState(""); const [newRuleLabelId, setNewRuleLabelId] = useState(null); const [ruleCreating, setRuleCreating] = useState(false); 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); } }, []); const loadAdminLabels = useCallback(async () => { setAdminLabelsLoading(true); setAdminLabelsError(""); try { const data = await getAdminLabels(); setAdminLabels(data || []); } catch { setAdminLabelsError("Labels konnten nicht geladen werden."); } finally { setAdminLabelsLoading(false); } }, []); const loadLabelRules = useCallback(async () => { setLabelRulesLoading(true); try { const data = await getLabelRules(); setLabelRules(data || []); } catch { // ignore } finally { setLabelRulesLoading(false); } }, []); function loadLabelsTab() { loadAdminLabels(); loadLabelRules(); } async function handleCreateAdminLabel(e: React.FormEvent) { e.preventDefault(); if (!newLabelName.trim()) return; setLabelCreating(true); try { await createAdminLabel(newLabelName.trim(), newLabelColor); setNewLabelName(""); setNewLabelColor("#ef4444"); await loadAdminLabels(); } catch (err) { setAdminLabelsError(err instanceof Error ? err.message : "Fehler"); } finally { setLabelCreating(false); } } async function handleDeleteAdminLabel(id: number, name: string) { if (!window.confirm(`Globales Label "${name}" wirklich loeschen?`)) return; try { await deleteAdminLabel(id); await loadAdminLabels(); await loadLabelRules(); } catch (err) { setAdminLabelsError(err instanceof Error ? err.message : "Fehler"); } } async function handleCreateRule(e: React.FormEvent) { e.preventDefault(); if (!newRuleValue.trim() || !newRuleLabelId) return; setRuleCreating(true); try { await createLabelRule(newRuleField, newRuleValue.trim(), newRuleLabelId); setNewRuleValue(""); await loadLabelRules(); } catch (err) { setAdminLabelsError(err instanceof Error ? err.message : "Fehler"); } finally { setRuleCreating(false); } } async function handleDeleteRule(id: number) { if (!window.confirm("Regel wirklich loeschen?")) return; try { await deleteLabelRule(id); await loadLabelRules(); } catch { // ignore } } 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."); } } // 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 = { ...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[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); return (
{(authLoading || !user) ? (
) : (<>

Administration

Dashboard {isSuperAdmin && Dienste} Benutzer Audit-Log Import {isSuperAdmin && LDAP (Global)} {!isSuperAdmin && user?.role === "domain_admin" && ( LDAP )} {isSuperAdmin && Labels} {isSuperAdmin && Security} {isSuperAdmin && Mandanten} {isSuperAdmin && 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 — superadmin: globaler Daemon; domain_admin: nur eigene Domain-Statistik */} {isSuperAdmin ? ( <>
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) > 0 ? `${smtpStatus.max_size_mb} MB` : "50 MB"}
) : (

Nicht erreichbar

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

Keine Daten

)}
) : (
SMTP – meine Domain(s) Tenant
{smtpStatus ? (
Domain(s) {(smtpStatus.domains?.length ?? 0) > 0 ? smtpStatus.domains!.join(", ") : "–"} Archivierte Mails {smtpStatus.total_mails?.toLocaleString("de-DE") ?? "–"} Speicher {smtpStatus.total_bytes ? formatBytes(smtpStatus.total_bytes) : "–"}
) : (

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: nur für superadmin */} {isSuperAdmin &&

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 — nur superadmin */} {isSuperAdmin && smtpStatus && (smtpStatus.allowed_ips?.length ?? 0) > 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 {isSuperAdmin && 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 || "–"} {isSuperAdmin && (
{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}` : ""}

)}
{/* ── Tenant LDAP (domain_admin) ── */}

LDAP / Active Directory — Mandantenkonfiguration

Konfiguriere den LDAP-Server für deinen Mandanten.

LDAP aktiviert setTenantLdapForm((f) => ({ ...f, enabled: e.target.checked }))} />
{tenantLdapError && ( {tenantLdapError} )} {tenantLdapLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : (
setTenantLdapForm((f) => ({ ...f, url: e.target.value }))} />
setTenantLdapForm((f) => ({ ...f, bind_dn: e.target.value }))} />
{tenantLdapConfig && !tenantLdapChangePassword ? (
) : ( setTenantLdapForm((f) => ({ ...f, bind_password: e.target.value }))} /> )}
setTenantLdapForm((f) => ({ ...f, base_dn: e.target.value }))} />
setTenantLdapForm((f) => ({ ...f, user_filter: e.target.value }))} />
{/* Group mappings -- domain_admin: nur user + auditor */}
{tenantLdapForm.group_mappings.length === 0 ? (

Keine Gruppen-Zuordnungen definiert.

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

{tenantLdapTestResult.server_info}

)} {tenantLdapTestResult.users_found > 0 && (

{tenantLdapTestResult.users_found} Benutzer gefunden

)} {tenantLdapTestResult.error_detail && (

{tenantLdapTestResult.error_detail}

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

Zuletzt geändert: {tenantLdapConfig.updated_at ? new Date(tenantLdapConfig.updated_at).toLocaleString("de-DE") : "–"} {tenantLdapConfig.updated_by ? ` von ${tenantLdapConfig.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 LDAP Status Aktionen {tenants.map((t) => ( {t.name} {t.slug} {t.domain_count ?? 0} {t.user_count ?? 0} {t.ldap_enabled === true ? ( Aktiv ) : t.ldap_url ? ( Deaktiviert ) : ( )} {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(); } }} />
{/* Tenant LDAP dialog (superadmin) */} {tenantLdapDialogId !== null && ( { setTenantLdapDialogId(null); loadTenants(); }} /> )}
{/* ── Labels (Admin) ── */} {adminLabelsError && ( {adminLabelsError} )} {/* Globale Labels */}

Globale Labels

setNewLabelName(e.target.value)} placeholder="Label-Name" className="h-8 w-48" />
{["#ef4444","#f97316","#eab308","#22c55e","#06b6d4","#6366f1","#a855f7","#ec4899"].map((c) => (
{adminLabelsLoading ? (
) : adminLabels.length === 0 ? (

Keine globalen Labels vorhanden.

) : ( Name Farbe Aktionen {adminLabels.map((label) => ( {label.name} ))}
)}
{/* Auto-Regeln */}

Auto-Regeln

setNewRuleValue(e.target.value)} placeholder="z.B. example.com" className="h-8 w-48" />
{labelRulesLoading ? (
) : labelRules.length === 0 ? (

Keine Regeln vorhanden.

) : ( Bedingung Wert Label Aktionen {labelRules.map((rule) => { const condLabels: Record = { from_domain: "Absender-Domain", source: "Import-Quelle", subject_contains: "Betreff enthaelt", }; const matchLabel = adminLabels.find((l) => l.id === rule.label_id); return ( {condLabels[rule.condition_field] || rule.condition_field} {rule.condition_value} {matchLabel ? ( {matchLabel.name} ) : ( ID {rule.label_id} )} ); })}
)}
)}
{/* 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} ))}
); } // ── Tenant LDAP Dialog (superadmin) ───────────────────────────────────────── function TenantLDAPDialog({ tenantID, onClose }: { tenantID: number; onClose: () => void }) { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [error, setError] = useState(""); const [testResult, setTestResult] = useState(null); const [changePassword, setChangePassword] = useState(false); const [form, setForm] = 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 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 = { ...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[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 ( { if (!open) onClose(); }}> LDAP-Konfiguration (Mandant #{tenantID}) LDAP-Server für diesen Mandanten konfigurieren. {error && ( {error} )} {loading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : (
LDAP aktiviert setForm((f) => ({ ...f, enabled: e.target.checked }))} />
setForm((f) => ({ ...f, url: e.target.value }))} />
setForm((f) => ({ ...f, bind_dn: e.target.value }))} />
{config && !changePassword ? (
) : ( setForm((f) => ({ ...f, bind_password: e.target.value }))} /> )}
setForm((f) => ({ ...f, base_dn: e.target.value }))} />
setForm((f) => ({ ...f, user_filter: e.target.value }))} />
{/* Group mappings -- superadmin per tenant: bis domain_admin */}
{form.group_mappings.length === 0 ? (

Keine Gruppen-Zuordnungen definiert.

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

{testResult.server_info}

)} {testResult.users_found > 0 && (

{testResult.users_found} Benutzer gefunden

)} {testResult.error_detail && (

{testResult.error_detail}

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

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

)} )}
); }