BeforeSave does not populate data

Viewed 166

I have the following code, a very simple model User, and I am trying to update the password for an existing user; however, in BeforeSave, I could NOT get u *User data populated, so for the first time create, password is hashed, but when I update it, password become plain text. Any ideas what I do wrong here?

package main

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Username string `gorm:"type:varchar(40);unique" json:"username,omitempty"`
    Password string `gorm:"size:255" json:"password,omitempty"`
    NickName string `gorm:"type:varchar(32)" json:"nick_name,omitempty"`
}

func MakePassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

// BeforeSave : hook before a user is saved
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
    fmt.Println("================= user: ", u)
    if u.Password != "" {
        hash, err := MakePassword(u.Password)
        if err != nil {
            return nil
        }
        tx.Statement.SetColumn("password", hash)
    }
    return
}

func main() {
    var connectionString = fmt.Sprintf(
        "%s:%s@/%s?charset=utf8&parseTime=True&loc=Local",
        "root", "password", "myproject",
    )
    db, _ := gorm.Open(mysql.Open(connectionString), &gorm.Config{})
    db.AutoMigrate(&User{})
    db.Save(&User{
        Username: "username",
        Password: "123",
    })
    // Up to this point, everything works, password is hashed.
    // the following does not work as I expected

    userMap := make(map[string]interface{})
    userMap["id"] = 1
    userMap["password"] = "new_password"

    db.Model(&User{}).Where(&User{
        Model: gorm.Model{ID: uint(userMap["id"].(int))},
    }).Updates(userMap)
}
1 Answers

The problem is probably your if statement: if u.Password != "". When updating using a map, I am almost certain that the empty model will be used, so u.Password will be empty.

I do it this way with BeforeCreate and BeforeUpdate instead of BeforeSave:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    return u.bcryptPassword(tx)
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    if tx.Statement.Changed("Password") {
        return u.bcryptPassword(tx)
    }

    return nil
}

func (u *User) bcryptPassword(tx *gorm.DB) error {
    var newPass string
    switch u := tx.Statement.Dest.(type) {
    case map[string]interface{}:
        newPass = u["password"].(string)
    case *User:
        newPass = u.Password
    case []*User:
        newPass = u[tx.Statement.CurDestIndex].Password
    }

    b, err := bcrypt.GenerateFromPassword([]byte(newPass), 10)
    if err != nil {
        return err
    }
    tx.Statement.SetColumn("password", b)

    return nil
}

You should rely on the given gorm transaction, not on your u variable.

Note: This implementation will also work for batch operations and inserts/updates with maps instead of structures.

Related