mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-27 07:29:36 +00:00
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.
112 lines
2.4 KiB
Go
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
|
|
})
|
|
}
|