You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
5.1 KiB
Go
199 lines
5.1 KiB
Go
// Package validation provides validation utilities for the KnowFoolery application.
|
|
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
"knowfoolery/backend/shared/domain/errors"
|
|
)
|
|
|
|
// Validator wraps the go-playground validator with custom validations.
|
|
type Validator struct {
|
|
validate *validator.Validate
|
|
}
|
|
|
|
// NewValidator creates a new Validator with custom validations registered.
|
|
func NewValidator() *Validator {
|
|
v := validator.New()
|
|
|
|
// Use JSON tag names in error messages
|
|
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
|
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
|
if name == "-" {
|
|
return ""
|
|
}
|
|
return name
|
|
})
|
|
|
|
// Register custom validations
|
|
err := v.RegisterValidation("alphanum_space", validateAlphanumSpace)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = v.RegisterValidation("no_html", validateNoHTML)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = v.RegisterValidation("safe_text", validateSafeText)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = v.RegisterValidation("player_name", validatePlayerName)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return &Validator{validate: v}
|
|
}
|
|
|
|
// Validate validates a struct and returns a domain error if validation fails.
|
|
func (v *Validator) Validate(s interface{}) error {
|
|
err := v.validate.Struct(s)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
validationErrors, ok := err.(validator.ValidationErrors)
|
|
if !ok {
|
|
return errors.Wrap(errors.CodeValidationFailed, "validation failed", err)
|
|
}
|
|
|
|
messages := make([]string, 0, len(validationErrors))
|
|
for _, e := range validationErrors {
|
|
messages = append(messages, formatValidationError(e))
|
|
}
|
|
|
|
return errors.Wrap(
|
|
errors.CodeValidationFailed,
|
|
strings.Join(messages, "; "),
|
|
nil,
|
|
)
|
|
}
|
|
|
|
// ValidateVar validates a single variable.
|
|
func (v *Validator) ValidateVar(field interface{}, tag string) error {
|
|
err := v.validate.Var(field, tag)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
validationErrors, ok := err.(validator.ValidationErrors)
|
|
if !ok {
|
|
return errors.Wrap(errors.CodeValidationFailed, "validation failed", err)
|
|
}
|
|
|
|
messages := make([]string, 0, len(validationErrors))
|
|
for _, e := range validationErrors {
|
|
messages = append(messages, formatValidationError(e))
|
|
}
|
|
|
|
return errors.Wrap(
|
|
errors.CodeValidationFailed,
|
|
strings.Join(messages, "; "),
|
|
nil,
|
|
)
|
|
}
|
|
|
|
// formatValidationError formats a single validation error into a readable message.
|
|
func formatValidationError(e validator.FieldError) string {
|
|
field := e.Field()
|
|
|
|
switch e.Tag() {
|
|
case "required":
|
|
return fmt.Sprintf("%s is required", field)
|
|
case "min":
|
|
return fmt.Sprintf("%s must be at least %s characters", field, e.Param())
|
|
case "max":
|
|
return fmt.Sprintf("%s must be at most %s characters", field, e.Param())
|
|
case "email":
|
|
return fmt.Sprintf("%s must be a valid email", field)
|
|
case "alphanum":
|
|
return fmt.Sprintf("%s must contain only alphanumeric characters", field)
|
|
case "alphanum_space":
|
|
return fmt.Sprintf("%s must contain only alphanumeric characters and spaces", field)
|
|
case "player_name":
|
|
return fmt.Sprintf("%s must be a valid player name (2-50 chars, alphanumeric with spaces)", field)
|
|
case "oneof":
|
|
return fmt.Sprintf("%s must be one of: %s", field, e.Param())
|
|
case "gte":
|
|
return fmt.Sprintf("%s must be greater than or equal to %s", field, e.Param())
|
|
case "lte":
|
|
return fmt.Sprintf("%s must be less than or equal to %s", field, e.Param())
|
|
case "uuid":
|
|
return fmt.Sprintf("%s must be a valid UUID", field)
|
|
default:
|
|
return fmt.Sprintf("%s is invalid", field)
|
|
}
|
|
}
|
|
|
|
// Custom validation functions
|
|
|
|
// validateAlphanumSpace validates that a string contains only alphanumeric characters and spaces.
|
|
func validateAlphanumSpace(fl validator.FieldLevel) bool {
|
|
str := fl.Field().String()
|
|
for _, r := range str {
|
|
if !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsSpace(r) && r != '-' && r != '_' && r != '.' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// validateNoHTML validates that a string contains no HTML tags.
|
|
func validateNoHTML(fl validator.FieldLevel) bool {
|
|
str := fl.Field().String()
|
|
return !strings.Contains(str, "<") && !strings.Contains(str, ">")
|
|
}
|
|
|
|
// validateSafeText validates that a string contains no potentially dangerous patterns.
|
|
func validateSafeText(fl validator.FieldLevel) bool {
|
|
str := strings.ToLower(fl.Field().String())
|
|
dangerousPatterns := []string{
|
|
"javascript:",
|
|
"data:",
|
|
"vbscript:",
|
|
"<script",
|
|
"</script",
|
|
}
|
|
|
|
for _, pattern := range dangerousPatterns {
|
|
if strings.Contains(str, pattern) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// validatePlayerName validates a player name format.
|
|
func validatePlayerName(fl validator.FieldLevel) bool {
|
|
str := fl.Field().String()
|
|
|
|
// Length check
|
|
if len(str) < 2 || len(str) > 50 {
|
|
return false
|
|
}
|
|
|
|
// Character validation
|
|
for _, r := range str {
|
|
if !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsSpace(r) && r != '-' && r != '_' && r != '.' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Common validation tags for reuse
|
|
const (
|
|
TagRequired = "required"
|
|
TagPlayerName = "required,player_name"
|
|
TagEmail = "required,email"
|
|
TagUUID = "required,uuid"
|
|
TagOptionalUUID = "omitempty,uuid"
|
|
)
|