package middleware import ( "context" "encoding/json" "fmt" "net/http" "reflect" "strconv" "strings" ) // ── Request Validation ──────────────────────────────────────────────────── type Validator struct { errors map[string][]string } func NewValidator() *Validator { return &Validator{errors: make(map[string][]string)} } func (v *Validator) AddError(field, message string) { v.errors[field] = append(v.errors[field], message) } func (v *Validator) HasErrors() bool { return len(v.errors) > 0 } func (v *Validator) Errors() map[string][]string { return v.errors } func (v *Validator) ErrorResponse() map[string]interface{} { return map[string]interface{}{ "error": "validation failed", "details": v.errors, } } // ValidateString kontrollerar strängfält func (v *Validator) ValidateString(field, value string, minLen, maxLen int, required bool) { if required && strings.TrimSpace(value) == "" { v.AddError(field, "is required") return } if value != "" { if len(value) < minLen { v.AddError(field, fmt.Sprintf("must be at least %d characters", minLen)) } if len(value) > maxLen { v.AddError(field, fmt.Sprintf("must be at most %d characters", maxLen)) } } } // ValidateEmail kontrollerar email func (v *Validator) ValidateEmail(field, value string, required bool) { if required && strings.TrimSpace(value) == "" { v.AddError(field, "is required") return } if value != "" && !ValidateEmail(value) { v.AddError(field, "invalid email format") } } // ValidateUUID kontrollerar UUID func (v *Validator) ValidateUUID(field, value string, required bool) { if required && strings.TrimSpace(value) == "" { v.AddError(field, "is required") return } if value != "" && !ValidateUUID(value) { v.AddError(field, "invalid UUID format") } } // ValidateInt kontrollerar heltal func (v *Validator) ValidateInt(field string, value int, min, max int, required bool) { if required && value == 0 { v.AddError(field, "is required") return } if value != 0 { if value < min { v.AddError(field, fmt.Sprintf("must be at least %d", min)) } if value > max { v.AddError(field, fmt.Sprintf("must be at most %d", max)) } } } // ValidateFloat kontrollerar decimaltal func (v *Validator) ValidateFloat(field string, value float64, min, max float64, required bool) { if required && value == 0 { v.AddError(field, "is required") return } if value != 0 { if value < min { v.AddError(field, fmt.Sprintf("must be at least %.2f", min)) } if value > max { v.AddError(field, fmt.Sprintf("must be at most %.2f", max)) } } } // ValidateEnum kontrollerar att värdet finns i tillåtna värden func (v *Validator) ValidateEnum(field, value string, allowed []string, required bool) { if required && strings.TrimSpace(value) == "" { v.AddError(field, "is required") return } if value != "" { found := false for _, a := range allowed { if a == value { found = true break } } if !found { v.AddError(field, fmt.Sprintf("must be one of: %s", strings.Join(allowed, ", "))) } } } // ── Validation Middleware ───────────────────────────────────────────────── // ValidateBody validerar request body mot en struct func ValidateBody(dst interface{}) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body == nil { http.Error(w, `{"error":"request body required"}`, http.StatusBadRequest) return } decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(dst); err != nil { http.Error(w, fmt.Sprintf(`{"error":"invalid request body: %s"}`, err.Error()), http.StatusBadRequest) return } // Validera fält validator := NewValidator() validateStruct(validator, dst) if validator.HasErrors() { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(validator.ErrorResponse()) return } // Spara validerad struct i context ctx := context.WithValue(r.Context(), "validated_body", dst) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // validateStruct validerar en struct baserat på tags func validateStruct(v *Validator, s interface{}) { val := reflect.ValueOf(s) if val.Kind() == reflect.Ptr { val = val.Elem() } typ := val.Type() for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := typ.Field(i) // Hämta validation tags tag := fieldType.Tag.Get("validate") if tag == "" { continue } // Parsa tag parts := strings.Split(tag, ",") required := false minLen := 0 maxLen := 255 for _, part := range parts { part = strings.TrimSpace(part) if part == "required" { required = true } else if strings.HasPrefix(part, "min=") { minLen, _ = strconv.Atoi(strings.TrimPrefix(part, "min=")) } else if strings.HasPrefix(part, "max=") { maxLen, _ = strconv.Atoi(strings.TrimPrefix(part, "max=")) } } // Validera baserat på typ switch field.Kind() { case reflect.String: v.ValidateString(fieldType.Name, field.String(), minLen, maxLen, required) case reflect.Int, reflect.Int64: v.ValidateInt(fieldType.Name, int(field.Int()), 0, 999999, required) case reflect.Float64: v.ValidateFloat(fieldType.Name, field.Float(), 0, 999999999, required) } } }