Gitfed
bastien-mrq/gitfed / internal / store / users.go
package store

import (
	"fmt"
	"time"

	bolt "go.etcd.io/bbolt"
)

// User is a local account on this instance. Deliberately has no password
// field — credentials live in a separate bucket (see auth.go) so a User
// value is always safe to hand back from ListUsers/GetUser without risking
// leaking a hash over the admin RPC wire.
type User struct {
	Username  string    `json:"username"`
	PubKeys   []string  `json:"pub_keys"` // authorized_keys-format entries
	IsAdmin   bool      `json:"is_admin"`
	CreatedAt time.Time `json:"created_at"`
	Bio       string    `json:"bio,omitempty"` // markdown, shown on the public /u/{username} profile page
}

func (s *Store) CreateUser(u User) error {
	if u.Username == "" {
		return fmt.Errorf("store: username required")
	}
	u.CreatedAt = time.Now().UTC()
	return s.db.Update(func(tx *bolt.Tx) error {
		if tx.Bucket(bucketUsers).Get([]byte(u.Username)) != nil {
			return fmt.Errorf("store: user %q already exists", u.Username)
		}
		return putJSON(tx, bucketUsers, u.Username, u)
	})
}

func (s *Store) GetUser(username string) (User, error) {
	var u User
	err := s.db.View(func(tx *bolt.Tx) error {
		return getJSON(tx, bucketUsers, username, &u)
	})
	return u, err
}

func (s *Store) ListUsers() ([]User, error) {
	var users []User
	err := s.db.View(func(tx *bolt.Tx) error {
		var err error
		users, err = listJSON[User](tx, bucketUsers)
		return err
	})
	return users, err
}

func (s *Store) DeleteUser(username string) error {
	return s.db.Update(func(tx *bolt.Tx) error {
		return deleteKey(tx, bucketUsers, username)
	})
}

// AddUserKey appends a public key (authorized_keys format) to the user.
func (s *Store) AddUserKey(username, pubKey string) error {
	return s.db.Update(func(tx *bolt.Tx) error {
		var u User
		if err := getJSON(tx, bucketUsers, username, &u); err != nil {
			return err
		}
		for _, k := range u.PubKeys {
			if k == pubKey {
				return nil
			}
		}
		u.PubKeys = append(u.PubKeys, pubKey)
		return putJSON(tx, bucketUsers, username, u)
	})
}

// RemoveUserKey removes a public key from the user. Removing every key
// would lock the user out of SSH entirely, so it refuses to remove the
// last one.
func (s *Store) RemoveUserKey(username, pubKey string) error {
	return s.db.Update(func(tx *bolt.Tx) error {
		var u User
		if err := getJSON(tx, bucketUsers, username, &u); err != nil {
			return err
		}
		kept := u.PubKeys[:0]
		for _, k := range u.PubKeys {
			if k != pubKey {
				kept = append(kept, k)
			}
		}
		if len(kept) == 0 {
			return fmt.Errorf("store: cannot remove %s's last key", username)
		}
		u.PubKeys = kept
		return putJSON(tx, bucketUsers, username, u)
	})
}

func (s *Store) SetUserAdmin(username string, isAdmin bool) error {
	return s.db.Update(func(tx *bolt.Tx) error {
		var u User
		if err := getJSON(tx, bucketUsers, username, &u); err != nil {
			return err
		}
		u.IsAdmin = isAdmin
		return putJSON(tx, bucketUsers, username, u)
	})
}

func (s *Store) SetUserBio(username, bio string) error {
	return s.db.Update(func(tx *bolt.Tx) error {
		var u User
		if err := getJSON(tx, bucketUsers, username, &u); err != nil {
			return err
		}
		u.Bio = bio
		return putJSON(tx, bucketUsers, username, u)
	})
}

// FindUserByKey returns the user owning the given authorized_keys-format key.
func (s *Store) FindUserByKey(pubKey string) (User, error) {
	var found User
	err := s.db.View(func(tx *bolt.Tx) error {
		users, err := listJSON[User](tx, bucketUsers)
		if err != nil {
			return err
		}
		for _, u := range users {
			for _, k := range u.PubKeys {
				if k == pubKey {
					found = u
					return nil
				}
			}
		}
		return ErrNotFound
	})
	return found, err
}