Files
archivmail/src/app/admin/page.tsx
T
sysops bc4a98de0d chore: admin/page.tsx in Einzelkomponenten aufteilen (3917 → 1304 Zeilen)
- Tab-Sektionen → src/components/admin/tabs/ (11 Dateien)
- Dialoge → src/components/admin/ (TenantLDAPDialog, UserDialogs)
- Keine Verhaltensänderungen, TypeScript fehlerfrei

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 12:30:16 +01:00

1305 lines
45 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useAuth } from "@/hooks/useAuth";
import {
getUsers,
createUser,
updateUser,
deleteUser,
getAuditLog,
getSMTPStatus,
getHealth,
getStorageStats,
getServices,
serviceAction,
getSystemStats,
uploadMailFiles,
getUploadProgress,
getSecurityAudit,
fixSecurityIssue,
getLDAPConfig,
saveLDAPConfig,
deleteLDAPConfig,
testLDAPConfig,
getTenants,
getTenantUsers,
createTenant,
updateTenant,
deleteTenant,
getTenantDomains,
addTenantDomain,
removeTenantDomain,
getTenantLDAPConfig,
saveTenantLDAPConfig,
deleteTenantLDAPConfig,
testTenantLDAPConfig,
syncAdminTenantLDAP,
type LDAPSyncResult,
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 LDAPConfig,
type LDAPTestResult,
type TenantLDAPConfig,
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
const [ldapConfig, setLdapConfig] = useState<LDAPConfig | null>(null);
const [ldapLoading, setLdapLoading] = useState(false);
const [ldapSaving, setLdapSaving] = useState(false);
const [ldapTesting, setLdapTesting] = useState(false);
const [ldapError, setLdapError] = useState("");
const [ldapTestResult, setLdapTestResult] = useState<LDAPTestResult | null>(null);
const [ldapForm, setLdapForm] = useState<LDAPConfig>({
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<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)
const [tenantLdapConfig, setTenantLdapConfig] = useState<TenantLDAPConfig | null>(null);
const [tenantLdapLoading, setTenantLdapLoading] = useState(false);
const [tenantLdapSaving, setTenantLdapSaving] = useState(false);
const [tenantLdapTesting, setTenantLdapTesting] = useState(false);
const [tenantLdapError, setTenantLdapError] = useState("");
const [tenantLdapTestResult, setTenantLdapTestResult] = useState<LDAPTestResult | null>(null);
const [tenantLdapForm, setTenantLdapForm] = useState<TenantLDAPConfig>({
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<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("");
// Logo for domain_admin own tenant
const [ownLogoPreviewUrl, setOwnLogoPreviewUrl] = useState<string | null>(null);
const [ownLogoUploading, setOwnLogoUploading] = useState(false);
const [ownLogoError, setOwnLogoError] = useState("");
// Tenant users dialog
const [tenantUsersDialogId, setTenantUsersDialogId] = useState<number | null>(null);
const [tenantUsersDialogName, setTenantUsersDialogName] = useState("");
const [tenantUsersDialogLdap, setTenantUsersDialogLdap] = useState(false);
const [tenantUsers, setTenantUsers] = useState<User[]>([]);
const [tenantUsersLoading, setTenantUsersLoading] = useState(false);
const [tenantUsersError, setTenantUsersError] = useState("");
const [tenantUsersSyncing, setTenantUsersSyncing] = useState(false);
const [tenantUsersSyncResult, setTenantUsersSyncResult] = useState<LDAPSyncResult | null>(null);
// 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);
}
}
// 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<LDAPConfig> = { ...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<typeof testLDAPConfig>[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 {
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 openUsersDialog(t: Tenant) {
setTenantUsersDialogId(t.id);
setTenantUsersDialogName(t.name);
setTenantUsersDialogLdap(t.ldap_enabled === true);
setTenantUsersLoading(true);
setTenantUsers([]);
setTenantUsersError("");
setTenantUsersSyncResult(null);
try {
const users = await getTenantUsers(t.id);
setTenantUsers(users || []);
} catch (err: unknown) {
setTenantUsersError(err instanceof Error ? err.message : "Nutzer konnten nicht geladen werden.");
} finally {
setTenantUsersLoading(false);
}
}
async function handleSyncLDAPUsers() {
if (!tenantUsersDialogId) return;
setTenantUsersSyncing(true);
setTenantUsersSyncResult(null);
try {
const result = await syncAdminTenantLDAP(tenantUsersDialogId);
setTenantUsersSyncResult(result);
const users = await getTenantUsers(tenantUsersDialogId);
setTenantUsers(users || []);
} catch (err: unknown) {
setTenantUsersSyncResult({ synced: 0, errors: [err instanceof Error ? err.message : "Sync fehlgeschlagen"] });
} finally {
setTenantUsersSyncing(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);
}
}
// 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);
}
// Load own tenant logo preview
try {
const res = await fetch("/api/tenant/logo", { credentials: "include" });
if (res.ok) {
const blob = await res.blob();
setOwnLogoPreviewUrl(URL.createObjectURL(blob));
}
} catch {
// no logo or not available
}
}, []);
async function handleSaveTenantLDAP(e: React.FormEvent) {
e.preventDefault();
setTenantLdapSaving(true);
setTenantLdapError("");
try {
const payload: Partial<TenantLDAPConfig> = { ...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<typeof testTenantLDAPConfig>[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);
}
}
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}
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>
);
}