veza/veza-backend-api/internal/common/validation.go
senke b6c004319c
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
v0.9.2
2026-03-05 19:27:34 +01:00

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())
}
}