M1/#293: add /api/admin/scan route gated on X-API-Token

This commit is contained in:
2026-04-19 15:16:15 +00:00
parent bea0604846
commit 8e91235b88
+44 -4
View File
@@ -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)
}