Add JWT authentication
- Create register and login use cases - Implement JWT service for token generation and validation - Add authentication middleware to protect habit endpoints - Create auth HTTP handlers (register, login) - Update habit handlers to extract userID from JWT token - Register/login endpoints: POST /auth/register, POST /auth/login - Habit endpoints now require Bearer token in Authorization header - Tested: register -> create habit -> list habits works correctly
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"habit-tracker-api/internal/domain/entities"
|
||||
"habit-tracker-api/internal/domain/repositories"
|
||||
"habit-tracker-api/internal/shared/errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type RegisterUserCommand struct {
|
||||
Email string
|
||||
Password string
|
||||
Timezone string
|
||||
}
|
||||
|
||||
type RegisterUserHandler struct {
|
||||
userRepo repositories.UserRepository
|
||||
}
|
||||
|
||||
func NewRegisterUserHandler(userRepo repositories.UserRepository) *RegisterUserHandler {
|
||||
return &RegisterUserHandler{userRepo: userRepo}
|
||||
}
|
||||
|
||||
func (h *RegisterUserHandler) Handle(ctx context.Context, cmd RegisterUserCommand) (string, error) {
|
||||
if cmd.Email == "" || cmd.Password == "" {
|
||||
return "", errors.ErrInvalidInput
|
||||
}
|
||||
|
||||
if len(cmd.Password) < 8 {
|
||||
return "", errors.ErrInvalidInput
|
||||
}
|
||||
|
||||
existing, _ := h.userRepo.FindByEmail(ctx, cmd.Email)
|
||||
if existing != nil {
|
||||
return "", errors.ErrAlreadyExists
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cmd.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
timezone := cmd.Timezone
|
||||
if timezone == "" {
|
||||
timezone = "UTC"
|
||||
}
|
||||
|
||||
user := entities.NewUser(cmd.Email, string(hashedPassword), timezone)
|
||||
|
||||
if err := h.userRepo.Create(ctx, user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return user.ID, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"habit-tracker-api/internal/domain/repositories"
|
||||
"habit-tracker-api/internal/shared/errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type LoginUserQuery struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
type LoginUserResult struct {
|
||||
UserID string
|
||||
Email string
|
||||
Timezone string
|
||||
}
|
||||
|
||||
type LoginUserHandler struct {
|
||||
userRepo repositories.UserRepository
|
||||
}
|
||||
|
||||
func NewLoginUserHandler(userRepo repositories.UserRepository) *LoginUserHandler {
|
||||
return &LoginUserHandler{userRepo: userRepo}
|
||||
}
|
||||
|
||||
func (h *LoginUserHandler) Handle(ctx context.Context, query LoginUserQuery) (*LoginUserResult, error) {
|
||||
if query.Email == "" || query.Password == "" {
|
||||
return nil, errors.ErrInvalidInput
|
||||
}
|
||||
|
||||
user, err := h.userRepo.FindByEmail(ctx, query.Email)
|
||||
if err != nil {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(query.Password)); err != nil {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
return &LoginUserResult{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Timezone: user.Timezone,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user