"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useLDAPConfig } from "@/hooks/useLDAPConfig"; import { useTenantLDAPConfig } from "@/hooks/useTenantLDAPConfig"; import { useTenantUsers } from "@/hooks/useTenantUsers"; import { getUsers, createUser, updateUser, deleteUser, getAuditLog, getSMTPStatus, getHealth, getStorageStats, getServices, serviceAction, getSystemStats, uploadMailFiles, getUploadProgress, getSecurityAudit, fixSecurityIssue, getTenants, createTenant, updateTenant, deleteTenant, getTenantDomains, addTenantDomain, removeTenantDomain, getAdminLabels, createAdminLabel, deleteAdminLabel, getLabelRules, createLabelRule, deleteLabelRule, getTenantLogoUrl, uploadTenantLogo, deleteTenantLogo, uploadMyTenantLogo, deleteMyTenantLogo, type User, type AuditEntry, type SMTPStatus, type StorageStats, type ServiceStatus, type SystemStats, type UploadJob, type SecurityAuditResult, type Tenant, type TenantDefaultUser, type TenantDomain, type MailLabel, type LabelRule, getCertInfo, uploadCert, generateSelfSignedCert, requestACMECert, type CertInfo, } from "@/lib/api"; import { Navbar } from "@/components/navbar"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Tab components import { DashboardTab } from "@/components/admin/tabs/DashboardTab"; import { ServicesTab } from "@/components/admin/tabs/ServicesTab"; import { UsersTab } from "@/components/admin/tabs/UsersTab"; import { AuditTab } from "@/components/admin/tabs/AuditTab"; import { ImportTab } from "@/components/admin/tabs/ImportTab"; import { SecurityTab } from "@/components/admin/tabs/SecurityTab"; import { LDAPTab } from "@/components/admin/tabs/LDAPTab"; import { TenantLDAPTab } from "@/components/admin/tabs/TenantLDAPTab"; import { TenantsTab } from "@/components/admin/tabs/TenantsTab"; import { LabelsTab } from "@/components/admin/tabs/LabelsTab"; import { CertTab } from "@/components/admin/tabs/CertTab"; import { ModulesTab } from "@/components/admin/ModulesTab"; import { ResetPasswordDialog, DeleteUserDialog } from "@/components/admin/UserDialogs"; const AUDIT_PAGE_SIZE = 25; 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 (global, superadmin) — managed by useLDAPConfig hook const { ldapConfig, ldapLoading, ldapSaving, ldapTesting, ldapError, ldapTestResult, ldapForm, setLdapForm, ldapChangePassword, setLdapChangePassword, loadLDAP, handleSaveLDAP, handleTestLDAP, handleDeleteLDAP, } = useLDAPConfig(); // 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 [tenantCreatedUsers, setTenantCreatedUsers] = useState([]); const [tenantCreatedName, setTenantCreatedName] = useState(""); const [tenantCredDialogOpen, setTenantCredDialogOpen] = useState(false); 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) — managed by useTenantLDAPConfig hook const { tenantLdapConfig, tenantLdapLoading, tenantLdapSaving, tenantLdapTesting, tenantLdapError, tenantLdapTestResult, tenantLdapForm, setTenantLdapForm, tenantLdapChangePassword, setTenantLdapChangePassword, ownLogoPreviewUrl, setOwnLogoPreviewUrl, ownLogoUploading, setOwnLogoUploading, ownLogoError, setOwnLogoError, loadTenantLDAP, handleSaveTenantLDAP, handleTestTenantLDAP, handleDeleteTenantLDAP, } = useTenantLDAPConfig(); // Superadmin: tenant LDAP dialog const [tenantLdapDialogId, setTenantLdapDialogId] = useState(null); // Logo dialog (superadmin: any tenant) const [logoDialogTenant, setLogoDialogTenant] = useState(null); const [logoPreviewUrl, setLogoPreviewUrl] = useState(null); const [logoUploading, setLogoUploading] = useState(false); const [logoError, setLogoError] = useState(""); // Tenant users dialog — managed by useTenantUsers hook const { tenantUsersDialogId, setTenantUsersDialogId, tenantUsersDialogName, tenantUsersDialogLdap, tenantUsers, tenantUsersLoading, tenantUsersError, tenantUsersSyncing, tenantUsersSyncResult, openUsersDialog, handleSyncLDAPUsers, } = useTenantUsers(); // 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); // Certificate state const [certInfo, setCertInfo] = useState(null); const [certLoading, setCertLoading] = useState(false); const [certError, setCertError] = useState(""); const [certSuccess, setCertSuccess] = useState(""); const [certFile, setCertFile] = useState(null); const [keyFile, setKeyFile] = useState(null); const [certUploadLoading, setCertUploadLoading] = useState(false); const [selfSignedCN, setSelfSignedCN] = useState("archivmail"); const [selfSignedDNS, setSelfSignedDNS] = useState("archivmail"); const [selfSignedIPs, setSelfSignedIPs] = useState("192.168.1.131"); const [selfSignedYears, setSelfSignedYears] = useState("10"); const [selfSignedLoading, setSelfSignedLoading] = useState(false); const [acmeDomain, setAcmeDomain] = useState(""); const [acmeEmail, setAcmeEmail] = useState(""); const [acmeLoading, setAcmeLoading] = useState(false); const [acmeOutput, setAcmeOutput] = 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); } }, []); const loadCert = useCallback(async () => { setCertLoading(true); setCertError(""); try { const info = await getCertInfo(); setCertInfo(info); } catch (e) { setCertError(String(e)); } finally { setCertLoading(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); 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(); setCountdown(30); dashIntervalRef.current = setInterval(() => { loadDashboard(); setCountdown(30); }, 30_000); 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); await runSecurityAudit(); } catch (e: unknown) { setSecurityError(e instanceof Error ? e.message : "Fix fehlgeschlagen."); } finally { setFixLoading(null); } } // 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 { const result = await createTenant(newTenantName, newTenantSlug); setTenantDialogOpen(false); setTenantCreatedName(result.name); setTenantCreatedUsers(result.default_users ?? []); setTenantCredDialogOpen(true); 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."); } } // Logo handlers (superadmin: any tenant) async function openLogoDialog(t: Tenant) { setLogoDialogTenant(t); setLogoError(""); setLogoPreviewUrl(null); if (t.has_logo) { try { const res = await fetch(getTenantLogoUrl(t.id), { credentials: "include" }); if (res.ok) { const blob = await res.blob(); setLogoPreviewUrl(URL.createObjectURL(blob)); } } catch { // preview not critical } } } async function handleLogoUpload(file: File) { if (!logoDialogTenant) return; setLogoUploading(true); setLogoError(""); try { await uploadTenantLogo(logoDialogTenant.id, file); const res = await fetch(getTenantLogoUrl(logoDialogTenant.id), { credentials: "include" }); if (res.ok) { const blob = await res.blob(); setLogoPreviewUrl(URL.createObjectURL(blob)); } setTenants((prev) => prev.map((t) => t.id === logoDialogTenant.id ? { ...t, has_logo: true } : t)); } catch (err: unknown) { setLogoError(err instanceof Error ? err.message : "Upload fehlgeschlagen."); } finally { setLogoUploading(false); } } async function handleLogoDelete() { if (!logoDialogTenant) return; setLogoUploading(true); setLogoError(""); try { await deleteTenantLogo(logoDialogTenant.id); setLogoPreviewUrl(null); setTenants((prev) => prev.map((t) => t.id === logoDialogTenant.id ? { ...t, has_logo: false } : t)); } catch (err: unknown) { setLogoError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); } finally { setLogoUploading(false); } } // Logo handlers (domain_admin: own tenant) async function handleOwnLogoUpload(file: File) { setOwnLogoUploading(true); setOwnLogoError(""); try { await uploadMyTenantLogo(file); const res = await fetch(`/api/tenant/logo`, { credentials: "include" }); if (res.ok) { const blob = await res.blob(); setOwnLogoPreviewUrl(URL.createObjectURL(blob)); } } catch (err: unknown) { setOwnLogoError(err instanceof Error ? err.message : "Upload fehlgeschlagen."); } finally { setOwnLogoUploading(false); } } async function handleOwnLogoDelete() { setOwnLogoUploading(true); setOwnLogoError(""); try { await deleteMyTenantLogo(); setOwnLogoPreviewUrl(null); } catch (err: unknown) { setOwnLogoError(err instanceof Error ? err.message : "Löschen fehlgeschlagen."); } finally { setOwnLogoUploading(false); } } 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 && Zertifikat} {isSuperAdmin && Mandanten} {isSuperAdmin && Module} { loadDashboard(); setCountdown(30); }} /> {isSuperAdmin && ( )} { setResetPasswordUserId(userId); setResetPasswordValue(""); setResetPasswordError(""); }} onOpenDeleteDialog={(u) => { setDeleteDialogUser(u); setDeleteDialogError(""); }} /> {isSuperAdmin && ( )} {isSuperAdmin && ( )} {!isSuperAdmin && user?.role === "domain_admin" && ( )} {isSuperAdmin && ( )} {isSuperAdmin && ( )} {isSuperAdmin && ( { setLogoDialogTenant(null); setLogoPreviewUrl(null); }} tenantLdapDialogId={tenantLdapDialogId} setTenantLdapDialogId={setTenantLdapDialogId} onLoadTenants={loadTenants} onToggleTenant={handleToggleTenant} /> )} )}
{/* Global dialogs (not tab-specific) */} setResetPasswordUserId(null)} value={resetPasswordValue} setValue={setResetPasswordValue} error={resetPasswordError} loading={resetPasswordLoading} onSubmit={handleResetPassword} /> setDeleteDialogUser(null)} deleteActionLoading={deleteActionLoading} deleteDialogError={deleteDialogError} onDeactivate={handleDeactivateConfirmed} onDelete={handleDeleteConfirmed} />
); }