package middleware import ( "context" "encoding/json" "errors" "farga/lib" "fmt" "net/http" "net/url" "reflect" "strconv" "strings" "github.com/go-playground/validator/v10" ) var validate *validator.Validate = validator.New() type contextKey[T any] struct{} // allows using types as context keys func decodeForm[T any](values url.Values) (T, error) { var dst T v := reflect.ValueOf(&dst).Elem() t := v.Type() if t.Kind() != reflect.Struct { return dst, fmt.Errorf("T must be a struct") } for i := 0; i < t.NumField(); i++ { field := t.Field(i) name := field.Tag.Get("form") if name == "" { name = strings.ToLower(field.Name) } raw := values.Get(name) if raw == "" { continue } f := v.Field(i) switch f.Kind() { case reflect.String: f.SetString(raw) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(raw, 10, f.Type().Bits()) if err != nil { return dst, fmt.Errorf("field %q: %w", name, err) } f.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: n, err := strconv.ParseUint(raw, 10, f.Type().Bits()) if err != nil { return dst, fmt.Errorf("field %q: %w", name, err) } f.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(raw, f.Type().Bits()) if err != nil { return dst, fmt.Errorf("field %q: %w", name, err) } f.SetFloat(n) case reflect.Bool: b, err := strconv.ParseBool(raw) if err != nil { return dst, fmt.Errorf("field %q: %w", name, err) } f.SetBool(b) } } return dst, nil } type ValidationError struct { Field string Message string } type ValidationErrors []ValidationError // replaces the ugly default error with a custom and better one. func fieldErrorMessage(fe validator.FieldError) string { switch fe.Tag() { case "required": return fmt.Sprintf("%s is required", fe.Field()) case "email": return fmt.Sprintf("%s must be a valid email address", fe.Field()) case "min": return fmt.Sprintf("%s must be at least %s characters", fe.Field(), fe.Param()) case "max": return fmt.Sprintf("%s must be at most %s characters", fe.Field(), fe.Param()) case "len": return fmt.Sprintf("%s must be exactly %s characters", fe.Field(), fe.Param()) case "gte": return fmt.Sprintf("%s must be greater than or equal to %s", fe.Field(), fe.Param()) case "lte": return fmt.Sprintf("%s must be less than or equal to %s", fe.Field(), fe.Param()) default: return fe.Error() } } // validates a given form and makes it available like form, _ := md.GetForm[(form)](r). func ValidateForm[form_type any](next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { next.ServeHTTP(w, r) return } err := r.ParseForm() if err != nil { http.Error(w, "Bad form", http.StatusBadRequest) return } form, err := decodeForm[form_type](r.Form) if err != nil { http.Error(w, "Invalid form", http.StatusBadRequest) // validation options might be wrong if this happens. return } err = validate.Struct(form) if err != nil { var errs ValidationErrors if _, ok := errors.AsType[validator.ValidationErrors](err); ok { for _, err := range err.(validator.ValidationErrors) { valError := ValidationError{ Field: err.Field(), Message: fieldErrorMessage(err), } errs = append(errs, valError) } // http.Error(w, "Invalid form params: "+errs.Error(), http.StatusBadRequest) } // encode errors as base64 marshalled, err := json.Marshal(errs) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } http.Redirect(w, r, r.URL.Path+"?err="+lib.ValidationErrorKey+lib.Base64Encoder.EncodeToString(marshalled), http.StatusSeeOther) return } ctx := context.WithValue(r.Context(), contextKey[form_type]{}, form) next.ServeHTTP(w, r.WithContext(ctx)) }) } // returns requested item + true if found, false if not. func Form[T any](r *http.Request) (T, bool) { form, ok := r.Context().Value(contextKey[T]{}).(T) return form, ok }