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 }) }