164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package models
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type UserBun struct {
|
|
bun.BaseModel `bun:"table:users,alias:u"`
|
|
|
|
ID int `bun:"id,pk,autoincrement"` // Integer ID (index primary key)
|
|
Username string `bun:"username,unique"` // Username (unique)
|
|
PasswordHash string `bun:"password_hash,nullzero"` // Bcrypt hashed password (not exported in JSON)
|
|
CreatedAt int64 `bun:"created_at"` // Epoch timestamp when the user was added to the database
|
|
Bio string `bun:"bio"` // Short byline set by the user
|
|
}
|
|
|
|
func (user *UserBun) GetID() int {
|
|
return user.ID
|
|
}
|
|
|
|
// Uses bcrypt to set the users password_hash from the given password
|
|
func (user *UserBun) SetPassword(ctx context.Context, tx bun.Tx, password string) error {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return errors.Wrap(err, "bcrypt.GenerateFromPassword")
|
|
}
|
|
newPassword := string(hashedPassword)
|
|
|
|
_, err = tx.NewUpdate().
|
|
Model(user).
|
|
Set("password_hash = ?", newPassword).
|
|
Where("id = ?", user.ID).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "tx.Update")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Uses bcrypt to check if the given password matches the users password_hash
|
|
func (user *UserBun) CheckPassword(ctx context.Context, tx bun.Tx, password string) error {
|
|
var hashedPassword string
|
|
err := tx.NewSelect().
|
|
Table("users").
|
|
Column("password_hash").
|
|
Where("id = ?", user.ID).
|
|
Limit(1).
|
|
Scan(ctx, &hashedPassword)
|
|
if err != nil {
|
|
return errors.Wrap(err, "tx.Select")
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
|
if err != nil {
|
|
return errors.Wrap(err, "Username or password incorrect")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Change the user's username
|
|
func (user *UserBun) ChangeUsername(ctx context.Context, tx bun.Tx, newUsername string) error {
|
|
_, err := tx.NewUpdate().
|
|
Model(user).
|
|
Set("username = ?", newUsername).
|
|
Where("id = ?", user.ID).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "tx.Update")
|
|
}
|
|
user.Username = newUsername
|
|
return nil
|
|
}
|
|
|
|
// Change the user's bio
|
|
func (user *UserBun) ChangeBio(ctx context.Context, tx bun.Tx, newBio string) error {
|
|
_, err := tx.NewUpdate().
|
|
Model(user).
|
|
Set("bio = ?", newBio).
|
|
Where("id = ?", user.ID).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "tx.Update")
|
|
}
|
|
user.Bio = newBio
|
|
return nil
|
|
}
|
|
|
|
// CreateUser creates a new user with the given username and password
|
|
func CreateUser(ctx context.Context, tx bun.Tx, username, password string) (*UserBun, error) {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "bcrypt.GenerateFromPassword")
|
|
}
|
|
|
|
user := &UserBun{
|
|
Username: username,
|
|
PasswordHash: string(hashedPassword),
|
|
CreatedAt: 0, // You may want to set this to time.Now().Unix()
|
|
Bio: "",
|
|
}
|
|
|
|
_, err = tx.NewInsert().
|
|
Model(user).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.Insert")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetUserByID queries the database for a user matching the given ID
|
|
// Returns nil, nil if no user is found
|
|
func GetUserByID(ctx context.Context, tx bun.Tx, id int) (*UserBun, error) {
|
|
user := new(UserBun)
|
|
err := tx.NewSelect().
|
|
Model(user).
|
|
Where("id = ?", id).
|
|
Limit(1).
|
|
Scan(ctx)
|
|
if err != nil {
|
|
if err.Error() == "sql: no rows in result set" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(err, "tx.Select")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// GetUserByUsername queries the database for a user matching the given username
|
|
// Returns nil, nil if no user is found
|
|
func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*UserBun, error) {
|
|
user := new(UserBun)
|
|
err := tx.NewSelect().
|
|
Model(user).
|
|
Where("username = ?", username).
|
|
Limit(1).
|
|
Scan(ctx)
|
|
if err != nil {
|
|
if err.Error() == "sql: no rows in result set" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(err, "tx.Select")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// IsUsernameUnique checks if the given username is unique (not already taken)
|
|
// Returns true if the username is available, false if it's taken
|
|
func IsUsernameUnique(ctx context.Context, tx bun.Tx, username string) (bool, error) {
|
|
count, err := tx.NewSelect().
|
|
Model((*UserBun)(nil)).
|
|
Where("username = ?", username).
|
|
Count(ctx)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "tx.Count")
|
|
}
|
|
return count == 0, nil
|
|
}
|