diff --git a/go.mod b/go.mod index df0e4ee..d2ee8de 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module codeflow.dananglin.me.uk/apollo/indieauth-server go 1.23.2 + +require go.etcd.io/bbolt v1.3.11 + +require golang.org/x/sys v0.4.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1ff72eb --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 8684b00..040da03 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,9 +8,14 @@ import ( ) type Config struct { - BindAddress string `json:"bindAddress"` - Port int32 `json:"port"` - Domain string `json:"domain"` + BindAddress string `json:"bindAddress"` + Port int32 `json:"port"` + Domain string `json:"domain"` + Database Database `json:"database"` +} + +type Database struct { + Path string `json:"path"` } func NewConfig(path string) (Config, error) { diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..7e1ac57 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,72 @@ +package database + +import ( + "fmt" + "os" + "path/filepath" + "time" + + bolt "go.etcd.io/bbolt" +) + +const ( + usersBucket string = "users" +) + +func New(path string) (*bolt.DB, error) { + dir := filepath.Dir(path) + + if err := os.MkdirAll(dir, 0o700); err != nil { + return nil, fmt.Errorf("unable to create directory %q: %w", dir, err) + } + + opts := bolt.Options{ + Timeout: 1 * time.Second, + } + + boltdb, err := bolt.Open(path, 0o600, &opts) + if err != nil { + return nil, fmt.Errorf( + "unable to open the database at %q: %w", + path, + err, + ) + } + + if err := ensureBuckets(boltdb); err != nil { + return nil, fmt.Errorf( + "unable to ensure that the required buckets are present in the database: %w", + err, + ) + } + + return boltdb, nil +} + +func ensureBuckets(boltdb *bolt.DB) error { + err := boltdb.Update(func(tx *bolt.Tx) error { + for _, bucket := range getBuckets() { + if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { + return fmt.Errorf( + "unable to ensure the existence of the %q bucket: %w", + string(bucket), + err, + ) + } + } + + return nil + }) + if err != nil { + return fmt.Errorf( + "error ensuring the existence of the buckets in the database: %w", + err, + ) + } + + return nil +} + +func getBuckets() [][]byte { + return [][]byte{[]byte(usersBucket)} +} diff --git a/internal/database/users.go b/internal/database/users.go new file mode 100644 index 0000000..0d477ce --- /dev/null +++ b/internal/database/users.go @@ -0,0 +1,61 @@ +package database + +import ( + "bytes" + "encoding/gob" + "fmt" + "time" + + bolt "go.etcd.io/bbolt" +) + +type User struct { + CreatedAt time.Time + UpdatedAt time.Time + HashedPassword string + Profile Profile +} + +type Profile struct { + Name string + URL string + Photo string + Email string +} + +func UpdateUser(boltdb *bolt.DB, identifier string, user User) error { + bucketName := []byte(usersBucket) + + err := boltdb.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket(bucketName) + + if bucket == nil { + return fmt.Errorf("the %s bucket does not exist", string(bucketName)) + } + + key := []byte(identifier) + + buffer := new(bytes.Buffer) + if err := gob.NewEncoder(buffer).Encode(user); err != nil { + return fmt.Errorf( + "unable to encode the user data: %w", + err, + ) + } + + if err := bucket.Put(key, buffer.Bytes()); err != nil { + return fmt.Errorf( + "unable to update the user in the %s bucket: %w", + string(bucketName), + err, + ) + } + + return nil + }) + if err != nil { + return fmt.Errorf("error updating the user in the database: %w", err) + } + + return nil +}