101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
// 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
|
|
}
|