Files
bvandeusen 4492826354 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>
2026-04-29 17:27:47 -04:00

46 lines
1.5 KiB
Go

package auth
import (
"encoding/json"
"net/http"
)
// RequireAdmin is a middleware that MUST run after RequireUser. It reads the
// authenticated user from request context and rejects non-admin callers with
// 403. If no user is in context (RequireUser was bypassed), it returns 500 —
// that is a programmer error in the routing setup, not a client error.
//
// Mount this on /api/admin/* after auth.RequireUser:
//
// r.Route("/api/admin", func(admin chi.Router) {
// admin.Use(auth.RequireUser(pool))
// admin.Use(auth.RequireAdmin())
// ...
// })
func RequireAdmin() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := UserFromContext(r.Context())
if !ok {
// Programmer error: RequireUser was not mounted before RequireAdmin.
writeAdminErr(w, http.StatusInternalServerError, "internal_error")
return
}
if !user.IsAdmin {
writeAdminErr(w, http.StatusForbidden, "not_authorized")
return
}
next.ServeHTTP(w, r)
})
}
}
// writeAdminErr writes a flat JSON error envelope {"error":"<code>"} and sets
// Content-Type. Uses a flat envelope (not the nested api.errorBody shape)
// because the spec for /api/admin/* errors defines {"error":"<code>"} directly.
func writeAdminErr(w http.ResponseWriter, status int, code string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": code})
}