// Package userid extracts the calling user identifier from the trusted // `X-User-ID` header injected by gateway and exposes it through the request // context. // // Backend trusts the header value because the network segment between gateway // and backend is the trust boundary (see `ARCHITECTURE.md` ยง15). The // middleware therefore only validates the syntactic shape (UUID) and rejects // malformed or absent values with the standard `400 invalid_request` envelope. package userid import ( "context" "net/http" "strings" "galaxy/backend/internal/server/httperr" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // Header is the canonical case-correct header name carrying the trusted user // id forwarded by gateway. const Header = "X-User-ID" // userIDContextKey is the unexported context key used to store the parsed // user id. The unexported value type prevents accidental collisions with // keys defined in unrelated packages. type userIDContextKey struct{} // Middleware returns the gin middleware that requires a syntactically valid // `X-User-ID` header on every authenticated user request and stores the // parsed UUID on the request context. func Middleware() gin.HandlerFunc { return func(c *gin.Context) { raw := strings.TrimSpace(c.GetHeader(Header)) if raw == "" { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, Header+" header is required") return } userID, err := uuid.Parse(raw) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, Header+" header must be a valid UUID") return } c.Request = c.Request.WithContext(WithValue(c.Request.Context(), userID)) c.Next() } } // FromContext returns the user id stored on ctx by Middleware. The boolean // reports whether a value was found. func FromContext(ctx context.Context) (uuid.UUID, bool) { if ctx == nil { return uuid.Nil, false } value, ok := ctx.Value(userIDContextKey{}).(uuid.UUID) if !ok { return uuid.Nil, false } return value, true } // WithValue stores userID on ctx under the package-private context key. // Exposed for tests that need to build a context outside the middleware. func WithValue(ctx context.Context, userID uuid.UUID) context.Context { return context.WithValue(ctx, userIDContextKey{}, userID) }