154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
)
|
|
|
|
const BrandIDHeader = "X-Brand-ID"
|
|
|
|
func BrandScope(pool *pgxpool.Pool) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
actor, ok := auth.ActorFromCtx(c.Request.Context())
|
|
if !ok || actor.TenantID == 0 || actor.UserID == 0 {
|
|
response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
brandID, hasHeader, err := requestedBrandID(c.GetHeader(BrandIDHeader))
|
|
if err != nil {
|
|
response.Error(c, response.ErrBadRequest(40033, "invalid_brand_id", "brand id must be a positive number"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
if !hasHeader {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
if pool == nil {
|
|
response.Error(c, response.ErrInternal(50012, "brand_scope_unavailable", "brand scope validation is unavailable"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
if err := validateBrandScope(c.Request.Context(), pool, actor.TenantID, brandID); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
response.Error(c, response.ErrForbidden(40313, "brand_forbidden", "brand is not available for current tenant"))
|
|
} else {
|
|
response.Error(c, response.ErrInternal(50012, "brand_scope_lookup_failed", "failed to validate brand scope"))
|
|
}
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
c.Request = c.Request.WithContext(auth.WithCurrentBrandID(c.Request.Context(), brandID))
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func RequireCurrentBrand() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if _, ok := auth.CurrentBrandIDFromCtx(c.Request.Context()); !ok {
|
|
response.Error(c, response.ErrBadRequest(40033, "brand_context_required", "current brand is required"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func ArticleBrandScope(pool *pgxpool.Pool) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
rawArticleID := strings.TrimSpace(c.Param("id"))
|
|
if rawArticleID == "" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
articleID, err := strconv.ParseInt(rawArticleID, 10, 64)
|
|
if err != nil || articleID <= 0 {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
actor, ok := auth.ActorFromCtx(c.Request.Context())
|
|
if !ok || actor.TenantID == 0 {
|
|
response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
brandID, ok := auth.CurrentBrandIDFromCtx(c.Request.Context())
|
|
if !ok {
|
|
response.Error(c, response.ErrBadRequest(40033, "brand_context_required", "current brand is required"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
if pool == nil {
|
|
response.Error(c, response.ErrInternal(50012, "brand_scope_unavailable", "brand scope validation is unavailable"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
var exists bool
|
|
if err := pool.QueryRow(c.Request.Context(), `
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM articles
|
|
WHERE id = $1
|
|
AND tenant_id = $2
|
|
AND brand_id = $3
|
|
AND deleted_at IS NULL
|
|
)
|
|
`, articleID, actor.TenantID, brandID).Scan(&exists); err != nil {
|
|
response.Error(c, response.ErrInternal(50012, "brand_scope_lookup_failed", "failed to validate article brand scope"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
if !exists {
|
|
response.Error(c, response.ErrNotFound(40411, "article_not_found", "article not found"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func requestedBrandID(raw string) (int64, bool, error) {
|
|
trimmed := strings.TrimSpace(raw)
|
|
if trimmed == "" {
|
|
return 0, false, nil
|
|
}
|
|
brandID, err := strconv.ParseInt(trimmed, 10, 64)
|
|
if err != nil {
|
|
return 0, true, err
|
|
}
|
|
if brandID <= 0 {
|
|
return 0, true, fmt.Errorf("brand id must be positive")
|
|
}
|
|
return brandID, true, nil
|
|
}
|
|
|
|
func validateBrandScope(ctx context.Context, pool *pgxpool.Pool, tenantID, brandID int64) error {
|
|
var exists int
|
|
return pool.QueryRow(ctx, `
|
|
SELECT 1
|
|
FROM brands
|
|
WHERE tenant_id = $1
|
|
AND id = $2
|
|
AND deleted_at IS NULL
|
|
LIMIT 1
|
|
`, tenantID, brandID).Scan(&exists)
|
|
}
|