// 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 } return errors.Wrap(errors.CodeValidationFailed, "validation failed", err) } // 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:", " 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" )