feat: change user password
This commit is contained in:
@@ -11,10 +11,11 @@ import (
|
||||
|
||||
type UserHandler struct {
|
||||
UserService *domain.UserService
|
||||
AuthService *domain.AuthService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService *domain.UserService) *UserHandler {
|
||||
return &UserHandler{UserService: userService}
|
||||
func NewUserHandler(userService *domain.UserService, authService *domain.AuthService) *UserHandler {
|
||||
return &UserHandler{UserService: userService, AuthService: authService}
|
||||
}
|
||||
|
||||
// CreateUser handles the creation of a user
|
||||
@@ -49,3 +50,49 @@ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
slog.InfoContext(ctx, "created user successfully", slog.Any("user_id", user.ID))
|
||||
response.JSON(ctx, w, http.StatusCreated, user)
|
||||
}
|
||||
|
||||
// ChangePassword handles changing a users password
|
||||
//
|
||||
// @Summary Change password route
|
||||
// @Description Change password for the authenticated user
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body request.ChangePasswordPayload true "Change password payload"
|
||||
// @Success 204
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 500 {object} response.ErrorResponse "could not get auth context"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to change password"
|
||||
// @Router /users/password [post]
|
||||
func (h *UserHandler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
payload, err := request.DecodeJSON[request.ChangePasswordPayload](r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to decode change password payload", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request body")
|
||||
return
|
||||
}
|
||||
|
||||
authContext, err := domain.GetAuthContext(ctx)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "could not get auth context")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.UserService.ChangePassword(ctx, authContext.UserID, payload.Password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to change password", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to change password")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.AuthService.LogoutAllDevices(ctx, authContext.UserID)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "failed to logout user from all devices after password change", slog.Any("error", err))
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "password changed successfully", slog.Any("user_id", authContext.UserID))
|
||||
response.Status(ctx, w, http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ type CreateUserPayload struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ChangePasswordPayload struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ type Config struct {
|
||||
}
|
||||
|
||||
func NewMux(cfg Config) http.Handler {
|
||||
userRepo := repository.NewUserRepo(cfg.Database)
|
||||
userService := domain.NewUserService(userRepo)
|
||||
userHandler := handler.NewUserHandler(userService)
|
||||
|
||||
authRepo := repository.NewAuthRepo(cfg.Database)
|
||||
authService := domain.NewAuthService(authRepo, []byte(cfg.JWTSecret))
|
||||
authHandler := handler.NewAuthHandler(authService)
|
||||
|
||||
userRepo := repository.NewUserRepo(cfg.Database)
|
||||
userService := domain.NewUserService(userRepo)
|
||||
userHandler := handler.NewUserHandler(userService, authService)
|
||||
|
||||
listRepo := repository.NewListRepo(cfg.Database)
|
||||
listService := domain.NewListService(listRepo)
|
||||
listHandler := handler.NewListHandler(listService, userService)
|
||||
@@ -40,6 +40,7 @@ func NewMux(cfg Config) http.Handler {
|
||||
apiMux.HandleFunc("POST /logout/all", authHandler.LogoutAllDevices)
|
||||
|
||||
apiMux.Handle("POST /users", protected(userHandler.CreateUser))
|
||||
apiMux.Handle("POST /users/password", protected(userHandler.ChangePassword))
|
||||
|
||||
apiMux.Handle("POST /lists", protected(listHandler.CreateList))
|
||||
apiMux.Handle("DELETE /lists/{id}", protected(listHandler.DeleteList))
|
||||
|
||||
+27
-3
@@ -8,6 +8,13 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserIDMissing = errors.New("user id is missing")
|
||||
ErrEmailMissing = errors.New("email is required")
|
||||
ErrNameMissing = errors.New("name is required")
|
||||
ErrPasswordMissing = errors.New("password is required")
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -17,6 +24,7 @@ type User struct {
|
||||
|
||||
type UserRepository interface {
|
||||
CreateUser(ctx context.Context, email string, name string, passwordHash string) (*User, error)
|
||||
ChangePassword(ctx context.Context, userID string, passwordHash string) error
|
||||
GetUserByEmail(ctx context.Context, email string) (*User, error)
|
||||
}
|
||||
|
||||
@@ -30,13 +38,13 @@ func NewUserService(r UserRepository) *UserService {
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, email string, name string, password string) (*User, error) {
|
||||
if len(email) == 0 {
|
||||
return nil, errors.New("email is required")
|
||||
return nil, ErrEmailMissing
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return nil, errors.New("name is required")
|
||||
return nil, ErrNameMissing
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("password is required")
|
||||
return nil, ErrPasswordMissing
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
@@ -47,6 +55,22 @@ func (s *UserService) CreateUser(ctx context.Context, email string, name string,
|
||||
return s.repo.CreateUser(ctx, email, name, string(hash))
|
||||
}
|
||||
|
||||
func (s *UserService) ChangePassword(ctx context.Context, userID string, password string) error {
|
||||
if len(userID) == 0 {
|
||||
return ErrUserIDMissing
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return ErrPasswordMissing
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.ChangePassword(ctx, userID, string(hash))
|
||||
}
|
||||
|
||||
func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
return s.repo.GetUserByEmail(ctx, email)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,18 @@ func (r *UserRepo) CreateUser(ctx context.Context, email string, name string, pa
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) ChangePassword(ctx context.Context, userID string, passwordHash string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"UPDATE users SET password_hash = $1 WHERE id = $2",
|
||||
passwordHash, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
user := &domain.User{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user