From 2dbddff0e2b892fee80f57ca102a19a16df78396 Mon Sep 17 00:00:00 2001 From: sysops Date: Tue, 17 Mar 2026 21:41:57 +0100 Subject: [PATCH] feat: rollenbasierte SMTP-Statistik + Service-Aktionen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleServiceAction: nur superadmin darf Dienste stoppen/starten - handleSMTPStatus: domain_admin bekommt tenant-gefilterte Stats (Domains, Mailanzahl, Speicher) statt globaler Daemon-Info - Admin-Dashboard: SMTP-Kacheln, Systemauslastung, IP-Allowlist nur für superadmin; domain_admin sieht eigene Domain-Statistik - Dienste-Tab: Aktions-Buttons nur für superadmin sichtbar Co-Authored-By: Claude Sonnet 4.6 --- internal/api/server.go | 36 +++++++++++ src/app/admin/page.tsx | 144 ++++++++++++++++++++++++----------------- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/internal/api/server.go b/internal/api/server.go index 1826c8f..3f79a1f 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -589,6 +589,35 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleSMTPStatus(w http.ResponseWriter, r *http.Request) { + sess := sessionFromCtx(r.Context()) + tenantID := tenantFromCtx(r.Context()) + + // domain_admin: return only their tenant's email statistics (no global daemon info) + if sess != nil && !auth.HasRole(sess.Role, userstore.RoleSuperAdmin) { + stats, err := s.store.StatsByTenant(r.Context(), tenantID) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to read stats") + return + } + domains := []string{} + if tenantID != nil && s.tenantStore != nil { + if dd, derr := s.tenantStore.ListDomains(r.Context(), *tenantID); derr == nil { + for _, d := range dd { + domains = append(domains, d.Domain) + } + } + } + writeJSON(w, http.StatusOK, map[string]interface{}{ + "enabled": true, + "tenant_only": true, + "domains": domains, + "total_mails": stats["count"], + "total_bytes": stats["total_size"], + }) + return + } + + // superadmin: global daemon status if s.smtpDaemon == nil { writeJSON(w, http.StatusOK, map[string]interface{}{"enabled": false, "running": false}) return @@ -1047,6 +1076,13 @@ func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) { + // Only superadmin may start/stop/restart services + sess := sessionFromCtx(r.Context()) + if sess == nil || !auth.HasRole(sess.Role, userstore.RoleSuperAdmin) { + writeError(w, http.StatusForbidden, "superadmin required") + return + } + name := r.PathValue("name") if !isAllowedService(name) { writeError(w, http.StatusBadRequest, "unknown service") diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 9640518..8b41d9f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -673,59 +673,85 @@ export default function AdminPage() { - {/* SMTP */} - - -
- SMTP-Daemon - - {smtpStatus?.running ? "Aktiv" : smtpStatus?.enabled === false ? "Deaktiviert" : "Gestoppt"} - -
- - {smtpStatus ? ( -
- Adresse - {smtpStatus.bind} - Domain - {smtpStatus.domain || "–"} - TLS - {smtpStatus.tls ? "Ja" : "Nein"} - Max. Größe - {smtpStatus.max_size_mb > 0 ? `${smtpStatus.max_size_mb} MB` : "50 MB"} + {/* SMTP — superadmin: globaler Daemon; domain_admin: nur eigene Domain-Statistik */} + {isSuperAdmin ? ( + <> + + +
+ SMTP-Daemon + + {smtpStatus?.running ? "Aktiv" : smtpStatus?.enabled === false ? "Deaktiviert" : "Gestoppt"} + +
+ + {smtpStatus ? ( +
+ Adresse + {smtpStatus.bind} + Domain + {smtpStatus.domain || "–"} + TLS + {smtpStatus.tls ? "Ja" : "Nein"} + Max. Größe + {smtpStatus.max_size_mb > 0 ? `${smtpStatus.max_size_mb} MB` : "50 MB"} +
+ ) : ( +

Nicht erreichbar

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

Keine Daten

+ )} +
+
+ + ) : ( + + +
+ SMTP – meine Domain(s) + Tenant
- ) : ( -

Nicht erreichbar

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

Keine Daten

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

Keine Daten

+ )} + + + )} {/* Archiv-Speicher */} @@ -753,8 +779,8 @@ export default function AdminPage() {
- {/* System Stats: CPU, RAM, Disks, Archivzeitraum */} -
+ {/* System Stats: nur für superadmin */} + {isSuperAdmin &&

Systemauslastung

{!systemStats ? ( @@ -885,10 +911,10 @@ export default function AdminPage() { )} )} -
+
} - {/* IP-Allowlist */} - {smtpStatus && smtpStatus.allowed_ips?.length > 0 && ( + {/* IP-Allowlist — nur superadmin */} + {isSuperAdmin && smtpStatus && smtpStatus.allowed_ips?.length > 0 && ( SMTP IP-Allowlist @@ -978,7 +1004,7 @@ export default function AdminPage() { Autostart Externer Zugriff Beschreibung - Aktionen + {isSuperAdmin && Aktionen} @@ -1029,6 +1055,7 @@ export default function AdminPage() { {svc.description || "–"} + {isSuperAdmin && (
{isActive ? ( @@ -1102,6 +1129,7 @@ export default function AdminPage() { )}
+ )} ); })}