first
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user