implement password reset

This commit is contained in:
Hayden 2024-05-24 20:34:49 -05:00
parent 2231c54f21
commit 64d2957853
No known key found for this signature in database
GPG key ID: 17CF79474E257545
5 changed files with 87 additions and 13 deletions

View file

@ -117,12 +117,10 @@ func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
} }
} }
type ( type ChangePassword struct {
ChangePassword struct {
Current string `json:"current,omitempty"` Current string `json:"current,omitempty"`
New string `json:"new,omitempty"` New string `json:"new,omitempty"`
} }
)
// HandleUserSelfChangePassword godoc // HandleUserSelfChangePassword godoc
// //
@ -146,7 +144,42 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
ctx := services.NewContext(r.Context()) 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 { if !ok {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
@ -165,6 +198,10 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
// @Router /v1/users/request-password-reset [Post] // @Router /v1/users/request-password-reset [Post]
func (ctrl *V1Controller) HandleUserRequestPasswordReset() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserRequestPasswordReset() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { 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) v, err := adapters.DecodeBody[services.PasswordResetRequest](r)
if err != nil { if err != nil {
return err return err
@ -173,7 +210,7 @@ func (ctrl *V1Controller) HandleUserRequestPasswordReset() errchain.HandlerFunc
go func() { go func() {
ctx := context.Background() ctx := context.Background()
err = ctrl.svc.User.RequestPasswordReset(ctx, v) err = ctrl.svc.User.PasswordResetRequest(ctx, v)
if err != nil { if err != nil {
log.Warn(). log.Warn().
Err(err). Err(err).

View file

@ -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.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), 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"), 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.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...)) r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))

View file

@ -234,18 +234,18 @@ func (svc *UserService) DeleteSelf(ctx context.Context, userID uuid.UUID) error
return svc.repos.Users.Delete(ctx, userID) 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) usr, err := svc.repos.Users.GetOneID(ctx, ctx.UserID)
if err != nil { if err != nil {
return false 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") log.Err(errors.New("current password is incorrect")).Msg("Failed to change password")
return false return false
} }
hashed, err := hasher.HashPassword(new) hashed, err := hasher.HashPassword(newPassword)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to hash password") log.Err(err).Msg("Failed to hash password")
return false return false
@ -260,7 +260,37 @@ func (svc *UserService) ChangePassword(ctx Context, current string, new string)
return true 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) usr, err := svc.repos.Users.GetOneEmail(ctx, req.Email)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("failed to get user for email reset") log.Warn().Err(err).Msg("failed to get user for email reset")

View file

@ -149,3 +149,10 @@ func (r *UserRepository) PasswordResetGet(ctx context.Context, token []byte) (*e
WithUser(). WithUser().
Only(ctx) 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
}

View file

@ -128,7 +128,6 @@
} }
toast.success("Password reset link sent to your email"); toast.success("Password reset link sent to your email");
return await Promise.resolve();
} }
</script> </script>