Files
edgeguard-native/internal/handlers/audit.go
Debian 35b7308ce2 feat: umfangreiches UI+API-Polish (v1.1.36–1.1.42)
Backend:
- Audit-Log: Search-Endpoint mit ILIKE-Filter (actor/action/subject/date)
- NTP: /ntp/status via chronyc tracking (Stratum, Offset, Quelle)
- System: /service-restart mit Allowlist (haproxy/squid/unbound/chrony/scheduler)
- Domain-Response-Headers + Rate-Limit (Migration 0024)
- Join-Tokens (Migration 0025), Cluster-mTLS, Aggregator-Fan-Out
- apt-Service für Update-Banner (apt-get update + Versionsprüfung)
- Backup-Retry mit exponential backoff (retry_apt 3×)
- publish.sh fail-fast + cleanup-old.sh (max 10 Versionen)

Frontend:
- Audit-Log-Page (/audit) mit Filter + Pagination
- ErrorBoundary an React-Root + Vite build-target festgenagelt (iOS 15+)
- Storage-Schema-Stamp: auto-wipe bei Versions-Mismatch (blank-page-Fix)
- EmptyState-Komponente überall ausgerollt
- SSL: Aggregate-Karte (total/expiring/expired/errors)
- Backups: Aggregate-Karte (letzter Backup/Größe/Fehlschläge 24h) + Backup-Now
- NTP: Sync-Status-Karte (chronyc tracking live)
- Domains: Backend-UP/DOWN-Chip aus HAProxy-Stats
- Backends: HAProxy-Status-Spalte (UP/DEGRADED/DOWN)
- Settings: Service-Neustart-Karte (haproxy/squid/unbound/chrony/scheduler)
- Settings: Upgrade-Status-Card, Wartungsmodus, Auto-Update, Retention
- Dashboard: Recent-Alerts, Cluster-Health, License-Chip, Onboarding-Hint
- System-Regeln im Firewall als eigener Tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:18:41 +02:00

154 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
)
type AuditHandler struct {
Repo *audit.Repo
}
func NewAuditHandler(repo *audit.Repo) *AuditHandler { return &AuditHandler{Repo: repo} }
func (h *AuditHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/audit")
g.GET("/recent", h.Recent)
g.GET("/search", h.Search)
g.GET("/live", h.Live)
}
// Search filtert audit_log nach Actor/Action/Subject/Date-Range.
// Query-Params: actor, action, subject (alle ILIKE-Substring),
// since/until (RFC3339), limit (max 500), offset.
func (h *AuditHandler) Search(c *gin.Context) {
f := audit.SearchFilter{
Actor: c.Query("actor"),
Action: c.Query("action"),
Subject: c.Query("subject"),
}
if s := c.Query("since"); s != "" {
if t, err := time.Parse(time.RFC3339, s); err == nil {
f.Since = &t
}
}
if s := c.Query("until"); s != "" {
if t, err := time.Parse(time.RFC3339, s); err == nil {
f.Until = &t
}
}
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
f.Limit = n
}
}
if v := c.Query("offset"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
f.Offset = n
}
}
rows, err := h.Repo.Search(c.Request.Context(), f)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"entries": rows, "limit": f.Limit, "offset": f.Offset})
}
// Recent returns the most recent audit_log entries — used by the
// dashboard fallback path (z.B. wenn WebSocket nicht verbinden kann).
// ?limit=N (1100, default 10).
func (h *AuditHandler) Recent(c *gin.Context) {
limit := 10
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
limit = n
}
}
rows, err := h.Repo.ListRecent(c.Request.Context(), limit)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"entries": rows})
}
// auditUpgrader: same-origin durch HAProxy, kein CheckOrigin.
var auditUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 4 * 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Live upgraded auf WebSocket: sendet einen Snapshot der letzten 50
// audit_log-Rows, danach jeden neuen Eintrag direkt aus dem
// Repo.broadcast()-Channel.
func (h *AuditHandler) Live(c *gin.Context) {
conn, err := auditUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
// Snapshot
if rows, err := h.Repo.ListRecent(c.Request.Context(), 50); err == nil {
// In aufsteigender Reihenfolge schicken (newest last) damit der
// Client nach unten scrollt + neue Events natürlich anhängt.
for i := len(rows) - 1; i >= 0; i-- {
if err := conn.WriteJSON(rows[i]); err != nil {
return
}
}
}
// Live-Subscribe
ch, unsub := h.Repo.Subscribe()
defer unsub()
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
// Read-Loop für close-frame + ping-pong
go func() {
defer cancel()
for {
if _, _, err := conn.NextReader(); err != nil {
return
}
}
}()
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
_ = conn.SetWriteDeadline(time.Now().Add(60 * time.Second))
for {
select {
case <-ctx.Done():
return
case e, ok := <-ch:
if !ok {
return
}
_ = conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteJSON(e); err != nil {
return
}
case <-ping.C:
_ = conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteControl(websocket.PingMessage, nil,
time.Now().Add(5*time.Second)); err != nil {
return
}
}
}
}