From 4879759fe650e7670c0a8efbefce3568b95a635d Mon Sep 17 00:00:00 2001 From: Isaque Veras <46972789+isaqueveras@users.noreply.github.com> Date: Sun, 1 Oct 2023 13:40:28 -0300 Subject: [PATCH] feat: router to change password (#133) (#135) * feat: router to change password * fix: change password * fix: function name --- application/auth/user_business.go | 57 +++++++++++++++++++ domain/auth/interface.go | 4 +- domain/auth/model.go | 15 ++++- .../persistencie/auth/postgres/auth.go | 4 +- .../persistencie/auth/postgres/session.go | 24 +++++++- .../persistencie/auth/postgres/user.go | 16 ++++++ .../persistencie/auth/session_repository.go | 8 ++- .../persistencie/auth/user_repository.go | 4 ++ interface/http/auth/handler.go | 20 +++++++ interface/http/auth/router.go | 1 + 10 files changed, 145 insertions(+), 8 deletions(-) diff --git a/application/auth/user_business.go b/application/auth/user_business.go index df5b55c..48d23e9 100644 --- a/application/auth/user_business.go +++ b/application/auth/user_business.go @@ -9,8 +9,10 @@ import ( "github.com/google/uuid" pg "github.com/isaqueveras/powersso/database/postgres" + domain "github.com/isaqueveras/powersso/domain/auth" "github.com/isaqueveras/powersso/infrastructure/persistencie/auth" "github.com/isaqueveras/powersso/oops" + "github.com/isaqueveras/powersso/utils" ) // Disable is the business logic for disable user @@ -32,3 +34,58 @@ func Disable(ctx context.Context, userUUID *uuid.UUID) error { return nil } + +// ChangePassword is the busines logic for change passoword +func ChangePassword(ctx context.Context, in *domain.ChangePassword) (err error) { + tx, err := pg.NewTransaction(ctx, false) + if err != nil { + return oops.Err(err) + } + defer tx.Rollback() + + repoUser := auth.NewUserRepository(tx) + repoSession := auth.NewSessionRepository(tx) + + user := domain.User{ID: in.UserID} + if err = repoUser.Get(&user); err != nil { + return oops.Err(err) + } + + if !user.OTPConfigured() { + return oops.New("2-factor authentication not configured") + } + + // Validate code otp + if err = utils.ValidateToken(user.OTPToken, in.CodeOTP); err != nil { + return oops.New("2-factor authentication code is invalid") + } + + // Generate new password crypto + gen := &domain.CreateAccount{Password: in.Password} + if err = gen.GeneratePassword(); err != nil { + return err + } + + // Change user password + in.Password, in.Key = gen.Password, gen.Key + if err = repoUser.ChangePassword(in); err != nil { + return oops.Err(err) + } + + // Getting all active user sessions + var sessions []*uuid.UUID + if sessions, err = repoSession.Get(in.UserID); err != nil { + return oops.Err(err) + } + + // Disabling all active user sessions + if err = repoSession.Delete(sessions...); err != nil { + return oops.Err(err) + } + + if err = tx.Commit(); err != nil { + return oops.Err(err) + } + + return +} diff --git a/domain/auth/interface.go b/domain/auth/interface.go index 3f72378..2040ed4 100644 --- a/domain/auth/interface.go +++ b/domain/auth/interface.go @@ -20,7 +20,8 @@ type IAuth interface { // ISession define an interface for data layer access methods type ISession interface { Create(userID *uuid.UUID, clientIP, userAgent *string) (*uuid.UUID, error) - Delete(sessionID *uuid.UUID) error + Delete(ids ...*uuid.UUID) error + Get(userID *uuid.UUID) ([]*uuid.UUID, error) } // IFlag define an interface for data layer access methods @@ -40,4 +41,5 @@ type IUser interface { Get(user *User) error Exist(email *string) error Disable(userUUID *uuid.UUID) error + ChangePassword(*ChangePassword) error } diff --git a/domain/auth/model.go b/domain/auth/model.go index d68d5c3..57b37e1 100644 --- a/domain/auth/model.go +++ b/domain/auth/model.go @@ -58,7 +58,6 @@ type CreateAccount struct { // Prepare prepare data for registration func (rr *CreateAccount) Prepare() (err error) { rr.Email = utils.Pointer(strings.ToLower(strings.TrimSpace(*rr.Email))) - rr.Password = utils.Pointer(strings.TrimSpace(*rr.Password)) if err = rr.GeneratePassword(); err != nil { return err @@ -76,6 +75,7 @@ func (rr *CreateAccount) RefreshTokenKey() { // GeneratePassword hash user password with bcrypt func (rr *CreateAccount) GeneratePassword() error { + rr.Password = utils.Pointer(strings.TrimSpace(*rr.Password)) rr.RefreshTokenKey() cost := CostHashPasswordDevelopment @@ -172,6 +172,19 @@ type Login struct { UserAgent *string `json:"-"` } +type ChangePassword struct { + UserID *uuid.UUID `json:"user_id"` + Password *string `json:"password"` + ConfirmPassword *string `json:"confirm_password"` + CodeOTP *string `json:"code_otp"` + Key *string `json:"-"` +} + +// ValidatePassword validate passwords for change password +func (c *ChangePassword) ValidatePassword() bool { + return strings.TrimSpace(*c.Password) == strings.TrimSpace(*c.ConfirmPassword) +} + // ComparePasswords compare user password and payload func (l *Login) ComparePasswords(passw, key *string) error { if err := bcrypt.CompareHashAndPassword([]byte(*passw), []byte(*key+*l.Password)); err != nil { diff --git a/infrastructure/persistencie/auth/postgres/auth.go b/infrastructure/persistencie/auth/postgres/auth.go index 55d8eaf..e19c993 100644 --- a/infrastructure/persistencie/auth/postgres/auth.go +++ b/infrastructure/persistencie/auth/postgres/auth.go @@ -85,8 +85,8 @@ func (pg *PGAuth) MarkTokenAsUsed(token *uuid.UUID) (err error) { func (pg *PGAuth) AddAttempts(userID *uuid.UUID) (err error) { if _, err = pg.DB.Builder. Update("users"). - Set("number_failed_attempts", squirrel.Expr("number_failed_attempts + 1")). - Set("last_failure_date", squirrel.Expr("NOW()")). + Set("attempts", squirrel.Expr("attempts + 1")). + Set("last_failure", squirrel.Expr("NOW()")). Where("id = ?", userID). Exec(); err != nil && err != sql.ErrNoRows { return oops.Err(err) diff --git a/infrastructure/persistencie/auth/postgres/session.go b/infrastructure/persistencie/auth/postgres/session.go index 1e575e3..1bebe17 100644 --- a/infrastructure/persistencie/auth/postgres/session.go +++ b/infrastructure/persistencie/auth/postgres/session.go @@ -43,14 +43,34 @@ func (pg *PGSession) Create(userID *uuid.UUID, clientIP, userAgent *string) (ses } // Delete delete session of the user in database -func (pg *PGSession) Delete(sessionID *uuid.UUID) (err error) { +func (pg *PGSession) Delete(ids ...*uuid.UUID) (err error) { if _, err = pg.DB.Builder. Update("sessions"). Set("deleted_at", squirrel.Expr("NOW()")). - Where("id = ? AND deleted_at IS NULL", sessionID). + Where("deleted_at IS NULL"). + Where(squirrel.Eq{"id": ids}). Exec(); err != nil && err != sql.ErrNoRows { return oops.Err(err) } return } + +func (pg *PGSession) Get(userID *uuid.UUID) (sessions []*uuid.UUID, err error) { + query := pg.DB.Builder.Select("id").From("sessions").Where("user_id = ? AND deleted_at IS NULL", userID) + + row, err := query.Query() + if err != nil { + return nil, oops.Err(err) + } + + for row.Next() { + var sessionID *uuid.UUID + if err = row.Scan(&sessionID); err != nil { + return nil, oops.Err(err) + } + sessions = append(sessions, sessionID) + } + + return +} diff --git a/infrastructure/persistencie/auth/postgres/user.go b/infrastructure/persistencie/auth/postgres/user.go index dabc028..d45130c 100644 --- a/infrastructure/persistencie/auth/postgres/user.go +++ b/infrastructure/persistencie/auth/postgres/user.go @@ -71,3 +71,19 @@ func (pg *PGUser) Disable(userUUID *uuid.UUID) (err error) { return } + +func (pg *PGUser) ChangePassword(in *domain.ChangePassword) error { + if err := pg.DB.Builder. + Update("users"). + Set("password", in.Password). + Set("attempts", 0). + Set("key", in.Key). + Set("last_failure", squirrel.Expr("NULL")). + Where(squirrel.Eq{"id": in.UserID, "active": true}). + Suffix("RETURNING id"). + Scan(new(string)); err != nil { + return oops.Err(err) + } + + return nil +} diff --git a/infrastructure/persistencie/auth/session_repository.go b/infrastructure/persistencie/auth/session_repository.go index 0f267c9..3c63650 100644 --- a/infrastructure/persistencie/auth/session_repository.go +++ b/infrastructure/persistencie/auth/session_repository.go @@ -26,6 +26,10 @@ func (r *repoSession) Create(userID *uuid.UUID, clientIP, userAgent *string) (*u } // Delete delete a session for a user -func (r *repoSession) Delete(sessionID *uuid.UUID) error { - return r.pg.Delete(sessionID) +func (r *repoSession) Delete(ids ...*uuid.UUID) error { + return r.pg.Delete(ids...) +} + +func (r *repoSession) Get(userID *uuid.UUID) ([]*uuid.UUID, error) { + return r.pg.Get(userID) } diff --git a/infrastructure/persistencie/auth/user_repository.go b/infrastructure/persistencie/auth/user_repository.go index b8bce0b..604b097 100644 --- a/infrastructure/persistencie/auth/user_repository.go +++ b/infrastructure/persistencie/auth/user_repository.go @@ -34,3 +34,7 @@ func (r *repoUser) Exist(email *string) error { func (r *repoUser) Disable(userUUID *uuid.UUID) error { return r.pg.Disable(userUUID) } + +func (r *repoUser) ChangePassword(in *domain.ChangePassword) error { + return r.pg.ChangePassword(in) +} diff --git a/interface/http/auth/handler.go b/interface/http/auth/handler.go index e87f418..b2d101a 100644 --- a/interface/http/auth/handler.go +++ b/interface/http/auth/handler.go @@ -91,6 +91,26 @@ func login(ctx *gin.Context) { ctx.JSON(http.StatusOK, output) } +func changePassword(ctx *gin.Context) { + in := &domain.ChangePassword{} + if err := ctx.ShouldBindJSON(in); err != nil { + oops.Handling(ctx, err) + return + } + + if ok := in.ValidatePassword(); !ok { + oops.Handling(ctx, oops.New("Invalid passwords")) + return + } + + if err := app.ChangePassword(ctx, in); err != nil { + oops.Handling(ctx, err) + return + } + + ctx.JSON(http.StatusOK, nil) +} + // logout godoc // @Summary User logout // @Description Route to logout a user account into the system diff --git a/interface/http/auth/router.go b/interface/http/auth/router.go index 76be93f..d049e61 100644 --- a/interface/http/auth/router.go +++ b/interface/http/auth/router.go @@ -15,6 +15,7 @@ func Router(r *gin.RouterGroup) { r.POST("create_account", createAccount) r.POST("login", login) r.GET("login/steps", loginSteps) + r.PUT("change_password", changePassword) } // RouterAuthorization is the router for the auth module.