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
This commit is contained in:
parent
8309f38999
commit
0e83ee056c
@ -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
|
||||
}
|
||||
|
||||
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
|
||||
// Will be implemented in task 6
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
// 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) {
|
||||
// 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",
|
||||
})
|
||||
}
|
||||
|
||||
174
internal/services/user_service.go
Normal file
174
internal/services/user_service.go
Normal file
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user