Files
Aether/backend/internal/admin/audit_handler.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

100 lines
2.8 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 admin
import (
"fmt"
"net/http"
"strconv"
"github.com/google/uuid"
"aether/internal/audit"
"aether/pkg/response"
)
// Die Audit-Endpunkte liegen im admin-Paket, nicht in audit: `auth` schreibt
// selbst Einträge und importiert deshalb `audit` – läge der Handler dort und
// bräuchte `auth` für die Admin-Prüfung, wäre der Import-Zyklus fertig.
// GET /api/v1/admin/audit?action=&actor_id=&limit=&offset=
func (h *Handler) ListAudit(w http.ResponseWriter, r *http.Request) {
if _, ok := requireAdmin(r); !ok {
response.Error(w, http.StatusForbidden, "admin required")
return
}
f := audit.Filter{Action: r.URL.Query().Get("action")}
if raw := r.URL.Query().Get("actor_id"); raw != "" {
id, err := uuid.Parse(raw)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid actor_id")
return
}
f.ActorID = &id
}
if raw := r.URL.Query().Get("limit"); raw != "" {
f.Limit, _ = strconv.Atoi(raw)
}
if raw := r.URL.Query().Get("offset"); raw != "" {
f.Offset, _ = strconv.Atoi(raw)
}
entries, total, err := h.auditor.List(r.Context(), f)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load audit log")
return
}
response.JSON(w, http.StatusOK, map[string]any{
"entries": entries,
"total": total,
})
}
// GET /api/v1/admin/audit/actions – füttert das Filter-Dropdown.
func (h *Handler) ListAuditActions(w http.ResponseWriter, r *http.Request) {
if _, ok := requireAdmin(r); !ok {
response.Error(w, http.StatusForbidden, "admin required")
return
}
actions, err := h.auditor.Actions(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load actions")
return
}
response.JSON(w, http.StatusOK, actions)
}
// POST /api/v1/admin/gc – Bereinigung von Hand anstoßen.
//
// Der GC läuft ohnehin im Hintergrund; dieser Endpunkt macht ihn beobachtbar.
// Ein Aufräumjob, dessen Wirkung man nicht nachsehen kann, ist schwer zu
// vertrauen – und genau darum geht es hier, weil er Dateien löscht.
func (h *Handler) RunGC(w http.ResponseWriter, r *http.Request) {
if _, ok := requireAdmin(r); !ok {
response.Error(w, http.StatusForbidden, "admin required")
return
}
if h.gc == nil {
response.Error(w, http.StatusServiceUnavailable, "cleanup not available")
return
}
res, err := h.gc.Run(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "cleanup failed")
return
}
h.logAdmin(r, audit.ActionGarbageCollect, "storage", "", "",
fmt.Sprintf("manual run: %d blobs scanned, %d removed, %d bytes freed",
res.Scanned, res.Deleted, res.FreedBytes))
response.JSON(w, http.StatusOK, map[string]any{
"scanned": res.Scanned,
"orphans": res.Orphans,
"deleted": res.Deleted,
"freed_bytes": res.FreedBytes,
"skipped_young": res.SkippedYoung,
})
}