diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 00000000..0f847480 --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,54 @@ +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" +) + +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). +func UserFromContext(ctx context.Context) (dbq.User, bool) { + u, ok := ctx.Value(userCtxKey).(dbq.User) + return u, ok +}