From 8e91235b8882aa19b31108795c5c9ea6371a992d Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Sun, 19 Apr 2026 15:16:15 +0000 Subject: [PATCH] M1/#293: add /api/admin/scan route gated on X-API-Token --- internal/server/server.go | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 71bfa3eb..e4c03dcd 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,20 +1,33 @@ package server import ( + "context" "encoding/json" "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/jackc/pgx/v5/pgxpool" + + "git.fabledsword.com/bvandeusen/minstrel/internal/auth" + "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -type Server struct { - Logger *slog.Logger +// ScanTrigger is the subset of the scanner the HTTP handler needs. Kept as an +// interface so tests can stub it without touching the DB. +type ScanTrigger interface { + Scan(ctx context.Context) (library.Stats, error) } -func New(logger *slog.Logger) *Server { - return &Server{Logger: logger} +type Server struct { + Logger *slog.Logger + Pool *pgxpool.Pool + Scanner ScanTrigger +} + +func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger) *Server { + return &Server{Logger: logger, Pool: pool, Scanner: scanner} } func (s *Server) Router() http.Handler { @@ -23,6 +36,17 @@ func (s *Server) Router() http.Handler { r.Use(middleware.Recoverer) r.Get("/healthz", s.handleHealthz) + + if s.Pool != nil { + r.Route("/api", func(api chi.Router) { + api.Route("/admin", func(admin chi.Router) { + admin.Use(auth.RequireAdmin(s.Pool)) + if s.Scanner != nil { + admin.Post("/scan", s.handleAdminScan) + } + }) + }) + } return r } @@ -31,3 +55,19 @@ func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } + +// handleAdminScan runs a scan synchronously and returns the resulting stats. +// Kept synchronous for v1: libraries are small and the operator can tell when +// it's done. Async jobs with status polling are a later-milestone concern. +func (s *Server) handleAdminScan(w http.ResponseWriter, r *http.Request) { + stats, err := s.Scanner.Scan(r.Context()) + w.Header().Set("Content-Type", "application/json") + if err != nil { + s.Logger.Error("admin scan failed", "err", err) + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]any{"error": err.Error(), "stats": stats}) + return + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(stats) +}