📝 docs: restore ADR-0011 validation library selection
- Restored ADR-0011 with updated implementation details - Documented go-playground/validator adoption and integration strategy - Added technical implementation examples and migration path - Updated status to 'Adopted' reflecting current usage 🔧 refactor: integrate authentication handlers with validation system - Added validation tags to all authentication request DTOs: - LoginRequest: username (3-50 chars), password (6+ chars) - RegisterRequest: username (3-50 chars), password (6-100 chars) - PasswordResetRequest: username (3-50 chars) - PasswordResetCompleteRequest: username (3-50 chars), new_password (6-100 chars) - Updated AuthHandler to accept validator parameter - Replaced manual validation with structured validator.Validate() calls - Added writeValidationError() helper for consistent error responses - Updated server to inject validator into authentication handler - Improved error messages with field-level validation details 🧪 test: update BDD tests for new validation error format - Updated authentication validation tests to expect structured errors - All 25 BDD scenarios passing with improved validation coverage - Maintained backward compatibility for error handling Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -401,10 +401,10 @@ func (sc *StepContext) theAuthenticationShouldFailWithValidationError() error {
|
||||
return fmt.Errorf("expected status 400, got %d", sc.client.GetLastStatusCode())
|
||||
}
|
||||
|
||||
// Check if response contains validation error
|
||||
// Check if response contains validation error (new structured format)
|
||||
body := string(sc.client.GetLastBody())
|
||||
if !strings.Contains(body, "invalid_request") {
|
||||
return fmt.Errorf("expected response to contain invalid_request error, got %s", body)
|
||||
if !strings.Contains(body, "validation_failed") && !strings.Contains(body, "invalid_request") {
|
||||
return fmt.Errorf("expected response to contain validation_failed or invalid_request error, got %s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -164,7 +164,7 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
if s.userService != nil && s.userRepo != nil {
|
||||
// Use unified user service - much simpler!
|
||||
if s.userService != nil {
|
||||
handler := userapi.NewAuthHandler(s.userService, s.userService)
|
||||
handler := userapi.NewAuthHandler(s.userService, s.userService, s.validator)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
handler.RegisterRoutes(r)
|
||||
})
|
||||
|
||||
@@ -2,9 +2,11 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"dance-lessons-coach/pkg/user"
|
||||
"dance-lessons-coach/pkg/validation"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -14,13 +16,15 @@ import (
|
||||
type AuthHandler struct {
|
||||
authService user.AuthService
|
||||
userService user.UserService
|
||||
validator *validation.Validator
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new authentication handler
|
||||
func NewAuthHandler(authService user.AuthService, userService user.UserService) *AuthHandler {
|
||||
func NewAuthHandler(authService user.AuthService, userService user.UserService, validator *validation.Validator) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
userService: userService,
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +37,29 @@ func (h *AuthHandler) RegisterRoutes(router chi.Router) {
|
||||
router.Post("/password-reset/complete", h.handlePasswordResetComplete)
|
||||
}
|
||||
|
||||
// writeValidationError writes a structured validation error response
|
||||
func (h *AuthHandler) writeValidationError(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
// The validator returns a ValidationError that we can use directly
|
||||
var validationErr *validation.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "validation_failed",
|
||||
"message": "Invalid request data",
|
||||
"details": validationErr.Messages,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback for other error types
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "validation_failed",
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// LoginRequest represents a login request
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
@@ -45,6 +72,7 @@ type LoginResponse struct {
|
||||
}
|
||||
|
||||
// handleLogin godoc
|
||||
//
|
||||
// @Summary User login
|
||||
// @Description Authenticate user and return JWT token
|
||||
// @Tags API/v1/User
|
||||
@@ -65,11 +93,13 @@ func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate username and password are not empty
|
||||
if req.Username == "" || req.Password == "" {
|
||||
http.Error(w, `{"error":"invalid_request","message":"Username and password are required"}`, http.StatusBadRequest)
|
||||
// Validate request using validator
|
||||
if h.validator != nil {
|
||||
if err := h.validator.Validate(req); err != nil {
|
||||
h.writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
user, err := h.authService.Authenticate(ctx, req.Username, req.Password)
|
||||
@@ -96,10 +126,11 @@ func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// RegisterRequest represents a user registration request
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
Password string `json:"password" validate:"required,min=6,max=100"`
|
||||
}
|
||||
|
||||
// handleRegister godoc
|
||||
//
|
||||
// @Summary User registration
|
||||
// @Description Register a new user account
|
||||
// @Tags API/v1/User
|
||||
@@ -120,16 +151,12 @@ func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate username length (min 3, max 50 characters)
|
||||
if len(req.Username) < 3 || len(req.Username) > 50 {
|
||||
http.Error(w, `{"error":"invalid_username","message":"Username must be between 3 and 50 characters"}`, http.StatusBadRequest)
|
||||
// Validate request using validator
|
||||
if h.validator != nil {
|
||||
if err := h.validator.Validate(req); err != nil {
|
||||
h.writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate password length (min 6 characters)
|
||||
if len(req.Password) < 6 {
|
||||
http.Error(w, `{"error":"invalid_password","message":"Password must be at least 6 characters"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
@@ -177,6 +204,7 @@ type PasswordResetRequest struct {
|
||||
}
|
||||
|
||||
// handlePasswordResetRequest godoc
|
||||
//
|
||||
// @Summary Request password reset
|
||||
// @Description Initiate password reset process for a user
|
||||
// @Tags API/v1/User
|
||||
@@ -196,6 +224,14 @@ func (h *AuthHandler) handlePasswordResetRequest(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request using validator
|
||||
if h.validator != nil {
|
||||
if err := h.validator.Validate(req); err != nil {
|
||||
h.writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Request password reset
|
||||
if err := h.userService.RequestPasswordReset(ctx, req.Username); err != nil {
|
||||
log.Error().Ctx(ctx).Err(err).Msg("Failed to request password reset")
|
||||
@@ -212,10 +248,11 @@ func (h *AuthHandler) handlePasswordResetRequest(w http.ResponseWriter, r *http.
|
||||
// PasswordResetCompleteRequest represents a password reset completion request
|
||||
type PasswordResetCompleteRequest struct {
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=6,max=100"`
|
||||
}
|
||||
|
||||
// handlePasswordResetComplete godoc
|
||||
//
|
||||
// @Summary Complete password reset
|
||||
// @Description Complete password reset with new password
|
||||
// @Tags API/v1/User
|
||||
@@ -235,6 +272,14 @@ func (h *AuthHandler) handlePasswordResetComplete(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request using validator
|
||||
if h.validator != nil {
|
||||
if err := h.validator.Validate(req); err != nil {
|
||||
h.writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Complete password reset
|
||||
if err := h.userService.CompletePasswordReset(ctx, req.Username, req.NewPassword); err != nil {
|
||||
log.Error().Ctx(ctx).Err(err).Msg("Failed to complete password reset")
|
||||
@@ -249,6 +294,7 @@ func (h *AuthHandler) handlePasswordResetComplete(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// handleAdminLogin godoc
|
||||
//
|
||||
// @Summary Admin login
|
||||
// @Description Authenticate admin user with master password
|
||||
// @Tags Admin/User
|
||||
|
||||
Reference in New Issue
Block a user