e5623d2f84
- Add Infrastructure Health section with CPU/Memory/Disk panels - Add Service Status section with PM2/Docker panels - Create GrafanaPanel component for iframe embedding - Build passes successfully
221 lines
5.5 KiB
Go
221 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|