Files
scrabble-game/backend/internal/server/middleware.go
T
developer 3a640a17a4
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 13s
Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11)
2026-06-04 07:27:49 +00:00

80 lines
2.4 KiB
Go

package server
import (
"context"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// headerUserID is the identity header the gateway injects after resolving a
// session to an internal account.
const headerUserID = "X-User-ID"
// contextKey is an unexported type for request-context keys set by this package.
type contextKey string
const userIDContextKey contextKey = "scrabble.user_id"
// RequireUserID returns middleware that requires a valid X-User-ID header and
// stores the parsed account id in the request context. Requests without a
// parseable UUID are rejected with 401. The backend treats X-User-ID as the
// sole identity input and never derives identity from the request body.
func RequireUserID() gin.HandlerFunc {
return func(c *gin.Context) {
id, err := uuid.Parse(c.GetHeader(headerUserID))
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing or invalid X-User-ID"})
return
}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), userIDContextKey, id))
c.Next()
}
}
// UserIDFromContext returns the authenticated account id stored by
// RequireUserID, and whether it was present.
func UserIDFromContext(ctx context.Context) (uuid.UUID, bool) {
id, ok := ctx.Value(userIDContextKey).(uuid.UUID)
return id, ok
}
// requireSameOrigin guards the admin console's state-changing requests: it rejects
// a non-safe request whose Origin (or, failing that, Referer) host does not match
// the request Host. The gateway authenticates the operator with Basic-Auth in front
// of /_gm; this same-origin check is the console's CSRF defence, stopping a
// cross-site form POST from riding the cached credential. Safe methods pass through.
func requireSameOrigin() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
c.Next()
return
}
if !sameOrigin(c.Request) {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
// sameOrigin reports whether the request's Origin (or, failing that, Referer) host
// matches the request Host. A state-changing request carrying neither header is
// rejected.
func sameOrigin(r *http.Request) bool {
for _, h := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if h == "" {
continue
}
u, err := url.Parse(h)
if err != nil {
return false
}
return u.Host == r.Host
}
return false
}