Skip to content

Commit

Permalink
feat: router to change password (#133) (#135)
Browse files Browse the repository at this point in the history
* feat: router to change password

* fix: change password

* fix: function name
  • Loading branch information
isaqueveras committed Oct 1, 2023
1 parent 3c2c843 commit 4879759
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 8 deletions.
57 changes: 57 additions & 0 deletions application/auth/user_business.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
4 changes: 3 additions & 1 deletion domain/auth/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,4 +41,5 @@ type IUser interface {
Get(user *User) error
Exist(email *string) error
Disable(userUUID *uuid.UUID) error
ChangePassword(*ChangePassword) error
}
15 changes: 14 additions & 1 deletion domain/auth/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/persistencie/auth/postgres/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 22 additions & 2 deletions infrastructure/persistencie/auth/postgres/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 16 additions & 0 deletions infrastructure/persistencie/auth/postgres/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 6 additions & 2 deletions infrastructure/persistencie/auth/session_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions infrastructure/persistencie/auth/user_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
20 changes: 20 additions & 0 deletions interface/http/auth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions interface/http/auth/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 4879759

Please sign in to comment.