Files
archivmail/src/app/admin/page.tsx
T
sysops b6856af2eb feat(PROJ-32): Message-ID-basierte Duplikatserkennung
- message_id Spalte + UNIQUE-Index in emails-Tabelle
- Save() prüft Message-ID vor SHA-256-Flow (kein Disk-I/O bei Duplikat)
- lookupByMessageID() als private Hilfsfunktion
- insertMeta() schreibt message_id, gibt error zurück (Race-safe)
- SaveMeta() schreibt message_id idempotent (Backfill)

feat(PROJ-34): Retention-Policy + Löschsperre (GoBD)

- retain_until TIMESTAMPTZ Spalte in emails-Tabelle
- ErrRetentionLock typed error
- Delete() prüft Retention-Frist vor Löschung
- Purge() löscht alle Mails mit abgelaufener Retention
- POST /api/admin/purge Endpunkt (superadmin only)
- config: storage.retention_days

fix: Superadmin-Benutzerübersicht zeigt Mandant-Spalte

- UsersTab: Mandant-Spalte wenn isSuperAdmin
- domain_auditor Rolle im Create-Dialog ergänzt
- storage Modulversion → 1.6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 01:29:25 +02:00

1107 lines
38 KiB
TypeScript

"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<SMTPStatus | null>(null);
const [storageStats, setStorageStats] = useState<StorageStats | null>(null);
const [systemStats, setSystemStats] = useState<SystemStats | null>(null);
const [apiOnline, setApiOnline] = useState<boolean | null>(null);
const [dashLoading, setDashLoading] = useState(true);
const [dashRefreshed, setDashRefreshed] = useState<Date | null>(null);
const [countdown, setCountdown] = useState(30);
// Services state
const [services, setServices] = useState<ServiceStatus[]>([]);
const [servicesLoading, setServicesLoading] = useState(false);
const [serviceActionLoading, setServiceActionLoading] = useState<string | null>(null);
const [serviceError, setServiceError] = useState("");
// Users state
const [users, setUsers] = useState<User[]>([]);
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<number | null>(null);
const [resetPasswordUserId, setResetPasswordUserId] = useState<number | null>(null);
const [resetPasswordValue, setResetPasswordValue] = useState("");
const [resetPasswordError, setResetPasswordError] = useState("");
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
// Delete confirmation dialog
const [deleteDialogUser, setDeleteDialogUser] = useState<User | null>(null);
const [deleteActionLoading, setDeleteActionLoading] = useState<"deactivate" | "delete" | null>(null);
const [deleteDialogError, setDeleteDialogError] = useState("");
// Audit state
const [auditEntries, setAuditEntries] = useState<AuditEntry[]>([]);
const [auditTotal, setAuditTotal] = useState(0);
const [auditPage, setAuditPage] = useState(1);
const [auditLoading, setAuditLoading] = useState(false);
// Security audit state
const [securityAudit, setSecurityAudit] = useState<SecurityAuditResult | null>(null);
const [securityLoading, setSecurityLoading] = useState(false);
const [securityError, setSecurityError] = useState("");
const [fixLoading, setFixLoading] = useState<string | null>(null);
const [fixMessage, setFixMessage] = useState("");
// Upload state
const [uploadDragging, setUploadDragging] = useState(false);
const [uploadJob, setUploadJob] = useState<UploadJob | null>(null);
const [uploadError, setUploadError] = useState("");
const [uploadLoading, setUploadLoading] = useState(false);
const uploadPollRef = useRef<ReturnType<typeof setInterval> | 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<Tenant[]>([]);
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<TenantDefaultUser[]>([]);
const [tenantCreatedName, setTenantCreatedName] = useState("");
const [tenantCredDialogOpen, setTenantCredDialogOpen] = useState(false);
const [tenantDeleteId, setTenantDeleteId] = useState<number | null>(null);
const [tenantDeleteLoading, setTenantDeleteLoading] = useState(false);
const [domainDialogTenant, setDomainDialogTenant] = useState<Tenant | null>(null);
const [tenantDomains, setTenantDomains] = useState<TenantDomain[]>([]);
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<number | null>(null);
// Logo dialog (superadmin: any tenant)
const [logoDialogTenant, setLogoDialogTenant] = useState<Tenant | null>(null);
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(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<MailLabel[]>([]);
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<LabelRule[]>([]);
const [labelRulesLoading, setLabelRulesLoading] = useState(false);
const [newRuleField, setNewRuleField] = useState("from_domain");
const [newRuleValue, setNewRuleValue] = useState("");
const [newRuleLabelId, setNewRuleLabelId] = useState<number | null>(null);
const [ruleCreating, setRuleCreating] = useState(false);
// Certificate state
const [certInfo, setCertInfo] = useState<CertInfo | null>(null);
const [certLoading, setCertLoading] = useState(false);
const [certError, setCertError] = useState("");
const [certSuccess, setCertSuccess] = useState("");
const [certFile, setCertFile] = useState<File | null>(null);
const [keyFile, setKeyFile] = useState<File | null>(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<ReturnType<typeof setInterval> | 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 (
<div className="min-h-screen">
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
<main className="mx-auto max-w-7xl px-4 py-6">
{(authLoading || !user) ? (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-10 w-full max-w-sm" />
<Skeleton className="h-64 w-full" />
</div>
) : (<>
<h1 className="mb-6 text-2xl font-bold">Administration</h1>
<Tabs defaultValue="dashboard">
<TabsList>
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
{isSuperAdmin && <TabsTrigger value="services">Dienste</TabsTrigger>}
<TabsTrigger value="users">Benutzer</TabsTrigger>
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
<TabsTrigger value="import">Import</TabsTrigger>
{isSuperAdmin && <TabsTrigger value="ldap" onClick={loadLDAP}>LDAP (Global)</TabsTrigger>}
{!isSuperAdmin && user?.role === "domain_admin" && (
<TabsTrigger value="tenant-ldap" onClick={loadTenantLDAP}>LDAP</TabsTrigger>
)}
{isSuperAdmin && <TabsTrigger value="labels" onClick={loadLabelsTab}>Labels</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="security">Security</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="cert" onClick={loadCert}>Zertifikat</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="tenants" onClick={loadTenants}>Mandanten</TabsTrigger>}
{isSuperAdmin && <TabsTrigger value="modules">Module</TabsTrigger>}
</TabsList>
<TabsContent value="dashboard" className="mt-4 space-y-6">
<DashboardTab
isSuperAdmin={isSuperAdmin}
smtpStatus={smtpStatus}
storageStats={storageStats}
systemStats={systemStats}
apiOnline={apiOnline}
dashLoading={dashLoading}
dashRefreshed={dashRefreshed}
countdown={countdown}
users={users}
usersLoading={usersLoading}
onRefresh={() => { loadDashboard(); setCountdown(30); }}
/>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="services" className="mt-4">
<ServicesTab
isSuperAdmin={isSuperAdmin}
services={services}
servicesLoading={servicesLoading}
serviceActionLoading={serviceActionLoading}
serviceError={serviceError}
onLoadServices={loadServices}
onServiceAction={handleServiceAction}
/>
</TabsContent>
)}
<TabsContent value="users">
<UsersTab
isSuperAdmin={isSuperAdmin}
users={users}
tenants={tenants}
usersLoading={usersLoading}
usersError={usersError}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
newUsername={newUsername}
setNewUsername={setNewUsername}
newEmail={newEmail}
setNewEmail={setNewEmail}
newPassword={newPassword}
setNewPassword={setNewPassword}
newRole={newRole}
setNewRole={setNewRole}
createLoading={createLoading}
createError={createError}
onCreateUser={handleCreateUser}
userActionLoading={userActionLoading}
onToggleActive={handleToggleActive}
onOpenResetPassword={(userId) => {
setResetPasswordUserId(userId);
setResetPasswordValue("");
setResetPasswordError("");
}}
onOpenDeleteDialog={(u) => { setDeleteDialogUser(u); setDeleteDialogError(""); }}
/>
</TabsContent>
<TabsContent value="audit">
<AuditTab
auditEntries={auditEntries}
auditTotal={auditTotal}
auditPage={auditPage}
auditLoading={auditLoading}
onLoadAudit={loadAudit}
/>
</TabsContent>
<TabsContent value="import">
<ImportTab
uploadDragging={uploadDragging}
setUploadDragging={setUploadDragging}
uploadJob={uploadJob}
uploadError={uploadError}
uploadLoading={uploadLoading}
onUploadFiles={handleUploadFiles}
/>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="security">
<SecurityTab
securityAudit={securityAudit}
securityLoading={securityLoading}
securityError={securityError}
fixLoading={fixLoading}
fixMessage={fixMessage}
onRunAudit={runSecurityAudit}
onRunFix={runFix}
/>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="ldap">
<LDAPTab
ldapConfig={ldapConfig}
ldapLoading={ldapLoading}
ldapSaving={ldapSaving}
ldapTesting={ldapTesting}
ldapError={ldapError}
ldapTestResult={ldapTestResult}
ldapForm={ldapForm}
setLdapForm={setLdapForm}
ldapChangePassword={ldapChangePassword}
setLdapChangePassword={setLdapChangePassword}
onSave={handleSaveLDAP}
onTest={handleTestLDAP}
onDelete={handleDeleteLDAP}
/>
</TabsContent>
)}
{!isSuperAdmin && user?.role === "domain_admin" && (
<TabsContent value="tenant-ldap">
<TenantLDAPTab
tenantLdapConfig={tenantLdapConfig}
tenantLdapLoading={tenantLdapLoading}
tenantLdapSaving={tenantLdapSaving}
tenantLdapTesting={tenantLdapTesting}
tenantLdapError={tenantLdapError}
tenantLdapTestResult={tenantLdapTestResult}
tenantLdapForm={tenantLdapForm}
setTenantLdapForm={setTenantLdapForm}
tenantLdapChangePassword={tenantLdapChangePassword}
setTenantLdapChangePassword={setTenantLdapChangePassword}
ownLogoPreviewUrl={ownLogoPreviewUrl}
ownLogoUploading={ownLogoUploading}
ownLogoError={ownLogoError}
onSave={handleSaveTenantLDAP}
onTest={handleTestTenantLDAP}
onDelete={handleDeleteTenantLDAP}
onOwnLogoUpload={handleOwnLogoUpload}
onOwnLogoDelete={handleOwnLogoDelete}
/>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="labels">
<LabelsTab
adminLabels={adminLabels}
adminLabelsLoading={adminLabelsLoading}
adminLabelsError={adminLabelsError}
newLabelName={newLabelName}
setNewLabelName={setNewLabelName}
newLabelColor={newLabelColor}
setNewLabelColor={setNewLabelColor}
labelCreating={labelCreating}
labelRules={labelRules}
labelRulesLoading={labelRulesLoading}
newRuleField={newRuleField}
setNewRuleField={setNewRuleField}
newRuleValue={newRuleValue}
setNewRuleValue={setNewRuleValue}
newRuleLabelId={newRuleLabelId}
setNewRuleLabelId={setNewRuleLabelId}
ruleCreating={ruleCreating}
onCreateLabel={handleCreateAdminLabel}
onDeleteLabel={handleDeleteAdminLabel}
onCreateRule={handleCreateRule}
onDeleteRule={handleDeleteRule}
/>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="cert">
<CertTab
certInfo={certInfo}
certLoading={certLoading}
certError={certError}
certSuccess={certSuccess}
certFile={certFile}
setCertFile={setCertFile}
keyFile={keyFile}
setKeyFile={setKeyFile}
certUploadLoading={certUploadLoading}
setCertUploadLoading={setCertUploadLoading}
selfSignedCN={selfSignedCN}
setSelfSignedCN={setSelfSignedCN}
selfSignedDNS={selfSignedDNS}
setSelfSignedDNS={setSelfSignedDNS}
selfSignedIPs={selfSignedIPs}
setSelfSignedIPs={setSelfSignedIPs}
selfSignedYears={selfSignedYears}
setSelfSignedYears={setSelfSignedYears}
selfSignedLoading={selfSignedLoading}
setSelfSignedLoading={setSelfSignedLoading}
acmeDomain={acmeDomain}
setAcmeDomain={setAcmeDomain}
acmeEmail={acmeEmail}
setAcmeEmail={setAcmeEmail}
acmeLoading={acmeLoading}
setAcmeLoading={setAcmeLoading}
acmeOutput={acmeOutput}
setAcmeOutput={setAcmeOutput}
setCertError={setCertError}
setCertSuccess={setCertSuccess}
setCertInfo={setCertInfo}
onLoadCert={loadCert}
/>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="tenants">
<TenantsTab
tenants={tenants}
tenantsLoading={tenantsLoading}
tenantsError={tenantsError}
tenantDialogOpen={tenantDialogOpen}
setTenantDialogOpen={setTenantDialogOpen}
newTenantName={newTenantName}
setNewTenantName={setNewTenantName}
newTenantSlug={newTenantSlug}
setNewTenantSlug={setNewTenantSlug}
tenantCreateLoading={tenantCreateLoading}
tenantCreateError={tenantCreateError}
onCreateTenant={handleCreateTenant}
tenantCredDialogOpen={tenantCredDialogOpen}
setTenantCredDialogOpen={setTenantCredDialogOpen}
tenantCreatedName={tenantCreatedName}
tenantCreatedUsers={tenantCreatedUsers}
tenantDeleteId={tenantDeleteId}
setTenantDeleteId={setTenantDeleteId}
tenantDeleteLoading={tenantDeleteLoading}
onDeleteTenant={handleDeleteTenant}
domainDialogTenant={domainDialogTenant}
setDomainDialogTenant={setDomainDialogTenant}
tenantDomains={tenantDomains}
domainsLoading={domainsLoading}
newDomain={newDomain}
setNewDomain={setNewDomain}
addDomainLoading={addDomainLoading}
domainError={domainError}
onOpenDomainDialog={openDomainDialog}
onAddDomain={handleAddDomain}
onRemoveDomain={handleRemoveDomain}
tenantUsersDialogId={tenantUsersDialogId}
setTenantUsersDialogId={setTenantUsersDialogId}
tenantUsersDialogName={tenantUsersDialogName}
tenantUsersDialogLdap={tenantUsersDialogLdap}
tenantUsers={tenantUsers}
tenantUsersLoading={tenantUsersLoading}
tenantUsersError={tenantUsersError}
tenantUsersSyncing={tenantUsersSyncing}
tenantUsersSyncResult={tenantUsersSyncResult}
onOpenUsersDialog={openUsersDialog}
onSyncLDAPUsers={handleSyncLDAPUsers}
logoDialogTenant={logoDialogTenant}
logoPreviewUrl={logoPreviewUrl}
logoUploading={logoUploading}
logoError={logoError}
onOpenLogoDialog={openLogoDialog}
onLogoUpload={handleLogoUpload}
onLogoDelete={handleLogoDelete}
onLogoDialogClose={() => { setLogoDialogTenant(null); setLogoPreviewUrl(null); }}
tenantLdapDialogId={tenantLdapDialogId}
setTenantLdapDialogId={setTenantLdapDialogId}
onLoadTenants={loadTenants}
onToggleTenant={handleToggleTenant}
/>
</TabsContent>
)}
<TabsContent value="modules" className="mt-4">
<ModulesTab />
</TabsContent>
</Tabs>
</>)}
</main>
{/* Global dialogs (not tab-specific) */}
<ResetPasswordDialog
open={resetPasswordUserId !== null}
onClose={() => setResetPasswordUserId(null)}
value={resetPasswordValue}
setValue={setResetPasswordValue}
error={resetPasswordError}
loading={resetPasswordLoading}
onSubmit={handleResetPassword}
/>
<DeleteUserDialog
user={deleteDialogUser}
onClose={() => setDeleteDialogUser(null)}
deleteActionLoading={deleteActionLoading}
deleteDialogError={deleteDialogError}
onDeactivate={handleDeactivateConfirmed}
onDelete={handleDeleteConfirmed}
/>
</div>
);
}