105 lines
2.9 KiB
Go

package database
import (
"errors"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
Username string `gorm:"unique;not null;index"`
PasswordHash string `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Notes []Note `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
}
var (
ErrUserNotFound = errors.New("user not found")
ErrUsernameTaken = errors.New("username is not available")
ErrInvalidCredentials = errors.New("invalid credentials")
)
// Create a new user using the given credentials (password hashed with `bcrypt`).
func CreateUser(db *gorm.DB, username string, password string) (*User, error) {
// Username availability
var count int64
db.Model(&User{}).Where("username = ?", username).Count(&count)
if count > 0 {
return nil, ErrUsernameTaken
}
// Password hashing, max length (72 bytes) validated in the handler layer
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := User{
Username: username,
PasswordHash: string(hash),
}
err = db.Create(&user).Error
return &user, err
}
// Get a user with their UUID.
func GetUserByID(db *gorm.DB, userID string) (*User, error) {
var user User
err := db.First(&user, "id = ?", userID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return &user, err
}
// Get a user with their username.
func GetUserByUsername(db *gorm.DB, username string) (*User, error) {
var user User
err := db.First(&user, "username = ?", username).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return &user, err
}
// Verify the given credentials by fetching the hashed password from the database and comparing it
// with the given one using `bcrypt`.
func VerifyUserCredentials(db *gorm.DB, username string, password string) (*User, error) {
user, err := GetUserByUsername(db, username)
if err != nil {
return nil, err // ErrUserNotFound
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
return nil, ErrInvalidCredentials
}
return user, nil
}
// Update the password of the given user (identified with UUID).
func UpdatePassword(db *gorm.DB, userID string, newPassword string) error {
// Once again (length) validation should be done in the handler layer
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return db.Model(&User{}).
Where("id = ?", userID).
Update("password_hash", string(hash)).
Error
}
// Remove a user and their notes (and notes' versions, hard delete with cascade).
func DeleteUser(db *gorm.DB, userID string) error {
return db.Transaction(func(tx *gorm.DB) error {
return tx.Delete(&User{}, "id = ?", userID).Error
})
}