From 64d2957853c35702582f2f687c79c5deebb62a3d Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 24 May 2024 20:34:49 -0500 Subject: [PATCH] implement password reset --- backend/app/api/handlers/v1/v1_ctrl_user.go | 53 ++++++++++++++++--- backend/app/api/routes.go | 1 + .../internal/core/services/service_user.go | 38 +++++++++++-- backend/internal/data/repo/repo_users.go | 7 +++ frontend/pages/index.vue | 1 - 5 files changed, 87 insertions(+), 13 deletions(-) diff --git a/backend/app/api/handlers/v1/v1_ctrl_user.go b/backend/app/api/handlers/v1/v1_ctrl_user.go index 7c9f577..8a05088 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_user.go +++ b/backend/app/api/handlers/v1/v1_ctrl_user.go @@ -117,12 +117,10 @@ func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc { } } -type ( - ChangePassword struct { - Current string `json:"current,omitempty"` - New string `json:"new,omitempty"` - } -) +type ChangePassword struct { + Current string `json:"current,omitempty"` + New string `json:"new,omitempty"` +} // HandleUserSelfChangePassword godoc // @@ -146,7 +144,42 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc { ctx := services.NewContext(r.Context()) - ok := ctrl.svc.User.ChangePassword(ctx, cp.Current, cp.New) + ok := ctrl.svc.User.PasswordChange(ctx, cp.Current, cp.New) + if !ok { + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.JSON(w, http.StatusNoContent, nil) + } +} + +// HandleUserSelfChangePasswordWithToken godoc +// +// @Summary Change Password +// @Tags User +// @Success 204 +// @Param payload body ChangePassword true "Password Payload" +// @Router /v1/users/change-password-token [PUT] +func (ctrl *V1Controller) HandleUserSelfChangePasswordWithToken() errchain.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + tokenQueryParam := r.URL.Query().Get("token") + if tokenQueryParam == "" { + return validate.NewRequestError(fmt.Errorf("missing token query param"), http.StatusBadRequest) + } + + if ctrl.isDemo { + return validate.NewRequestError(nil, http.StatusForbidden) + } + + var cp ChangePassword + err := server.Decode(r, &cp) + if err != nil { + log.Err(err).Msg("user failed to change password") + } + + ctx := services.NewContext(r.Context()) + + ok := ctrl.svc.User.PasswordChange(ctx, cp.Current, cp.New) if !ok { return validate.NewRequestError(err, http.StatusInternalServerError) } @@ -165,6 +198,10 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc { // @Router /v1/users/request-password-reset [Post] func (ctrl *V1Controller) HandleUserRequestPasswordReset() errchain.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { + if ctrl.isDemo { + return validate.NewRequestError(nil, http.StatusForbidden) + } + v, err := adapters.DecodeBody[services.PasswordResetRequest](r) if err != nil { return err @@ -173,7 +210,7 @@ func (ctrl *V1Controller) HandleUserRequestPasswordReset() errchain.HandlerFunc go func() { ctx := context.Background() - err = ctrl.svc.User.RequestPasswordReset(ctx, v) + err = ctrl.svc.User.PasswordResetRequest(ctx, v) if err != nil { log.Warn(). Err(err). diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 1975130..af97569 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -85,6 +85,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain) { r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...)) r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...)) r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...)) + r.Put(v1Base("/users/self/change-password-token"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePasswordWithToken())) r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...)) r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...)) diff --git a/backend/internal/core/services/service_user.go b/backend/internal/core/services/service_user.go index 6f04e85..05c5a6f 100644 --- a/backend/internal/core/services/service_user.go +++ b/backend/internal/core/services/service_user.go @@ -234,18 +234,18 @@ func (svc *UserService) DeleteSelf(ctx context.Context, userID uuid.UUID) error return svc.repos.Users.Delete(ctx, userID) } -func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) { +func (svc *UserService) PasswordChange(ctx Context, currentPassword, newPassword string) (ok bool) { usr, err := svc.repos.Users.GetOneID(ctx, ctx.UserID) if err != nil { return false } - if !hasher.CheckPasswordHash(current, usr.PasswordHash) { + if !hasher.CheckPasswordHash(currentPassword, usr.PasswordHash) { log.Err(errors.New("current password is incorrect")).Msg("Failed to change password") return false } - hashed, err := hasher.HashPassword(new) + hashed, err := hasher.HashPassword(newPassword) if err != nil { log.Err(err).Msg("Failed to hash password") return false @@ -260,7 +260,37 @@ func (svc *UserService) ChangePassword(ctx Context, current string, new string) return true } -func (svc *UserService) RequestPasswordReset(ctx context.Context, req PasswordResetRequest) error { +func (svc *UserService) PasswordChangeWithToken(ctx Context, token, newPassword string) error { + hashed, err := hasher.HashPassword(newPassword) + if err != nil { + return err + } + + tokenHash := hasher.HashToken(token) + + resetToken, err := svc.repos.Users.PasswordResetGet(ctx.Context, tokenHash) + if err != nil { + return err + } + + if resetToken.UserID != ctx.UserID { + return ErrorTokenIDMismatch + } + + err = svc.repos.Users.ChangePassword(ctx.Context, ctx.UserID, hashed) + if err != nil { + return err + } + + err = svc.repos.Users.PasswordResetDelete(ctx.Context, tokenHash) + if err != nil { + return err + } + + return nil +} + +func (svc *UserService) PasswordResetRequest(ctx context.Context, req PasswordResetRequest) error { usr, err := svc.repos.Users.GetOneEmail(ctx, req.Email) if err != nil { log.Warn().Err(err).Msg("failed to get user for email reset") diff --git a/backend/internal/data/repo/repo_users.go b/backend/internal/data/repo/repo_users.go index a316bb8..26c00e2 100644 --- a/backend/internal/data/repo/repo_users.go +++ b/backend/internal/data/repo/repo_users.go @@ -149,3 +149,10 @@ func (r *UserRepository) PasswordResetGet(ctx context.Context, token []byte) (*e WithUser(). Only(ctx) } + +func (r *UserRepository) PasswordResetDelete(ctx context.Context, token []byte) error { + _, err := r.db.ActionToken.Delete(). + Where(actiontoken.Token(token)). + Exec(ctx) + return err +} diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index c727532..2122e1b 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -128,7 +128,6 @@ } toast.success("Password reset link sent to your email"); - return await Promise.resolve(); }