From 0e83ee056c4c9a1bf959afc480afd724c456b3f0 Mon Sep 17 00:00:00 2001 From: ats-tech25 Date: Wed, 5 Nov 2025 15:25:41 +0000 Subject: [PATCH] feat(auth): Implement comprehensive user authentication and authorization - Add complete authentication handlers for registration, login, profile retrieval, and update - Implement UserService with robust validation and error handling - Create new user_service.go with core authentication logic - Add support for JWT token generation and user management - Implement input validation, password hashing, and error responses - Enhance security with email normalization and input sanitization - Prepare groundwork for secure user authentication flow --- internal/handlers/auth.go | 176 ++++++++++++++++++++++++++++-- internal/services/user_service.go | 174 +++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 internal/services/user_service.go diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index f9a7668..8260b9b 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -1,33 +1,187 @@ package handlers import ( + "net/http" + + "attune-heart-therapy/internal/middleware" + "attune-heart-therapy/internal/services" + "github.com/gin-gonic/gin" ) type AuthHandler struct { - // Will be implemented in later tasks + userService services.UserService } -func NewAuthHandler() *AuthHandler { - return &AuthHandler{} +func NewAuthHandler(userService services.UserService) *AuthHandler { + return &AuthHandler{ + userService: userService, + } } +// Register handles POST /api/auth/register for new user registration func (h *AuthHandler) Register(c *gin.Context) { - // Will be implemented in task 6 - c.JSON(501, gin.H{"message": "Not implemented yet"}) + var req services.RegisterRequest + + // Bind JSON request to struct + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request format", + "details": err.Error(), + }) + return + } + + // Create new user + user, err := h.userService.Register(req) + if err != nil { + // Check for specific error types + if err.Error() == "validation failed" || + err.Error() == "password must be at least 8 characters long" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Validation failed", + "details": err.Error(), + }) + return + } + + if err.Error() == "user with email "+req.Email+" already exists" { + c.JSON(http.StatusConflict, gin.H{ + "error": "User already exists", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create user", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "User registered successfully", + "user": user, + }) } +// Login handles POST /api/auth/login for user authentication func (h *AuthHandler) Login(c *gin.Context) { - // Will be implemented in task 6 - c.JSON(501, gin.H{"message": "Not implemented yet"}) + var loginReq struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` + } + + // Bind JSON request to struct + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request format", + "details": err.Error(), + }) + return + } + + // Authenticate user + user, token, err := h.userService.Login(loginReq.Email, loginReq.Password) + if err != nil { + if err.Error() == "invalid credentials" { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid email or password", + }) + return + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Login failed", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Login successful", + "user": user, + "token": token, + }) } +// GetProfile handles GET /api/auth/profile for retrieving user profile func (h *AuthHandler) GetProfile(c *gin.Context) { - // Will be implemented in task 6 - c.JSON(501, gin.H{"message": "Not implemented yet"}) + // Get user ID from JWT token (set by auth middleware) + userID, exists := middleware.GetUserIDFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "User not authenticated", + }) + return + } + + // Get user profile + user, err := h.userService.GetProfile(userID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "User not found", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "user": user, + }) } +// UpdateProfile handles PUT /api/auth/profile for updating user profile func (h *AuthHandler) UpdateProfile(c *gin.Context) { - // Will be implemented in task 6 - c.JSON(501, gin.H{"message": "Not implemented yet"}) + // Get user ID from JWT token (set by auth middleware) + userID, exists := middleware.GetUserIDFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "User not authenticated", + }) + return + } + + var req services.UpdateProfileRequest + + // Bind JSON request to struct + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request format", + "details": err.Error(), + }) + return + } + + // Update user profile + user, err := h.userService.UpdateProfile(userID, req) + if err != nil { + if err.Error() == "invalid user ID" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid user ID", + }) + return + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update profile", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Profile updated successfully", + "user": user, + }) +} + +// Logout handles POST /api/auth/logout for user logout +// Note: Since we're using stateless JWT tokens, logout is handled client-side +// by removing the token. This endpoint is provided for consistency. +func (h *AuthHandler) Logout(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "Logout successful", + }) } diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..1431901 --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,174 @@ +package services + +import ( + "errors" + "fmt" + "strings" + + "attune-heart-therapy/internal/models" + "attune-heart-therapy/internal/repositories" + + "github.com/go-playground/validator/v10" +) + +// userService implements the UserService interface +type userService struct { + userRepo repositories.UserRepository + jwtService JWTService + validator *validator.Validate +} + +// NewUserService creates a new instance of UserService +func NewUserService(userRepo repositories.UserRepository, jwtService JWTService) UserService { + return &userService{ + userRepo: userRepo, + jwtService: jwtService, + validator: validator.New(), + } +} + +// Register creates a new user account with password hashing and validation +func (s *userService) Register(req RegisterRequest) (*models.User, error) { + // Validate the request + if err := s.validator.Struct(req); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Additional password validation + if len(req.Password) < 8 { + return nil, errors.New("password must be at least 8 characters long") + } + + // Normalize email + req.Email = strings.ToLower(strings.TrimSpace(req.Email)) + + // Check if user already exists + existingUser, err := s.userRepo.GetByEmail(req.Email) + if err == nil && existingUser != nil { + return nil, fmt.Errorf("user with email %s already exists", req.Email) + } + + // Create new user + user := &models.User{ + FirstName: strings.TrimSpace(req.FirstName), + LastName: strings.TrimSpace(req.LastName), + Email: req.Email, + Phone: strings.TrimSpace(req.Phone), + Location: strings.TrimSpace(req.Location), + DateOfBirth: req.DateOfBirth, + IsAdmin: false, // New users are not admin by default + } + + // Hash the password + if err := user.HashPassword(req.Password); err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + // Save user to database + if err := s.userRepo.Create(user); err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + // Clear password hash from response for security + user.PasswordHash = "" + + return user, nil +} + +// Login authenticates a user and returns user info with JWT token +func (s *userService) Login(email, password string) (*models.User, string, error) { + // Validate input + if email == "" { + return nil, "", errors.New("email is required") + } + if password == "" { + return nil, "", errors.New("password is required") + } + + // Normalize email + email = strings.ToLower(strings.TrimSpace(email)) + + // Get user by email + user, err := s.userRepo.GetByEmail(email) + if err != nil { + return nil, "", errors.New("invalid credentials") + } + + // Check password + if !user.CheckPassword(password) { + return nil, "", errors.New("invalid credentials") + } + + // Generate JWT token + token, err := s.jwtService.GenerateToken(user.ID, user.Email, user.IsAdmin) + if err != nil { + return nil, "", fmt.Errorf("failed to generate token: %w", err) + } + + // Clear password hash from response for security + user.PasswordHash = "" + + return user, token, nil +} + +// GetProfile retrieves user profile information by user ID +func (s *userService) GetProfile(userID uint) (*models.User, error) { + if userID == 0 { + return nil, errors.New("invalid user ID") + } + + user, err := s.userRepo.GetByID(userID) + if err != nil { + return nil, fmt.Errorf("failed to get user profile: %w", err) + } + + // Clear password hash from response for security + user.PasswordHash = "" + + return user, nil +} + +// UpdateProfile updates user profile information +func (s *userService) UpdateProfile(userID uint, req UpdateProfileRequest) (*models.User, error) { + if userID == 0 { + return nil, errors.New("invalid user ID") + } + + // Validate the request + if err := s.validator.Struct(req); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Get existing user + user, err := s.userRepo.GetByID(userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + // Update fields if provided + if req.FirstName != "" { + user.FirstName = strings.TrimSpace(req.FirstName) + } + if req.LastName != "" { + user.LastName = strings.TrimSpace(req.LastName) + } + if req.Phone != "" { + user.Phone = strings.TrimSpace(req.Phone) + } + if req.Location != "" { + user.Location = strings.TrimSpace(req.Location) + } + if !req.DateOfBirth.IsZero() { + user.DateOfBirth = req.DateOfBirth + } + + // Update user in database + if err := s.userRepo.Update(user); err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + + // Clear password hash from response for security + user.PasswordHash = "" + + return user, nil +}