package models import ( "errors" "strings" "time" "git.kirsle.net/apps/gosocial/pkg/config" "git.kirsle.net/apps/gosocial/pkg/log" "golang.org/x/crypto/bcrypt" "gorm.io/gorm/clause" ) // User account table. type User struct { ID uint64 `gorm:"primaryKey"` Username string `gorm:"uniqueIndex"` Email string `gorm:"uniqueIndex"` HashedPassword string IsAdmin bool `gorm:"index"` Status UserStatus `gorm:"index"` // active, disabled Visibility string `gorm:"index"` // public, private Name *string Birthdate time.Time Certified bool CreatedAt time.Time `gorm:"index"` UpdatedAt time.Time `gorm:"index"` LastLoginAt time.Time `gorm:"index"` // Relational tables. ProfileField []ProfileField } // UserStatus options. type UserStatus string const ( UserStatusActive = "active" UserStatusDisabled = "disabled" ) // CreateUser. It is assumed username and email are correctly formatted. func CreateUser(username, email, password string) (*User, error) { // Verify username and email are unique. if _, err := FindUser(username); err == nil { return nil, errors.New("That username already exists. Please try a different username.") } else if _, err := FindUser(email); err == nil { return nil, errors.New("That email address is already registered.") } u := &User{ Username: username, Email: email, Status: UserStatusActive, } if err := u.HashPassword(password); err != nil { return nil, err } result := DB.Create(u) return u, result.Error } // GetUser by ID. func GetUser(userId uint64) (*User, error) { user := &User{} result := DB.Preload(clause.Associations).First(&user, userId) return user, result.Error } // FindUser by username or email. func FindUser(username string) (*User, error) { if username == "" { return nil, errors.New("username is required") } u := &User{} if strings.ContainsRune(username, '@') { result := DB.Preload(clause.Associations).Where("email = ?", username).Limit(1).First(u) return u, result.Error } result := DB.Preload(clause.Associations).Where("username = ?", username).Limit(1).First(u) return u, result.Error } // HashPassword sets the user's hashed (bcrypt) password. func (u *User) HashPassword(password string) error { passwd, err := bcrypt.GenerateFromPassword([]byte(password), config.BcryptCost) if err != nil { return err } u.HashedPassword = string(passwd) return nil } // CheckPassword verifies the password is correct. Returns nil on success. func (u *User) CheckPassword(password string) error { return bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(password)) } // SetProfileField sets or creates a named profile field. func (u *User) SetProfileField(name, value string) { // Check if it exists. log.Debug("User(%s).SetProfileField(%s, %s)", u.Username, name, value) var exists bool for _, field := range u.ProfileField { log.Debug("\tCheck existing field %s", field.Name) if field.Name == name { log.Debug("\tFound existing field!") changed := field.Value != value field.Value = value exists = true // Save it now. TODO: otherwise gorm doesn't know we changed // it and it won't be inserted when the User is saved. But // this is probably not performant to do! if changed { DB.Save(&field) } break } } if exists { return } u.ProfileField = append(u.ProfileField, ProfileField{ Name: name, Value: value, }) } // GetProfileField returns the value of a profile field or blank string. func (u *User) GetProfileField(name string) string { for _, field := range u.ProfileField { if field.Name == name { return field.Value } } return "" } // ProfileFieldIn checks if a substring is IN a profile field. Currently // does a naive strings.Contains(), intended for the "here_for" field. func (u *User) ProfileFieldIn(field, substr string) bool { value := u.GetProfileField(field) return strings.Contains(value, substr) } // Save user. func (u *User) Save() error { result := DB.Save(u) return result.Error }