156 lines
4.9 KiB
Go
156 lines
4.9 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"veza-backend-api/internal/response"
|
|
"veza-backend-api/internal/validators"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-playground/validator/v10"
|
|
)
|
|
|
|
// DefaultMaxJSONBodySize TASK-SEC-005: 1MB par défaut (configurable via MAX_JSON_BODY_SIZE)
|
|
const DefaultMaxJSONBodySize = 1 * 1024 * 1024 // 1MB
|
|
|
|
// GetMaxJSONBodySize returns the max JSON body size from MAX_JSON_BODY_SIZE env (bytes), default 1MB.
|
|
func GetMaxJSONBodySize() int64 {
|
|
if v := os.Getenv("MAX_JSON_BODY_SIZE"); v != "" {
|
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 {
|
|
return n
|
|
}
|
|
}
|
|
return DefaultMaxJSONBodySize
|
|
}
|
|
|
|
// MaxJSONBodySize kept for backward compatibility in tests; use GetMaxJSONBodySize() for runtime limit.
|
|
var MaxJSONBodySize = DefaultMaxJSONBodySize
|
|
|
|
// BindAndValidateJSON lie et valide les données JSON de la requête de manière robuste
|
|
// MOD-P1-002: Helper centralisé pour bind + validate + format d'erreur standard
|
|
// Utilisable par tous les handlers (pas seulement ceux avec CommonHandler)
|
|
//
|
|
// Comportement:
|
|
// - Vérifie la taille du body (max 10MB)
|
|
// - Parse le JSON avec ShouldBindJSON (Gin)
|
|
// - Valide avec le validator centralisé (go-playground/validator)
|
|
// - Retourne false si erreur (erreur déjà envoyée au client avec code 400)
|
|
//
|
|
// Usage:
|
|
//
|
|
// var req MyRequest
|
|
// if !common.BindAndValidateJSON(c, &req) {
|
|
// return // Erreur déjà envoyée au client
|
|
// }
|
|
func BindAndValidateJSON(c *gin.Context, obj interface{}) bool {
|
|
maxSize := GetMaxJSONBodySize()
|
|
// 1. Vérifier la taille du body
|
|
if c.Request.ContentLength > maxSize {
|
|
response.BadRequest(c, fmt.Sprintf("Request body too large: maximum size is %d bytes", maxSize))
|
|
return false
|
|
}
|
|
|
|
// 2. Limiter la lecture du body pour éviter les attaques par body trop gros
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
|
|
|
// 3. Parser le JSON avec ShouldBindJSON
|
|
if err := c.ShouldBindJSON(obj); err != nil {
|
|
handleBindingError(c, err)
|
|
return false
|
|
}
|
|
|
|
// 4. Valider avec le validator centralisé (go-playground/validator)
|
|
validator := validators.NewValidator()
|
|
validationErrors := validator.Validate(obj)
|
|
if len(validationErrors) > 0 {
|
|
// Convertir en format standardisé
|
|
details := make(map[string]string)
|
|
for _, ve := range validationErrors {
|
|
details[ve.Field] = ve.Message
|
|
}
|
|
response.ValidationError(c, "Validation failed", details)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// BindAndValidate binds the request body to the given object and validates it.
|
|
// DEPRECATED: Utiliser BindAndValidateJSON à la place pour une validation complète
|
|
// If binding or validation fails, it sends a standardized error response and returns false.
|
|
// Returns true if successful.
|
|
func BindAndValidate(c *gin.Context, obj interface{}) bool {
|
|
return BindAndValidateJSON(c, obj)
|
|
}
|
|
|
|
// handleBindingError gère les erreurs de binding JSON
|
|
// MOD-P1-002: Gestion d'erreurs améliorée pour JSON malformé
|
|
func handleBindingError(c *gin.Context, err error) {
|
|
var jsonSyntaxError *json.SyntaxError
|
|
var jsonUnmarshalTypeError *json.UnmarshalTypeError
|
|
var maxBytesError *http.MaxBytesError
|
|
var ve validator.ValidationErrors
|
|
|
|
switch {
|
|
case errors.As(err, &maxBytesError):
|
|
// Body trop gros
|
|
response.BadRequest(c, fmt.Sprintf("Request body too large: maximum size is %d bytes", GetMaxJSONBodySize()))
|
|
|
|
case errors.As(err, &jsonSyntaxError):
|
|
// JSON syntaxiquement invalide
|
|
response.BadRequest(c, fmt.Sprintf("Invalid JSON syntax at offset %d: %s", jsonSyntaxError.Offset, jsonSyntaxError.Error()))
|
|
|
|
case errors.As(err, &jsonUnmarshalTypeError):
|
|
// Type incorrect pour un champ
|
|
response.BadRequest(c, fmt.Sprintf("Invalid type for field '%s': expected %s", jsonUnmarshalTypeError.Field, jsonUnmarshalTypeError.Type.String()))
|
|
|
|
case errors.Is(err, io.EOF):
|
|
// Body vide
|
|
response.BadRequest(c, "Request body is empty or invalid JSON")
|
|
|
|
case errors.Is(err, io.ErrUnexpectedEOF):
|
|
// JSON incomplet
|
|
response.BadRequest(c, "Incomplete or malformed JSON")
|
|
|
|
case errors.As(err, &ve):
|
|
// Erreurs de validation Gin (binding tags)
|
|
details := make(map[string]string)
|
|
for _, fe := range ve {
|
|
details[fe.Field()] = msgForTag(fe)
|
|
}
|
|
response.ValidationError(c, "Validation failed", details)
|
|
|
|
default:
|
|
// Erreur générique
|
|
errStr := err.Error()
|
|
if strings.Contains(errStr, "unknown field") {
|
|
response.BadRequest(c, "Unknown fields in JSON payload")
|
|
} else {
|
|
response.BadRequest(c, "Invalid request body format")
|
|
}
|
|
}
|
|
}
|
|
|
|
func msgForTag(fe validator.FieldError) string {
|
|
switch fe.Tag() {
|
|
case "required":
|
|
return "This field is required"
|
|
case "email":
|
|
return "Invalid email format"
|
|
case "min":
|
|
return fmt.Sprintf("Must be at least %s characters long", fe.Param())
|
|
case "max":
|
|
return fmt.Sprintf("Must be at most %s characters long", fe.Param())
|
|
case "uuid":
|
|
return "Invalid UUID format"
|
|
default:
|
|
return fmt.Sprintf("Failed validation on tag: %s", fe.Tag())
|
|
}
|
|
}
|