Kaynağa Gözat

P1 #6: Validation with go-playground/validator

- Added internal/validator package with custom validators (uuid, slug, latitude, longitude, place_type, user_role, currency, datetime, etc.)
- Updated auth handlers (register, login) with validation
- Updated places handlers (create, update, moderate) with validation
- Updated services handlers (create, update) with validation
- Updated reviews handler (create) with validation
- Updated bookings handler (create) with validation
- Updated users handlers (updateMe, adminUpdateUser) with validation
- Updated tags handlers (createTag, deleteTag, createFeature, deleteFeature) with validation
- Standardized validation error responses (422 with field details)
neyrogovnarik 2 ay önce
ebeveyn
işleme
3632604e2e

+ 60 - 10
backend/internal/handlers/auth.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/services"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type AuthHandler struct {
@@ -23,10 +24,10 @@ func NewAuthHandler(authSvc *services.AuthService, appEnv string) *AuthHandler {
 }
 
 type registerRequest struct {
-	Email    string `json:"email"`
-	Password string `json:"password"`
-	Role     string `json:"role"`
-	Name     string `json:"name"`
+	Email    string `json:"email" validate:"required,email,max=255"`
+	Password string `json:"password" validate:"required,min=8,max=72"`
+	Role     string `json:"role" validate:"omitempty,user_role"`
+	Name     string `json:"name" validate:"omitempty,max=255"`
 }
 
 func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
@@ -36,13 +37,12 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if req.Email == "" || req.Password == "" {
-		writeError(w, http.StatusBadRequest, "email and password are required")
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 
-	validRoles := map[string]bool{"customer": true, "landlord": true, "executor": true}
-	if !validRoles[req.Role] {
+	if req.Role == "" {
 		req.Role = "customer"
 	}
 
@@ -66,8 +66,8 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
 }
 
 type loginRequest struct {
-	Email    string `json:"email"`
-	Password string `json:"password"`
+	Email    string `json:"email" validate:"required,email,max=255"`
+	Password string `json:"password" validate:"required,max=72"`
 }
 
 func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
@@ -77,6 +77,11 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	result, refreshToken, err := h.authSvc.Login(r.Context(), services.LoginInput{
 		Email:    req.Email,
 		Password: req.Password,
@@ -184,4 +189,49 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
 
 func writeError(w http.ResponseWriter, status int, msg string) {
 	writeJSON(w, status, map[string]string{"error": msg})
+}
+
+func writeValidationError(w http.ResponseWriter, err error) {
+	var ve validator.ValidationErrors
+	if errors.As(err, &ve) {
+		details := make(map[string]string)
+		for _, fe := range ve {
+			details[fe.Field()] = validationMessage(fe.Tag(), fe.Param())
+		}
+		writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
+			"error":   "validation failed",
+			"details": details,
+		})
+		return
+	}
+	writeError(w, http.StatusBadRequest, err.Error())
+}
+
+func validationMessage(tag, param string) string {
+	switch tag {
+	case "required":
+		return "field is required"
+	case "email":
+		return "invalid email format"
+	case "min":
+		return "value too short (min " + param + ")"
+	case "max":
+		return "value too long (max " + param + ")"
+	case "user_role":
+		return "invalid role (customer, landlord, executor, moderator, superadmin)"
+	case "latitude":
+		return "latitude must be between -90 and 90"
+	case "longitude":
+		return "longitude must be between -180 and 180"
+	case "place_type":
+		return "type must be 'place' or 'studio'"
+	case "currency":
+		return "currency must be 3-letter ISO code"
+	case "uuid":
+		return "invalid UUID format"
+	case "slug":
+		return "invalid slug format (lowercase, numbers, hyphens only)"
+	default:
+		return "validation failed: " + tag
+	}
 }

+ 10 - 12
backend/internal/handlers/bookings.go

@@ -9,6 +9,7 @@ import (
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/repository"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type BookingHandler struct {
@@ -21,10 +22,10 @@ func NewBookingHandler(bookingRepo *repository.BookingRepo, placeRepo *repositor
 }
 
 type createBookingRequest struct {
-	PlaceID   string `json:"place_id"`
-	StartTime string `json:"start_time"`
-	EndTime   string `json:"end_time"`
-	Comment   string `json:"comment"`
+	PlaceID   string  `json:"place_id" validate:"required,uuid"`
+	StartTime string  `json:"start_time" validate:"required,datetime=2006-01-02T15:04:05Z07:00"`
+	EndTime   string  `json:"end_time" validate:"required,datetime=2006-01-02T15:04:05Z07:00"`
+	Comment   *string `json:"comment" validate:"omitempty,max=1000"`
 }
 
 func (h *BookingHandler) Create(w http.ResponseWriter, r *http.Request) {
@@ -36,17 +37,14 @@ func (h *BookingHandler) Create(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	start, err := time.Parse(time.RFC3339, req.StartTime)
-	if err != nil {
-		writeError(w, http.StatusBadRequest, "invalid start_time format (use RFC3339)")
-		return
-	}
-	end, err := time.Parse(time.RFC3339, req.EndTime)
-	if err != nil {
-		writeError(w, http.StatusBadRequest, "invalid end_time format (use RFC3339)")
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 
+	start, _ := time.Parse(time.RFC3339, req.StartTime)
+	end, _ := time.Parse(time.RFC3339, req.EndTime)
+
 	if !end.After(start) {
 		writeError(w, http.StatusBadRequest, "end_time must be after start_time")
 		return

+ 53 - 30
backend/internal/handlers/places.go

@@ -10,6 +10,7 @@ import (
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/services"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type PlaceHandler struct {
@@ -102,19 +103,39 @@ func (h *PlaceHandler) GetByID(w http.ResponseWriter, r *http.Request) {
 }
 
 type createPlaceRequest struct {
-	Title       string            `json:"title"`
-	Description *string           `json:"description"`
-	Address     *string           `json:"address"`
-	Lat         float64           `json:"lat"`
-	Lng         float64           `json:"lng"`
-	Type        string            `json:"type"`
-	AccessInfo  *string           `json:"access_info"`
-	CoverImage  *string           `json:"cover_image"`
-	Tags        []string          `json:"tags"`
-	Features    []string          `json:"features"`
-	HourlyRate  *int              `json:"hourly_rate"`
-	Currency    string            `json:"currency"`
-	MinHours    int               `json:"min_hours"`
+	Title       string   `json:"title" validate:"required,min=1,max=255"`
+	Description *string  `json:"description" validate:"omitempty,max=5000"`
+	Address     *string  `json:"address" validate:"omitempty,max=500"`
+	Lat         float64  `json:"lat" validate:"required,latitude"`
+	Lng         float64  `json:"lng" validate:"required,longitude"`
+	Type        string   `json:"type" validate:"required,place_type"`
+	AccessInfo  *string  `json:"access_info" validate:"omitempty,max=2000"`
+	CoverImage  *string  `json:"cover_image" validate:"omitempty,url,max=500"`
+	Tags        []string `json:"tags" validate:"dive,slug,max=50"`
+	Features    []string `json:"features" validate:"dive,slug,max=50"`
+	HourlyRate  *int     `json:"hourly_rate" validate:"omitempty,min=0"`
+	Currency    string   `json:"currency" validate:"omitempty,currency,len=3"`
+	MinHours    int      `json:"min_hours" validate:"min=0,max=100"`
+}
+
+type updatePlaceRequest struct {
+	Title       *string  `json:"title" validate:"omitempty,min=1,max=255"`
+	Description *string  `json:"description" validate:"omitempty,max=5000"`
+	Address     *string  `json:"address" validate:"omitempty,max=500"`
+	Lat         *float64 `json:"lat" validate:"omitempty,latitude"`
+	Lng         *float64 `json:"lng" validate:"omitempty,longitude"`
+	AccessInfo  *string  `json:"access_info" validate:"omitempty,max=2000"`
+	CoverImage  *string  `json:"cover_image" validate:"omitempty,url,max=500"`
+	Tags        []string `json:"tags" validate:"dive,slug,max=50"`
+	Features    []string `json:"features" validate:"dive,slug,max=50"`
+	HourlyRate  *int     `json:"hourly_rate" validate:"omitempty,min=0"`
+	Currency    *string  `json:"currency" validate:"omitempty,currency,len=3"`
+	MinHours    *int     `json:"min_hours" validate:"omitempty,min=0,max=100"`
+}
+
+type moderatePlaceRequest struct {
+	Action  string  `json:"action" validate:"required,oneof=approve reject"`
+	Comment *string `json:"comment" validate:"omitempty,max=1000"`
 }
 
 func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
@@ -127,13 +148,8 @@ func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if req.Title == "" {
-		writeError(w, http.StatusBadRequest, "title is required")
-		return
-	}
-
-	if req.Type == "" {
-		writeError(w, http.StatusBadRequest, "type is required (place or studio)")
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 
@@ -184,12 +200,17 @@ func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
 	userID := middleware.GetUserID(r.Context())
 	role := middleware.GetUserRole(r.Context())
 
-	var req createPlaceRequest
+	var req updatePlaceRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	isModerator := role == "moderator" || role == "superadmin"
 
 	input := services.UpdatePlaceInput{
@@ -197,15 +218,15 @@ func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
 		OwnerID: userID,
 	}
 
-	if req.Title != "" { input.Title = &req.Title }
+	if req.Title != nil { input.Title = req.Title }
 	input.Description = req.Description
 	input.Address = req.Address
-	if req.Lat != 0 { input.Lat = &req.Lat }
-	if req.Lng != 0 { input.Lng = &req.Lng }
+	input.Lat = req.Lat
+	input.Lng = req.Lng
 	input.AccessInfo = req.AccessInfo
 	input.HourlyRate = req.HourlyRate
-	if req.Currency != "" { input.Currency = &req.Currency }
-	if req.MinHours != 0 { input.MinHours = &req.MinHours }
+	input.Currency = req.Currency
+	input.MinHours = req.MinHours
 
 	place, err := h.placeSvc.Update(r.Context(), input, isModerator)
 	if err != nil {
@@ -224,15 +245,17 @@ func (h *PlaceHandler) Moderate(w http.ResponseWriter, r *http.Request) {
 	id := chi.URLParam(r, "id")
 	userID := middleware.GetUserID(r.Context())
 
-	var req struct {
-		Action  string `json:"action"`
-		Comment string `json:"comment"`
-	}
+	var req moderatePlaceRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	if err := h.placeSvc.Moderate(r.Context(), id, req.Action, req.Comment, userID); err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return

+ 8 - 7
backend/internal/handlers/reviews.go

@@ -7,6 +7,7 @@ import (
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/repository"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type ReviewHandler struct {
@@ -40,10 +41,10 @@ func (h *ReviewHandler) List(w http.ResponseWriter, r *http.Request) {
 }
 
 type createReviewRequest struct {
-	TargetType string `json:"target_type"`
-	TargetID   string `json:"target_id"`
-	Rating     int    `json:"rating"`
-	Text       string `json:"text"`
+	TargetType string  `json:"target_type" validate:"required,oneof=place service"`
+	TargetID   string  `json:"target_id" validate:"required,uuid"`
+	Rating     int     `json:"rating" validate:"required,min=1,max=5"`
+	Text       *string `json:"text" validate:"omitempty,max=5000"`
 }
 
 func (h *ReviewHandler) Create(w http.ResponseWriter, r *http.Request) {
@@ -55,8 +56,8 @@ func (h *ReviewHandler) Create(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if req.TargetType == "" || req.TargetID == "" || req.Rating < 1 || req.Rating > 5 {
-		writeError(w, http.StatusBadRequest, "target_type, target_id and rating (1-5) are required")
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 
@@ -65,7 +66,7 @@ func (h *ReviewHandler) Create(w http.ResponseWriter, r *http.Request) {
 		TargetType: req.TargetType,
 		TargetID:   req.TargetID,
 		Rating:     req.Rating,
-		Text:       strPtr(req.Text),
+		Text:       req.Text,
 	}
 
 	if err := h.reviewRepo.Create(r.Context(), review); err != nil {

+ 27 - 12
backend/internal/handlers/services.go

@@ -9,6 +9,7 @@ import (
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/repository"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type ServiceHandler struct {
@@ -83,12 +84,21 @@ func (h *ServiceHandler) GetByID(w http.ResponseWriter, r *http.Request) {
 }
 
 type createServiceRequest struct {
-	Title           string   `json:"title"`
-	Description     *string  `json:"description"`
-	Price           int      `json:"price"`
-	Currency        string   `json:"currency"`
-	DurationMinutes *int     `json:"duration_minutes"`
-	Tags            []string `json:"tags"`
+	Title           string   `json:"title" validate:"required,min=1,max=255"`
+	Description     *string  `json:"description" validate:"omitempty,max=5000"`
+	Price           int      `json:"price" validate:"required,min=1"`
+	Currency        string   `json:"currency" validate:"required,currency,len=3"`
+	DurationMinutes *int     `json:"duration_minutes" validate:"omitempty,min=1,max=10080"`
+	Tags            []string `json:"tags" validate:"dive,slug,max=50"`
+}
+
+type updateServiceRequest struct {
+	Title           *string  `json:"title" validate:"omitempty,min=1,max=255"`
+	Description     *string  `json:"description" validate:"omitempty,max=5000"`
+	Price           *int     `json:"price" validate:"omitempty,min=1"`
+	Currency        *string  `json:"currency" validate:"omitempty,currency,len=3"`
+	DurationMinutes *int     `json:"duration_minutes" validate:"omitempty,min=1,max=10080"`
+	Tags            []string `json:"tags" validate:"dive,slug,max=50"`
 }
 
 func (h *ServiceHandler) Create(w http.ResponseWriter, r *http.Request) {
@@ -100,8 +110,8 @@ func (h *ServiceHandler) Create(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if req.Title == "" || req.Price <= 0 {
-		writeError(w, http.StatusBadRequest, "title and price are required")
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 
@@ -134,12 +144,17 @@ func (h *ServiceHandler) Update(w http.ResponseWriter, r *http.Request) {
 	userID := middleware.GetUserID(r.Context())
 	role := middleware.GetUserRole(r.Context())
 
-	var req createServiceRequest
+	var req updateServiceRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	svc, err := h.serviceRepo.GetByID(r.Context(), id)
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
@@ -155,10 +170,10 @@ func (h *ServiceHandler) Update(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if req.Title != "" { svc.Title = req.Title }
+	if req.Title != nil { svc.Title = *req.Title }
 	if req.Description != nil { svc.Description = req.Description }
-	if req.Price > 0 { svc.Price = req.Price }
-	if req.Currency != "" { svc.Currency = req.Currency }
+	if req.Price != nil { svc.Price = *req.Price }
+	if req.Currency != nil { svc.Currency = *req.Currency }
 	if req.DurationMinutes != nil { svc.DurationMinutes = req.DurationMinutes }
 
 	if err := h.serviceRepo.Update(r.Context(), svc); err != nil {

+ 42 - 16
backend/internal/handlers/tags.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/repository"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type TagHandler struct {
@@ -35,16 +36,36 @@ func (h *TagHandler) ListFeatures(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, features)
 }
 
+type createTagRequest struct {
+	ID       string `json:"id" validate:"required,slug,max=50"`
+	Name     string `json:"name" validate:"required,max=255"`
+	Category string `json:"category" validate:"omitempty,max=50"`
+	SortOrder int    `json:"sort_order" validate:"min=0"`
+}
+
+type createFeatureRequest struct {
+	ID       string `json:"id" validate:"required,slug,max=50"`
+	Name     string `json:"name" validate:"required,max=255"`
+	Category string `json:"category" validate:"omitempty,max=50"`
+	Icon     string `json:"icon" validate:"omitempty,max=50"`
+	SortOrder int    `json:"sort_order" validate:"min=0"`
+}
+
+type deleteTagRequest struct {
+	ID string `json:"id" validate:"required,slug,max=50"`
+}
+
 func (h *TagHandler) CreateTag(w http.ResponseWriter, r *http.Request) {
-	var req struct {
-		ID   string `json:"id"`
-		Name string `json:"name"`
-	}
+	var req createTagRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
-	tag := &models.Tag{ID: req.ID, Name: req.Name}
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+	tag := &models.Tag{ID: req.ID, Name: req.Name, Category: req.Category, SortOrder: req.SortOrder}
 	if err := h.tagRepo.Create(r.Context(), tag); err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return
@@ -53,12 +74,15 @@ func (h *TagHandler) CreateTag(w http.ResponseWriter, r *http.Request) {
 }
 
 func (h *TagHandler) DeleteTag(w http.ResponseWriter, r *http.Request) {
-	id := r.URL.Query().Get("id")
-	if id == "" {
-		writeError(w, http.StatusBadRequest, "id is required")
+	var req deleteTagRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		req.ID = r.URL.Query().Get("id")
+	}
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
-	if err := h.tagRepo.Delete(r.Context(), id); err != nil {
+	if err := h.tagRepo.Delete(r.Context(), req.ID); err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return
 	}
@@ -66,15 +90,16 @@ func (h *TagHandler) DeleteTag(w http.ResponseWriter, r *http.Request) {
 }
 
 func (h *TagHandler) CreateFeature(w http.ResponseWriter, r *http.Request) {
-	var req struct {
-		ID   string `json:"id"`
-		Name string `json:"name"`
-	}
+	var req createFeatureRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
-	feature := &models.Feature{ID: req.ID, Name: req.Name}
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+	feature := &models.Feature{ID: req.ID, Name: req.Name, Category: req.Category, Icon: req.Icon, SortOrder: req.SortOrder}
 	if err := h.featureRepo.Create(r.Context(), feature); err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return
@@ -84,8 +109,9 @@ func (h *TagHandler) CreateFeature(w http.ResponseWriter, r *http.Request) {
 
 func (h *TagHandler) DeleteFeature(w http.ResponseWriter, r *http.Request) {
 	id := r.URL.Query().Get("id")
-	if id == "" {
-		writeError(w, http.StatusBadRequest, "id is required")
+	req := deleteTagRequest{ID: id}
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
 		return
 	}
 	if err := h.featureRepo.Delete(r.Context(), id); err != nil {

+ 22 - 19
backend/internal/handlers/users.go

@@ -9,6 +9,7 @@ import (
 	"github.com/photoplaces/backend/internal/middleware"
 	"github.com/photoplaces/backend/internal/models"
 	"github.com/photoplaces/backend/internal/repository"
+	"github.com/photoplaces/backend/internal/validator"
 )
 
 type UserHandler struct {
@@ -38,6 +39,14 @@ func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, user)
 }
 
+type updateMeRequest struct {
+	Name      *string `json:"name" validate:"omitempty,max=255"`
+	Phone     *string `json:"phone" validate:"omitempty,max=20"`
+	Bio       *string `json:"bio" validate:"omitempty,max=2000"`
+	AvatarURL *string `json:"avatar_url" validate:"omitempty,url,max=500"`
+	Country   *string `json:"country" validate:"omitempty,len=2"`
+}
+
 func (h *UserHandler) UpdateMe(w http.ResponseWriter, r *http.Request) {
 	userID := middleware.GetUserID(r.Context())
 
@@ -47,18 +56,17 @@ func (h *UserHandler) UpdateMe(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	var req struct {
-		Name      *string `json:"name"`
-		Phone     *string `json:"phone"`
-		Bio       *string `json:"bio"`
-		AvatarURL *string `json:"avatar_url"`
-		Country   *string `json:"country"`
-	}
+	var req updateMeRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	if req.Name != nil { user.Name = req.Name }
 	if req.Phone != nil { user.Phone = req.Phone }
 	if req.Bio != nil { user.Bio = req.Bio }
@@ -76,8 +84,8 @@ func (h *UserHandler) UpdateMe(w http.ResponseWriter, r *http.Request) {
 // Admin endpoints
 
 type adminUpdateUserRequest struct {
-	Role   *string `json:"role"`
-	Status *string `json:"status"`
+	Role   *string `json:"role" validate:"omitempty,user_role"`
+	Status *string `json:"status" validate:"omitempty,user_status"`
 }
 
 func (h *UserHandler) AdminUpdateUser(w http.ResponseWriter, r *http.Request) {
@@ -89,12 +97,12 @@ func (h *UserHandler) AdminUpdateUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if err := validator.Validate(req); err != nil {
+		writeValidationError(w, err)
+		return
+	}
+
 	if req.Role != nil {
-		validRoles := map[string]bool{"superadmin": true, "moderator": true, "landlord": true, "executor": true, "customer": true}
-		if !validRoles[*req.Role] {
-			writeError(w, http.StatusBadRequest, "invalid role")
-			return
-		}
 		if err := h.userRepo.UpdateRole(r.Context(), id, *req.Role); err != nil {
 			writeError(w, http.StatusInternalServerError, err.Error())
 			return
@@ -102,11 +110,6 @@ func (h *UserHandler) AdminUpdateUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if req.Status != nil {
-		validStatuses := map[string]bool{"active": true, "banned": true, "pending_verification": true}
-		if !validStatuses[*req.Status] {
-			writeError(w, http.StatusBadRequest, "invalid status")
-			return
-		}
 		if err := h.userRepo.UpdateStatus(r.Context(), id, *req.Status); err != nil {
 			writeError(w, http.StatusInternalServerError, err.Error())
 			return

+ 9 - 2
backend/internal/validator/validator.go

@@ -2,14 +2,15 @@ package validator
 
 import (
 	"regexp"
+	"time"
 
 	"github.com/go-playground/validator/v10"
 )
 
 var (
 	validate *validator.Validate
-	uuidRegex    = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
-	slugRegex    = regexp.MustCompile(`^[a-z0-9-]+$`)
+	uuidRegex     = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
+	slugRegex     = regexp.MustCompile(`^[a-z0-9-]+$`)
 	currencyRegex = regexp.MustCompile(`^[A-Z]{3}$`)
 )
 
@@ -28,6 +29,7 @@ func init() {
 	_ = validate.RegisterValidation("place_status", validatePlaceStatus)
 	_ = validate.RegisterValidation("service_status", validateServiceStatus)
 	_ = validate.RegisterValidation("booking_status", validateBookingStatus)
+	_ = validate.RegisterValidation("datetime", validateDateTime)
 }
 
 // Validate validates a struct and returns ValidationErrors if any
@@ -114,4 +116,9 @@ func validateServiceStatus(fl validator.FieldLevel) bool {
 func validateBookingStatus(fl validator.FieldLevel) bool {
 	statuses := map[string]bool{"pending": true, "confirmed": true, "cancelled": true, "completed": true}
 	return statuses[fl.Field().String()]
+}
+
+func validateDateTime(fl validator.FieldLevel) bool {
+	_, err := time.Parse(time.RFC3339, fl.Field().String())
+	return err == nil
 }