bc4a98de0d
- 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>
1305 lines
45 KiB
TypeScript
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>
|
|
);
|
|
}
|