157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
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
|
|
}
|