Create users package
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
36
users/user.go
Normal file
36
users/user.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package users
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleUnset Role = ""
|
||||
RoleUser Role = "user"
|
||||
RoleAdmin Role = "admin"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
HashedPassword []byte `json:"hashed_password"`
|
||||
Roles []Role `json:"roles"`
|
||||
}
|
||||
|
||||
type UserStore interface {
|
||||
Get(username string) (*User, error)
|
||||
Store(user *User) error
|
||||
Delete(username string) error
|
||||
}
|
||||
|
||||
func (u *User) ValidatePassword(password string) error {
|
||||
return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(password))
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.HashedPassword = hashed
|
||||
return nil
|
||||
}
|
37
users/user_test.go
Normal file
37
users/user_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package users_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"git.t-juice.club/torjus/gpaste/users"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
t.Run("Password", func(t *testing.T) {
|
||||
userMap := make(map[string]string)
|
||||
for i := 0; i < 10; i++ {
|
||||
userMap[randomString(8)] = randomString(16)
|
||||
}
|
||||
|
||||
for username, password := range userMap {
|
||||
user := &users.User{Username: username}
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
t.Fatalf("Error setting password: %s", err)
|
||||
}
|
||||
if err := user.ValidatePassword(password); err != nil {
|
||||
t.Fatalf("Error validating password: %s", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstabcdefghijklmnopqrstuvwxyz" +
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
69
users/userstore_bolt.go
Normal file
69
users/userstore_bolt.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var keyUsers = []byte("users")
|
||||
|
||||
type BoltUserStore struct {
|
||||
db *bbolt.DB
|
||||
}
|
||||
|
||||
func NewBoltUserStore(path string) (*BoltUserStore, error) {
|
||||
db, err := bbolt.Open(path, 0666, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Update(func(tx *bbolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists(keyUsers)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BoltUserStore{db: db}, nil
|
||||
}
|
||||
|
||||
func (s *BoltUserStore) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *BoltUserStore) Get(username string) (*User, error) {
|
||||
var user User
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(keyUsers)
|
||||
rawUser := bkt.Get([]byte(username))
|
||||
if err := json.Unmarshal(rawUser, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *BoltUserStore) Store(user *User) error {
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(keyUsers)
|
||||
|
||||
data, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bkt.Put([]byte(user.Username), data)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BoltUserStore) Delete(username string) error {
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(keyUsers)
|
||||
return bkt.Delete([]byte(username))
|
||||
})
|
||||
}
|
27
users/userstore_bolt_test.go
Normal file
27
users/userstore_bolt_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package users_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.t-juice.club/torjus/gpaste/users"
|
||||
)
|
||||
|
||||
func TestBoltUserStore(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newFunc := func() (func(), users.UserStore) {
|
||||
tmpFile := filepath.Join(tmpDir, randomString(8))
|
||||
|
||||
store, err := users.NewBoltUserStore(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating store: %s", err)
|
||||
}
|
||||
cleanup := func() {
|
||||
store.Close()
|
||||
}
|
||||
return cleanup, store
|
||||
}
|
||||
|
||||
RunUserStoreTest(newFunc, t)
|
||||
|
||||
}
|
39
users/userstore_memory.go
Normal file
39
users/userstore_memory.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MemoryUserStore struct {
|
||||
users map[string]*User
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewMemoryUserStore() *MemoryUserStore {
|
||||
return &MemoryUserStore{users: make(map[string]*User)}
|
||||
}
|
||||
func (s *MemoryUserStore) Get(username string) (*User, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
user, ok := s.users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no such user: %s", username)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *MemoryUserStore) Store(user *User) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.users[user.Username] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryUserStore) Delete(username string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
delete(s.users, username)
|
||||
return nil
|
||||
}
|
15
users/userstore_memory_test.go
Normal file
15
users/userstore_memory_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package users_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.t-juice.club/torjus/gpaste/users"
|
||||
)
|
||||
|
||||
func TestMemoryUserStore(t *testing.T) {
|
||||
newFunc := func() (func(), users.UserStore) {
|
||||
return func() {}, users.NewMemoryUserStore()
|
||||
}
|
||||
|
||||
RunUserStoreTest(newFunc, t)
|
||||
}
|
51
users/userstore_test.go
Normal file
51
users/userstore_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package users_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.t-juice.club/torjus/gpaste/users"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func RunUserStoreTest(newFunc func() (func(), users.UserStore), t *testing.T) {
|
||||
t.Run("Basics", func(t *testing.T) {
|
||||
cleanup, s := newFunc()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
userMap := make(map[string]*users.User)
|
||||
passwordMap := make(map[string]string)
|
||||
for i := 0; i < 10; i++ {
|
||||
username := randomString(8)
|
||||
password := randomString(16)
|
||||
passwordMap[username] = password
|
||||
user := &users.User{
|
||||
Username: username,
|
||||
Roles: []users.Role{users.RoleAdmin},
|
||||
}
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
t.Fatalf("Error setting password: %s", err)
|
||||
}
|
||||
userMap[username] = user
|
||||
}
|
||||
|
||||
for _, user := range userMap {
|
||||
if err := s.Store(user); err != nil {
|
||||
t.Fatalf("Error storing user: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for k := range userMap {
|
||||
user, err := s.Get(k)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting user: %s", err)
|
||||
}
|
||||
if err := user.ValidatePassword(passwordMap[user.Username]); err != nil {
|
||||
t.Errorf("Error verifying password: %s", err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(user, userMap[k]) {
|
||||
t.Errorf("User mismatch: %s", cmp.Diff(user, userMap[k]))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user