refactor: server.go in separate Handler-Dateien aufgeteilt
server.go (2357 -> 391 Zeilen) enthaelt nur noch Server-Struct, Konstruktor, Router, Middleware und Hilfsfunktionen. Neue Dateien: - auth_handlers.go: Login, Logout, Me - search_handlers.go: Suche, Mail-Anzeige, Anhaenge, Raw-Download - admin_handlers.go: User-CRUD, SMTP/Storage-Stats, Services, Security - import_handlers.go: IMAP + POP3 Account-Verwaltung und Import - dashboard_handlers.go: System-Stats, Audit-Log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/archivmail/internal/audit"
|
||||
"github.com/archivmail/internal/storage"
|
||||
"github.com/archivmail/pkg/mailparser"
|
||||
)
|
||||
|
||||
// ── Audit Log handler ─────────────────────────────────────────────────────
|
||||
|
||||
func (s *Server) handleAuditLog(w http.ResponseWriter, r *http.Request) {
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
pageSizeStr := r.URL.Query().Get("page_size")
|
||||
username := r.URL.Query().Get("username")
|
||||
eventType := r.URL.Query().Get("event_type")
|
||||
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
pageSize, _ := strconv.Atoi(pageSizeStr)
|
||||
if pageSize <= 0 {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
entries, total, err := s.audlog.Query(audit.QueryFilter{
|
||||
Username: username,
|
||||
EventType: eventType,
|
||||
PageSize: pageSize,
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "audit query failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"total": total,
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// ── System stats handler ─────────────────────────────────────────────────
|
||||
|
||||
type diskStat struct {
|
||||
Mount string `json:"mount"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
UsedBytes uint64 `json:"used_bytes"`
|
||||
FreeBytes uint64 `json:"free_bytes"`
|
||||
UsedPct float64 `json:"used_pct"`
|
||||
FSType string `json:"fstype"`
|
||||
}
|
||||
|
||||
type mailInfo struct {
|
||||
ID string `json:"id"`
|
||||
Date string `json:"date"`
|
||||
From string `json:"from"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
var excludedFSTypes = map[string]bool{
|
||||
"tmpfs": true, "proc": true, "sysfs": true, "devtmpfs": true,
|
||||
"cgroup": true, "cgroup2": true, "overlay": true, "squashfs": true,
|
||||
"debugfs": true, "tracefs": true, "securityfs": true, "pstore": true,
|
||||
"efivarfs": true, "bpf": true, "hugetlbfs": true, "mqueue": true,
|
||||
"ramfs": true, "devpts": true, "fusectl": true, "configfs": true,
|
||||
"autofs": true, "nsfs": true, "rpc_pipefs": true,
|
||||
"fuse.lxcfs": true, "fuse": true,
|
||||
}
|
||||
|
||||
func (s *Server) handleSystemStats(w http.ResponseWriter, r *http.Request) {
|
||||
// CPU: /proc/loadavg
|
||||
cpuResp := map[string]interface{}{"load1": 0.0, "load5": 0.0, "load15": 0.0, "num_cpu": runtime.NumCPU()}
|
||||
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
||||
parts := strings.Fields(string(data))
|
||||
if len(parts) >= 3 {
|
||||
l1, _ := strconv.ParseFloat(parts[0], 64)
|
||||
l5, _ := strconv.ParseFloat(parts[1], 64)
|
||||
l15, _ := strconv.ParseFloat(parts[2], 64)
|
||||
cpuResp = map[string]interface{}{"load1": l1, "load5": l5, "load15": l15, "num_cpu": runtime.NumCPU()}
|
||||
}
|
||||
}
|
||||
|
||||
// RAM: /proc/meminfo
|
||||
ramResp := map[string]interface{}{"total_bytes": uint64(0), "used_bytes": uint64(0), "free_bytes": uint64(0), "used_pct": 0.0}
|
||||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||
kv := parseMeminfo(string(data))
|
||||
total := kv["MemTotal"] * 1024
|
||||
available := kv["MemAvailable"] * 1024
|
||||
used := total - available
|
||||
var usedPct float64
|
||||
if total > 0 {
|
||||
usedPct = math.Round(float64(used)/float64(total)*1000) / 10
|
||||
}
|
||||
ramResp = map[string]interface{}{
|
||||
"total_bytes": total,
|
||||
"used_bytes": used,
|
||||
"free_bytes": available,
|
||||
"used_pct": usedPct,
|
||||
}
|
||||
}
|
||||
|
||||
// Disks: /proc/mounts + syscall.Statfs
|
||||
var disks []diskStat
|
||||
seenMounts := map[string]bool{} // deduplicate by mountpoint
|
||||
seenDevices := map[string]bool{} // deduplicate by device (catches ZFS bind-mounts)
|
||||
if data, err := os.ReadFile("/proc/mounts"); err == nil {
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
device := fields[0]
|
||||
mount := fields[1]
|
||||
fstype := fields[2]
|
||||
if excludedFSTypes[fstype] || seenMounts[mount] || seenDevices[device] {
|
||||
continue
|
||||
}
|
||||
seenMounts[mount] = true
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(mount, &stat); err != nil {
|
||||
continue
|
||||
}
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bavail * uint64(stat.Bsize)
|
||||
used := total - free
|
||||
if total == 0 {
|
||||
continue // skip pseudo-mounts with no storage (e.g. lxcfs overlays)
|
||||
}
|
||||
seenDevices[device] = true
|
||||
var usedPct float64
|
||||
if total > 0 {
|
||||
usedPct = math.Round(float64(used)/float64(total)*1000) / 10
|
||||
}
|
||||
disks = append(disks, diskStat{
|
||||
Mount: mount,
|
||||
TotalBytes: total,
|
||||
UsedBytes: used,
|
||||
FreeBytes: free,
|
||||
UsedPct: usedPct,
|
||||
FSType: fstype,
|
||||
})
|
||||
}
|
||||
}
|
||||
if disks == nil {
|
||||
disks = []diskStat{}
|
||||
}
|
||||
|
||||
// Archive: first & last mail
|
||||
archiveResp := map[string]interface{}{"first_mail": nil, "last_mail": nil}
|
||||
first, last, err := s.store.FirstAndLastMail()
|
||||
if err == nil {
|
||||
if first != nil {
|
||||
archiveResp["first_mail"] = mailRefToInfo(s.store, first)
|
||||
}
|
||||
if last != nil {
|
||||
archiveResp["last_mail"] = mailRefToInfo(s.store, last)
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"cpu": cpuResp,
|
||||
"ram": ramResp,
|
||||
"disks": disks,
|
||||
"archive": archiveResp,
|
||||
})
|
||||
}
|
||||
|
||||
func parseMeminfo(content string) map[string]uint64 {
|
||||
result := make(map[string]uint64)
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
for scanner.Scan() {
|
||||
k, v, ok := strings.Cut(scanner.Text(), ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(strings.TrimSpace(v))
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseUint(fields[0], 10, 64)
|
||||
if err == nil {
|
||||
result[k] = val
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mailRefToInfo(store *storage.Store, ref *storage.MailRef) *mailInfo {
|
||||
dateStr := ref.ModTime.UTC().Format(time.RFC3339)
|
||||
raw, err := store.Load(ref.ID)
|
||||
if err != nil {
|
||||
return &mailInfo{ID: ref.ID, Date: dateStr}
|
||||
}
|
||||
pm, err := mailparser.Parse(raw)
|
||||
if err != nil {
|
||||
return &mailInfo{ID: ref.ID, Date: dateStr}
|
||||
}
|
||||
if !pm.Date.IsZero() {
|
||||
dateStr = pm.Date.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return &mailInfo{
|
||||
ID: ref.ID,
|
||||
Date: dateStr,
|
||||
From: pm.From,
|
||||
Subject: pm.Subject,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user