266 lines
6.9 KiB
Go
266 lines
6.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-playground/validator/v10"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ValidationError represents a validation error
|
|
type ValidationError struct {
|
|
Field string `json:"field"`
|
|
Tag string `json:"tag"`
|
|
Value string `json:"value"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// ValidationResponse represents the validation error response
|
|
type ValidationResponse struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message"`
|
|
Details []ValidationError `json:"details,omitempty"`
|
|
}
|
|
|
|
var validate *validator.Validate
|
|
|
|
func init() {
|
|
validate = validator.New()
|
|
|
|
// Register custom tag name function to use json tags
|
|
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
|
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
|
if name == "-" {
|
|
return ""
|
|
}
|
|
return name
|
|
})
|
|
}
|
|
|
|
// ValidateJSON validates JSON request body against struct validation tags
|
|
func ValidateJSON(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Skip validation for GET requests and requests without body
|
|
if c.Request.Method == "GET" || c.Request.ContentLength == 0 {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Store original body for potential re-reading
|
|
c.Set("validation_enabled", true)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ValidateStruct validates a struct and returns formatted errors
|
|
func ValidateStruct(s interface{}) []ValidationError {
|
|
var errors []ValidationError
|
|
|
|
err := validate.Struct(s)
|
|
if err != nil {
|
|
for _, err := range err.(validator.ValidationErrors) {
|
|
var element ValidationError
|
|
element.Field = err.Field()
|
|
element.Tag = err.Tag()
|
|
element.Value = err.Param()
|
|
element.Message = getErrorMessage(err)
|
|
errors = append(errors, element)
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
// ValidateAndBind validates and binds JSON request to struct
|
|
func ValidateAndBind(c *gin.Context, obj interface{}) error {
|
|
// Bind JSON to struct
|
|
if err := c.ShouldBindJSON(obj); err != nil {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Invalid JSON",
|
|
Message: "Request body contains invalid JSON: " + err.Error(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Validate struct
|
|
if validationErrors := ValidateStruct(obj); len(validationErrors) > 0 {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Validation Failed",
|
|
Message: "Request validation failed",
|
|
Details: validationErrors,
|
|
})
|
|
return validator.ValidationErrors{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getErrorMessage returns a human-readable error message for validation errors
|
|
func getErrorMessage(fe validator.FieldError) string {
|
|
switch fe.Tag() {
|
|
case "required":
|
|
return "This field is required"
|
|
case "email":
|
|
return "Invalid email format"
|
|
case "min":
|
|
return "Value is too short (minimum " + fe.Param() + " characters)"
|
|
case "max":
|
|
return "Value is too long (maximum " + fe.Param() + " characters)"
|
|
case "url":
|
|
return "Invalid URL format"
|
|
case "oneof":
|
|
return "Value must be one of: " + fe.Param()
|
|
case "uuid":
|
|
return "Invalid UUID format"
|
|
case "gte":
|
|
return "Value must be greater than or equal to " + fe.Param()
|
|
case "lte":
|
|
return "Value must be less than or equal to " + fe.Param()
|
|
case "len":
|
|
return "Value must be exactly " + fe.Param() + " characters"
|
|
case "dive":
|
|
return "Invalid array element"
|
|
default:
|
|
return "Invalid value for " + fe.Field()
|
|
}
|
|
}
|
|
|
|
// RequiredFields validates that specific fields are present in the request
|
|
func RequiredFields(fields ...string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var json map[string]interface{}
|
|
|
|
if err := c.ShouldBindJSON(&json); err != nil {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Invalid JSON",
|
|
Message: "Request body contains invalid JSON",
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
var missingFields []string
|
|
for _, field := range fields {
|
|
if _, exists := json[field]; !exists {
|
|
missingFields = append(missingFields, field)
|
|
}
|
|
}
|
|
|
|
if len(missingFields) > 0 {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Missing Required Fields",
|
|
Message: "The following required fields are missing: " + strings.Join(missingFields, ", "),
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Store the parsed JSON for use in handlers
|
|
c.Set("parsed_json", json)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ValidateUUID validates that a URL parameter is a valid UUID
|
|
func ValidateUUID(param string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
value := c.Param(param)
|
|
if value == "" {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Missing Parameter",
|
|
Message: "Required parameter '" + param + "' is missing",
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Validate UUID format
|
|
if err := validate.Var(value, "uuid"); err != nil {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Invalid Parameter",
|
|
Message: "Parameter '" + param + "' must be a valid UUID",
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ValidateQueryParams validates query parameters
|
|
func ValidateQueryParams(rules map[string]string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var errors []ValidationError
|
|
|
|
for param, rule := range rules {
|
|
value := c.Query(param)
|
|
if value != "" {
|
|
if err := validate.Var(value, rule); err != nil {
|
|
for _, err := range err.(validator.ValidationErrors) {
|
|
errors = append(errors, ValidationError{
|
|
Field: param,
|
|
Tag: err.Tag(),
|
|
Value: err.Param(),
|
|
Message: getErrorMessage(err),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
c.JSON(http.StatusBadRequest, ValidationResponse{
|
|
Error: "Invalid Query Parameters",
|
|
Message: "One or more query parameters are invalid",
|
|
Details: errors,
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// SanitizeInput sanitizes input strings to prevent XSS and injection attacks
|
|
func SanitizeInput() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// This is a basic implementation - in production you might want to use
|
|
// a more sophisticated sanitization library like bluemonday
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ValidatePermissions validates that permission scopes follow the expected format
|
|
func ValidatePermissions(c *gin.Context, permissions []string) []ValidationError {
|
|
var errors []ValidationError
|
|
|
|
for i, perm := range permissions {
|
|
// Check basic format: should contain only alphanumeric, dots, and underscores
|
|
if err := validate.Var(perm, "required,min=1,max=255,alphanum|contains=.|contains=_"); err != nil {
|
|
errors = append(errors, ValidationError{
|
|
Field: "permissions[" + string(rune(i)) + "]",
|
|
Tag: "format",
|
|
Value: perm,
|
|
Message: "Permission scope must contain only alphanumeric characters, dots, and underscores",
|
|
})
|
|
}
|
|
|
|
// Check for dangerous patterns
|
|
if strings.Contains(perm, "..") || strings.HasPrefix(perm, ".") || strings.HasSuffix(perm, ".") {
|
|
errors = append(errors, ValidationError{
|
|
Field: "permissions[" + string(rune(i)) + "]",
|
|
Tag: "format",
|
|
Value: perm,
|
|
Message: "Permission scope has invalid format",
|
|
})
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|