all repos

onasty @ 9d9c06c

a one-time notes service
3 files changed, 56 insertions(+), 15 deletions(-)
fix: compare old and new password hashes (#127)

* test(e2e): if provided password is not correct

* refactor: use hasher.Compare to compare old and new password

* fix typos

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(e2e): assert the error

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-05 19:49:32 +0300
Parent: 9f5c654
M e2e/apiv1_auth_test.go

@@ -4,6 +4,7 @@ import (

"net/http" "github.com/gofrs/uuid/v5" + "github.com/olexsmir/onasty/internal/hasher" "github.com/olexsmir/onasty/internal/models" )

@@ -298,7 +299,7 @@ sessionDB = e.getLastSessionByUserID(uid)

e.Empty(sessionDB.RefreshToken) } -type apiv1AtuhChangePasswordRequest struct { +type apiv1AuthChangePasswordRequest struct { CurrentPassword string `json:"current_password"` NewPassword string `json:"new_password"` }

@@ -312,7 +313,7 @@

httpResp := e.httpRequest( http.MethodPost, "/api/v1/auth/change-password", - e.jsonify(apiv1AtuhChangePasswordRequest{ + e.jsonify(apiv1AuthChangePasswordRequest{ CurrentPassword: password, NewPassword: newPassword, }),

@@ -323,6 +324,30 @@ e.Equal(httpResp.Code, http.StatusOK)

userDB := e.getUserByEmail(email) e.NoError(e.hasher.Compare(userDB.Password, newPassword)) +} + +func (e *AppTestSuite) TestAuthV1_ChangePassword_wrongPassword() { + password := e.uuid() + newPassword := e.uuid() + email := e.uuid() + "@test.com" + _, toks := e.createAndSingIn(email, password) + + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/auth/change-password", + e.jsonify(apiv1AuthChangePasswordRequest{ + CurrentPassword: e.uuid(), + NewPassword: newPassword, + }), + toks.AccessToken, + ) + + e.Equal(http.StatusBadRequest, httpResp.Code) + + userDB := e.getUserByEmail(email) + + err := e.hasher.Compare(userDB.Password, newPassword) + e.ErrorIs(err, hasher.ErrMismatchedHashes) } type (
M internal/service/usersrv/usersrv.go

@@ -188,24 +188,26 @@ ctx context.Context,

userID uuid.UUID, inp dtos.ChangeUserPassword, ) error { - // TODO: compare current password with providede, and assert on mismatch - //nolint:exhaustruct if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil { return err } - oldPass, err := u.hasher.Hash(inp.CurrentPassword) + user, err := u.userstore.GetByID(ctx, userID) if err != nil { return err } + if err = u.hasher.Compare(user.Password, inp.CurrentPassword); err != nil { + return errors.Join(err, models.ErrUserInvalidPassword) + } + newPass, err := u.hasher.Hash(inp.NewPassword) if err != nil { return err } - if err := u.userstore.ChangePassword(ctx, userID, oldPass, newPass); err != nil { + if err := u.userstore.ChangePassword(ctx, userID, newPass); err != nil { return err }
M internal/store/psql/userepo/userepo.go

@@ -14,16 +14,17 @@

type UserStorer interface { Create(ctx context.Context, inp models.User) (uuid.UUID, error) - // GetUserByCredentials returns user by email and password + // GetByEmail returns user by email and password // the password should be hashed GetByEmail(ctx context.Context, email string) (models.User, error) + GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error) + GetByID(ctx context.Context, userID uuid.UUID) (models.User, error) - GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error) MarkUserAsActivated(ctx context.Context, id uuid.UUID) error // ChangePassword changes user password from oldPassword to newPassword // and oldPassword and newPassword should be hashed - ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error + ChangePassword(ctx context.Context, userID uuid.UUID, newPassword string) error // SetPassword sets new password for user by their id // password should be hashed

@@ -112,6 +113,22 @@

return id, err } +func (r *UserRepo) GetByID(ctx context.Context, userID uuid.UUID) (models.User, error) { + query := `--sql +select id, email, password, activated, created_at, last_login_at +from users +where id = $1` + + var user models.User + err := r.db.QueryRow(ctx, query, userID). + Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt) + if errors.Is(err, pgx.ErrNoRows) { + return models.User{}, models.ErrUserNotFound + } + + return user, err +} + func (r *UserRepo) GetByOAuthID( ctx context.Context, provider, providerID string,

@@ -164,15 +181,12 @@

func (r *UserRepo) ChangePassword( ctx context.Context, userID uuid.UUID, - oldPass, newPass string, + newPasswd string, ) error { query, args, err := pgq. Update("users"). - Set("password", newPass). - Where(pgq.Eq{ - "id": userID.String(), - "password": oldPass, - }). + Set("password", newPasswd). + Where(pgq.Eq{"id": userID.String()}). SQL() if err != nil { return err