Files
3x-ui/web/middleware/validate.go
MHSanaei 7fda988fb2 feat(backend): gate request bodies with go-playground/validator
Add a generic BindAndValidate helper in web/middleware that wraps gin's
content-aware binder with an explicit validator.Struct call and emits a
structured `entity.Msg{Obj: ValidationPayload{Issues...}}` on failure so
the frontend can map each issue to an i18n key.

Tag the user-facing fields on model.Inbound, model.Node, and
entity.AllSetting with the range/enum constraints they were previously
relying on hand-rolled CheckValid logic (or nothing) to enforce, and
wire the helper into the inbound/node/settings controllers that bind
those structs directly. Promotes validator/v10 from indirect to direct
require, plus six unit tests covering valid payloads, range violations,
enum violations, malformed JSON, in-place binding, and JSON-only strict
mode.

This is PR1 of a planned end-to-end Zod rollout — controllers using
local form structs (custom_geo, setEnable, fallbacks, client) keep
their existing handling and will be migrated as their schemas firm up.
2026-05-25 19:17:54 +02:00

112 lines
2.4 KiB
Go

package middleware
import (
"errors"
"net/http"
"reflect"
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/mhsanaei/3x-ui/v3/web/entity"
)
var validate = validator.New(validator.WithRequiredStructEnabled())
func BindAndValidate[T any](c *gin.Context) (*T, bool) {
var dst T
if err := c.ShouldBind(&dst); err != nil {
writeBindFailure(c, err)
return nil, false
}
if err := validate.Struct(&dst); err != nil {
writeBindFailure(c, err)
return nil, false
}
return &dst, true
}
func BindAndValidateInto(c *gin.Context, dst any) bool {
if err := c.ShouldBind(dst); err != nil {
writeBindFailure(c, err)
return false
}
if err := validate.Struct(dst); err != nil {
writeBindFailure(c, err)
return false
}
return true
}
func BindJSONAndValidate[T any](c *gin.Context) (*T, bool) {
var dst T
if err := c.ShouldBindWith(&dst, binding.JSON); err != nil {
writeBindFailure(c, err)
return nil, false
}
if err := validate.Struct(&dst); err != nil {
writeBindFailure(c, err)
return nil, false
}
return &dst, true
}
func BindJSONAndValidateInto(c *gin.Context, dst any) bool {
if err := c.ShouldBindWith(dst, binding.JSON); err != nil {
writeBindFailure(c, err)
return false
}
if err := validate.Struct(dst); err != nil {
writeBindFailure(c, err)
return false
}
return true
}
type FieldIssue struct {
Field string `json:"field"`
Rule string `json:"rule"`
Param string `json:"param,omitempty"`
Message string `json:"message"`
}
type ValidationPayload struct {
Issues []FieldIssue `json:"issues"`
Message string `json:"message"`
}
func writeBindFailure(c *gin.Context, err error) {
payload := ValidationPayload{Issues: []FieldIssue{}, Message: err.Error()}
var ve validator.ValidationErrors
if errors.As(err, &ve) {
payload.Issues = make([]FieldIssue, 0, len(ve))
for _, fe := range ve {
payload.Issues = append(payload.Issues, FieldIssue{
Field: fe.Field(),
Rule: fe.Tag(),
Param: fe.Param(),
Message: fe.Error(),
})
}
}
c.AbortWithStatusJSON(http.StatusOK, entity.Msg{
Success: false,
Msg: "request body failed validation",
Obj: payload,
})
}
func init() {
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" || name == "" {
return fld.Name
}
return name
})
}