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>
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
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,
|
||
})
|
||
}
|