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, }) }