♻️ Simplify user storage
This commit is contained in:
parent
9cef5c63a2
commit
e6a62bfeb1
139
store.go
139
store.go
|
@ -26,6 +26,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -65,7 +66,8 @@ const (
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string `json:"-"`
|
||||||
|
PasswordHash string `json:"Password"`
|
||||||
Role GlobalRole
|
Role GlobalRole
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,18 +98,6 @@ func (u User) key() string {
|
||||||
return usersPrefix + u.Username
|
return usersPrefix + u.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) prefix() string {
|
|
||||||
return fmt.Sprintf("user:%s", u.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u User) roleKey() string {
|
|
||||||
return u.prefix() + ":role"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u User) passwordKey() string {
|
|
||||||
return u.prefix() + ":password"
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDBStore(filename string) (*DBStore, error) {
|
func NewDBStore(filename string) (*DBStore, error) {
|
||||||
db, err := buntdb.Open(filename)
|
db, err := buntdb.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -128,24 +118,21 @@ var ErrUserExists = errors.New("user already exists")
|
||||||
var ErrUserNotFound = errors.New("user not found")
|
var ErrUserNotFound = errors.New("user not found")
|
||||||
var ErrInvalidUsername = errors.New("invalid username")
|
var ErrInvalidUsername = errors.New("invalid username")
|
||||||
|
|
||||||
func (store *DBStore) setUserValues(tx *buntdb.Tx, user User) (exists bool, err error) {
|
func (u User) merge(updates User) (User, error) {
|
||||||
pwString, err := buildPassword(user.Username, user.Password)
|
merged := u
|
||||||
|
|
||||||
|
if updates.Password != "" {
|
||||||
|
pwHash, err := hashPassword(updates.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("cannot hash password: %w", err)
|
return u, fmt.Errorf("cannot hash password: %w", err)
|
||||||
|
}
|
||||||
|
merged.PasswordHash = pwHash
|
||||||
|
}
|
||||||
|
if updates.Role != "" {
|
||||||
|
merged.Role = updates.Role
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, replaced, err := tx.Set(user.roleKey(), string(user.Role), nil); err != nil {
|
return merged, nil
|
||||||
return false, err
|
|
||||||
} else if replaced {
|
|
||||||
exists = true
|
|
||||||
}
|
|
||||||
if _, replaced, err := tx.Set(user.passwordKey(), pwString, nil); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else if replaced {
|
|
||||||
exists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return exists, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *DBStore) AddUser(user User) (err error) {
|
func (store *DBStore) AddUser(user User) (err error) {
|
||||||
|
@ -153,17 +140,17 @@ func (store *DBStore) AddUser(user User) (err error) {
|
||||||
return ErrInvalidUsername
|
return ErrInvalidUsername
|
||||||
}
|
}
|
||||||
if err = store.db.Update(func(tx *buntdb.Tx) error {
|
if err = store.db.Update(func(tx *buntdb.Tx) error {
|
||||||
if _, exists, err := tx.Set(user.key(), "", nil); err != nil {
|
userBytes, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists, err := tx.Set(user.key(), string(userBytes), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if exists {
|
} else if exists {
|
||||||
return ErrUserExists
|
return ErrUserExists
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists, err := store.setUserValues(tx, user); err != nil {
|
|
||||||
return err
|
|
||||||
} else if exists {
|
|
||||||
return ErrUserExists
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -175,22 +162,16 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
|
||||||
if strings.Contains(username, ":") {
|
if strings.Contains(username, ":") {
|
||||||
return user, ErrInvalidUsername
|
return user, ErrInvalidUsername
|
||||||
}
|
}
|
||||||
user.Username = username
|
|
||||||
if err := store.db.View(func(tx *buntdb.Tx) error {
|
if err := store.db.View(func(tx *buntdb.Tx) error {
|
||||||
if val, err := tx.Get(user.roleKey()); err != nil && err != buntdb.ErrNotFound {
|
user.Username = username
|
||||||
|
if val, err := tx.Get(user.key()); err != nil && err != buntdb.ErrNotFound {
|
||||||
return err
|
return err
|
||||||
} else if err == buntdb.ErrNotFound {
|
} else if err == buntdb.ErrNotFound {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
} else {
|
} else if err := json.Unmarshal([]byte(val), &user); err != nil {
|
||||||
user.Role = GlobalRole(val)
|
return fmt.Errorf("cannot unmarshal user: %w", err)
|
||||||
}
|
|
||||||
if val, err := tx.Get(user.passwordKey()); err != nil && err != buntdb.ErrNotFound {
|
|
||||||
return err
|
|
||||||
} else if err == buntdb.ErrNotFound {
|
|
||||||
return ErrUserNotFound
|
|
||||||
} else {
|
|
||||||
user.Password = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -200,24 +181,26 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
|
||||||
|
|
||||||
func (store *DBStore) GetUsers() (users []User, err error) {
|
func (store *DBStore) GetUsers() (users []User, err error) {
|
||||||
err = store.db.View(func(tx *buntdb.Tx) error {
|
err = store.db.View(func(tx *buntdb.Tx) error {
|
||||||
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool {
|
var processingError error
|
||||||
|
|
||||||
|
if err := tx.AscendKeys(userPrefix+"*", func(key, value string) bool {
|
||||||
var user User
|
var user User
|
||||||
user.Username = strings.TrimPrefix(key, usersPrefix)
|
|
||||||
|
if err := json.Unmarshal([]byte(value), &user); err != nil {
|
||||||
|
processingError = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just in case ...
|
||||||
|
user.Username = strings.TrimPrefix(key, userPrefix)
|
||||||
|
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
return true
|
return true
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range users {
|
return processingError
|
||||||
if roleString, err := tx.Get(users[i].roleKey()); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
users[i].Role = GlobalRole(roleString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return users, err
|
return users, err
|
||||||
|
@ -228,7 +211,29 @@ func (store *DBStore) Update(user User) error {
|
||||||
return ErrInvalidUsername
|
return ErrInvalidUsername
|
||||||
}
|
}
|
||||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||||
_, err := store.setUserValues(tx, user)
|
var existingUser User
|
||||||
|
|
||||||
|
if val, err := tx.Get(user.key()); err != nil && err != buntdb.ErrNotFound {
|
||||||
|
return err
|
||||||
|
} else if err == buntdb.ErrNotFound {
|
||||||
|
return ErrUserNotFound
|
||||||
|
} else if err := json.Unmarshal([]byte(val), &existingUser); err != nil {
|
||||||
|
return fmt.Errorf("cannot unmarshal user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedUser, err := existingUser.merge(user)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot merge user: %w", err)
|
||||||
|
}
|
||||||
|
mergedUser.Username = user.Username
|
||||||
|
|
||||||
|
userBytes, err := json.Marshal(mergedUser)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = tx.Set(mergedUser.key(), string(userBytes), nil)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -239,35 +244,19 @@ func (store *DBStore) RemoveUser(username string) (err error) {
|
||||||
}
|
}
|
||||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||||
user := User{Username: username}
|
user := User{Username: username}
|
||||||
|
|
||||||
// Delete the main key first. This is a good indicator if the user generally exists.
|
|
||||||
if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound {
|
if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now get all attributes and delete them as well. One by one.
|
|
||||||
var keys []string
|
|
||||||
if err := tx.AscendKeys(user.prefix()+":*", func(key, value string) bool {
|
|
||||||
keys = append(keys, key)
|
|
||||||
return true
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("cannot iterate keys: %w", err)
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
if _, err := tx.Delete(key); err != nil {
|
|
||||||
return fmt.Errorf("cannot remove key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPassword(username string, password string) (string, error) {
|
func hashPassword(password string) (string, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s:%s", username, hash), nil
|
return string(hash), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ func TestStoreUserHandling(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("adding users should work", func(t *testing.T) {
|
t.Run("adding users should work", func(t *testing.T) {
|
||||||
if err := store.AddUser(User{"myuser", "mypass", GlobalRoleUser}); err != nil {
|
if err := store.AddUser(User{Username: "myuser", Password: "mypass", Role: GlobalRoleUser}); err != nil {
|
||||||
t.Errorf("cannot add user: %v", err)
|
t.Errorf("cannot add user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue