// Package middleware provides HTTP middleware shared across modules, // notably the admin/customer auth guards, CORS and rate limiting. package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "backend/internal/platform/security" ) const ( ctxUserIDKey = "auth_user_id" ctxRoleKey = "auth_role" ) func extractBearerToken(c *gin.Context) (string, bool) { header := c.GetHeader("Authorization") if header == "" { return "", false } parts := strings.SplitN(header, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") || parts[1] == "" { return "", false } return parts[1], true } // requireAudience builds a guard that only accepts access tokens issued for // the given audience (admin or customer space) and role. Because the JWT // audience and the expected role are both checked, an admin-space token can // never authenticate a customer-only route, and vice versa. func requireAudience(secret string, aud security.Audience, role string) gin.HandlerFunc { return func(c *gin.Context) { tokenString, ok := extractBearerToken(c) if !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) return } claims, err := security.ParseAccessToken(secret, tokenString, aud) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"}) return } if claims.Role != role { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "insufficient role"}) return } userID, err := uuid.Parse(claims.Subject) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid subject claim"}) return } c.Set(ctxUserIDKey, userID) c.Set(ctxRoleKey, claims.Role) c.Next() } } // RequireAdmin protects admin-panel routes. Only tokens issued in the admin // audience with role "admin" pass. func RequireAdmin(secret string) gin.HandlerFunc { return requireAudience(secret, security.AudienceAdmin, "admin") } // RequireCustomer protects storefront account routes. Only tokens issued in // the customer audience with role "customer" pass. Not mounted yet in this // phase (no customer-facing routes), but kept fully separate from // RequireAdmin so activating the customer module later never risks sharing // a session/cookie space with the admin panel. func RequireCustomer(secret string) gin.HandlerFunc { return requireAudience(secret, security.AudienceCustomer, "customer") } // GetUserID returns the authenticated user's ID set by RequireAdmin/RequireCustomer. func GetUserID(c *gin.Context) (uuid.UUID, bool) { v, exists := c.Get(ctxUserIDKey) if !exists { return uuid.UUID{}, false } id, ok := v.(uuid.UUID) return id, ok } // GetRole returns the authenticated user's role set by RequireAdmin/RequireCustomer. func GetRole(c *gin.Context) (string, bool) { v, exists := c.Get(ctxRoleKey) if !exists { return "", false } role, ok := v.(string) return role, ok }