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
}