package api import ( "encoding/json" "errors" "fmt" "net/http" "dance-lessons-coach/pkg/user" "dance-lessons-coach/pkg/validation" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" ) // AuthHandler handles authentication-related HTTP requests 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, validator *validation.Validator) *AuthHandler { return &AuthHandler{ authService: authService, userService: userService, validator: validator, } } // RegisterRoutes registers authentication routes func (h *AuthHandler) RegisterRoutes(router chi.Router) { router.Post("/login", h.handleLogin) router.Post("/register", h.handleRegister) router.Post("/password-reset/request", h.handlePasswordResetRequest) router.Post("/password-reset/complete", h.handlePasswordResetComplete) router.Post("/validate", h.handleValidateToken) } // 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"` Password string `json:"password" validate:"required,min=6"` } // LoginResponse represents a login response type LoginResponse struct { Token string `json:"token"` } // handleLogin godoc // // @Summary User login // @Description Authenticate user or admin and return JWT token. Supports both regular users and admin authentication. // @Tags API/v1/User // @Accept json // @Produce json // @Param request body LoginRequest true "Login credentials" // @Success 200 {object} LoginResponse "Successful authentication" // @Failure 400 {object} map[string]string "Invalid request" // @Failure 401 {object} map[string]string "Invalid credentials" // @Failure 500 {object} map[string]string "Server error" // @Router /v1/auth/login [post] func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest) return } // Validate request using validator if h.validator != nil { if err := h.validator.Validate(req); err != nil { h.writeValidationError(w, err) return } } // Try unified authentication (regular user first, then admin fallback) var authenticatedUser *user.User var authError error // Try regular user authentication first authenticatedUser, authError = h.authService.Authenticate(ctx, req.Username, req.Password) // If regular auth fails, try admin authentication if authError != nil { authenticatedUser, authError = h.authService.AdminAuthenticate(ctx, req.Password) } // If both authentication methods failed if authError != nil { log.Trace().Ctx(ctx).Err(authError).Str("username", req.Username).Msg("Authentication failed") http.Error(w, `{"error":"invalid_credentials","message":"Invalid username or password"}`, http.StatusUnauthorized) return } // Generate JWT token using the authenticated user (regular or admin) token, err := h.authService.GenerateJWT(ctx, authenticatedUser) if err != nil { log.Error().Ctx(ctx).Err(err).Msg("Failed to generate JWT token") http.Error(w, `{"error":"server_error","message":"Failed to generate authentication token"}`, http.StatusInternalServerError) return } // Return token w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(LoginResponse{Token: token}) } // 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,max=100"` } // handleRegister godoc // // @Summary User registration // @Description Register a new user account // @Tags API/v1/User // @Accept json // @Produce json // @Param request body RegisterRequest true "Registration details" // @Success 201 {object} map[string]string "User created" // @Failure 400 {object} map[string]string "Invalid request" // @Failure 409 {object} map[string]string "Username already taken" // @Failure 500 {object} map[string]string "Server error" // @Router /v1/auth/register [post] func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest) return } // Validate request using validator if h.validator != nil { if err := h.validator.Validate(req); err != nil { h.writeValidationError(w, err) return } } // Check if user already exists exists, err := h.userService.UserExists(ctx, req.Username) if err != nil { log.Error().Ctx(ctx).Err(err).Msg("Failed to check if user exists") http.Error(w, `{"error":"server_error","message":"Failed to process registration"}`, http.StatusInternalServerError) return } if exists { http.Error(w, `{"error":"user_exists","message":"Username already taken"}`, http.StatusConflict) return } // Hash password hashedPassword, err := h.userService.HashPassword(ctx, req.Password) if err != nil { log.Error().Ctx(ctx).Err(err).Msg("Failed to hash password") http.Error(w, `{"error":"server_error","message":"Failed to process registration"}`, http.StatusInternalServerError) return } // Create user newUser := &user.User{ Username: req.Username, PasswordHash: hashedPassword, IsAdmin: false, } if err := h.userService.CreateUser(ctx, newUser); err != nil { log.Error().Ctx(ctx).Err(err).Msg("Failed to create user") http.Error(w, `{"error":"server_error","message":"Failed to create user"}`, http.StatusInternalServerError) return } // Return success w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"}) } // PasswordResetRequest represents a password reset request type PasswordResetRequest struct { Username string `json:"username" validate:"required,min=3,max=50"` } // handlePasswordResetRequest godoc // // @Summary Request password reset // @Description Initiate password reset process for a user // @Tags API/v1/User // @Accept json // @Produce json // @Param request body PasswordResetRequest true "Password reset request" // @Success 200 {object} map[string]string "Reset allowed" // @Failure 400 {object} map[string]string "Invalid request" // @Failure 500 {object} map[string]string "Server error" // @Router /v1/auth/password-reset/request [post] func (h *AuthHandler) handlePasswordResetRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req PasswordResetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest) 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") http.Error(w, `{"error":"server_error","message":"Failed to process password reset request"}`, http.StatusInternalServerError) return } // Return success w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Password reset allowed, user can now reset password"}) } // 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,max=100"` } // handlePasswordResetComplete godoc // // @Summary Complete password reset // @Description Complete password reset with new password // @Tags API/v1/User // @Accept json // @Produce json // @Param request body PasswordResetCompleteRequest true "Password reset completion" // @Success 200 {object} map[string]string "Password updated" // @Failure 400 {object} map[string]string "Invalid request" // @Failure 500 {object} map[string]string "Server error" // @Router /v1/auth/password-reset/complete [post] func (h *AuthHandler) handlePasswordResetComplete(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req PasswordResetCompleteRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest) 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") http.Error(w, `{"error":"server_error","message":"Failed to complete password reset"}`, http.StatusInternalServerError) return } // Return success w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Password reset completed successfully"}) } // TokenValidationRequest represents a JWT token validation request // This is used for testing JWT validation with different token scenarios type TokenValidationRequest struct { Token string `json:"token" validate:"required"` } // handleValidateToken godoc // // @Summary Validate JWT token // @Description Validate a JWT token and return user information if valid // @Tags API/v1/User // @Accept json // @Produce json // @Param request body TokenValidationRequest true "Token validation request" // @Success 200 {object} map[string]interface{} "Token is valid with user info" // @Failure 400 {object} map[string]string "Invalid request" // @Failure 401 {object} map[string]string "Invalid token" // @Router /v1/auth/validate [post] func (h *AuthHandler) handleValidateToken(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req TokenValidationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest) return } // Validate request using validator if h.validator != nil { if err := h.validator.Validate(req); err != nil { h.writeValidationError(w, err) return } } // Validate the JWT token user, err := h.authService.ValidateJWT(ctx, req.Token) if err != nil { log.Trace().Ctx(ctx).Err(err).Msg("JWT validation failed in validate endpoint") http.Error(w, fmt.Sprintf(`{"error":"invalid_token","message":"%s"}`, err.Error()), http.StatusUnauthorized) return } // Return success with user info w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "valid": true, "user_id": user.ID, "message": "Token is valid", }) }