feat(auth): add RequireAdmin middleware for /api/admin/* routes

Replaces the old X-API-Token-based RequireAdmin in middleware.go with a
context-aware RequireAdmin() that runs after RequireUser, checks
user.IsAdmin, and returns 403 {"error":"not_authorized"} for non-admins
or 500 {"error":"internal_error"} if RequireUser was bypassed. Updates
server.go to mount RequireUser then RequireAdmin on the /api/admin group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 17:27:47 -04:00
parent f73a5ccef5
commit 4492826354
4 changed files with 144 additions and 39 deletions
+3 -38
View File
@@ -2,11 +2,6 @@ package auth
import (
"context"
"errors"
"net/http"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
@@ -15,39 +10,9 @@ type ctxKey int
const userCtxKey ctxKey = 1
// RequireAdmin gates a handler on X-API-Token matching an admin user. This is
// the Minstrel-native token path (`/api/*`); Subsonic-compatible auth under
// `/rest/*` lands with the Subsonic server.
func RequireAdmin(pool *pgxpool.Pool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-API-Token")
if token == "" {
http.Error(w, "missing X-API-Token", http.StatusUnauthorized)
return
}
q := dbq.New(pool)
user, err := q.GetUserByAPIToken(r.Context(), token)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
http.Error(w, "auth lookup failed", http.StatusInternalServerError)
return
}
if !user.IsAdmin {
http.Error(w, "admin required", http.StatusForbidden)
return
}
ctx := context.WithValue(r.Context(), userCtxKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// UserFromContext returns the authenticated user when the request passed
// through RequireAdmin (or a future RequireUser middleware).
// UserFromContext returns the authenticated user placed in context by
// RequireUser. Returns false when RequireUser has not run (e.g. in tests that
// bypass the middleware, or programmer-error routing).
func UserFromContext(ctx context.Context) (dbq.User, bool) {
u, ok := ctx.Value(userCtxKey).(dbq.User)
return u, ok