generated from templates/go-generic
Compare commits
No commits in common. "create-user" and "main" have entirely different histories.
create-use
...
main
7 changed files with 3 additions and 359 deletions
4
go.mod
4
go.mod
|
@ -1,7 +1,3 @@
|
|||
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
|
||||
|
|
14
go.sum
14
go.sum
|
@ -1,14 +0,0 @@
|
|||
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=
|
|
@ -8,14 +8,9 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
BindAddress string `json:"bindAddress"`
|
||||
Port int32 `json:"port"`
|
||||
Domain string `json:"domain"`
|
||||
Database Database `json:"database"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Path string `json:"path"`
|
||||
BindAddress string `json:"bindAddress"`
|
||||
Port int32 `json:"port"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func NewConfig(path string) (Config, error) {
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
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)}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package utilities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
httpScheme = "http://"
|
||||
httpsScheme = "https://"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingHostname = errors.New("the hostname is missing from the URL")
|
||||
ErrHostIsIPAddress = errors.New("the hostname is an IP address")
|
||||
ErrInvalidURLScheme = errors.New("invalid URL scheme")
|
||||
ErrURLContainsFragment = errors.New("the URL contains a fragment")
|
||||
ErrURLContainsPort = errors.New("the URL contains a port")
|
||||
)
|
||||
|
||||
// ValidateProfileURL validates the given profile URL according to the indieauth
|
||||
// specification. ValidateProfileURL returns the canonicalised profile URL after
|
||||
// validation checks.
|
||||
func ValidateProfileURL(profileURL string) (string, error) {
|
||||
// Using regex to get and validate the scheme.
|
||||
// If its missing then set the scheme to https
|
||||
pattern := regexp.MustCompile(`^[a-z].*:\/\/|^[a-z].*:`)
|
||||
scheme := pattern.FindString(profileURL)
|
||||
|
||||
if scheme == "" {
|
||||
profileURL = httpsScheme + profileURL
|
||||
} else if scheme != httpsScheme && scheme != httpScheme {
|
||||
return "", ErrInvalidURLScheme
|
||||
}
|
||||
|
||||
parsedProfileURL, err := url.Parse(profileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse the URL %q: %w", profileURL, err)
|
||||
}
|
||||
|
||||
if parsedProfileURL.Hostname() == "" {
|
||||
return "", ErrMissingHostname
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(parsedProfileURL.Hostname()); ip != nil {
|
||||
return "", ErrHostIsIPAddress
|
||||
}
|
||||
|
||||
if parsedProfileURL.Fragment != "" {
|
||||
return "", ErrURLContainsFragment
|
||||
}
|
||||
|
||||
if parsedProfileURL.Port() != "" {
|
||||
return "", ErrURLContainsPort
|
||||
}
|
||||
|
||||
if parsedProfileURL.Scheme == "" {
|
||||
parsedProfileURL.Scheme = "https"
|
||||
}
|
||||
|
||||
if parsedProfileURL.Path == "" {
|
||||
parsedProfileURL.Path = "/"
|
||||
}
|
||||
|
||||
return parsedProfileURL.String(), nil
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
package utilities_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/indieauth-server/internal/utilities"
|
||||
)
|
||||
|
||||
func TestValidateProfileURL(t *testing.T) {
|
||||
validProfileURLTestCases := []struct {
|
||||
name string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Canonicalised URL",
|
||||
url: "https://barry.example.org/",
|
||||
want: "https://barry.example.org/",
|
||||
},
|
||||
{
|
||||
name: "Canonicalised URL with path",
|
||||
url: "https://example.org/username/barry",
|
||||
want: "https://example.org/username/barry",
|
||||
},
|
||||
{
|
||||
name: "Canonicalised URL with query string",
|
||||
url: "http://example.org/users?id=1001",
|
||||
want: "http://example.org/users?id=1001",
|
||||
},
|
||||
{
|
||||
name: "Non-canonicalised URL with missing scheme",
|
||||
url: "barry.example.org/",
|
||||
want: "https://barry.example.org/",
|
||||
},
|
||||
{
|
||||
name: "Non-canonicalised URL with missing path",
|
||||
url: "http://barry.example.org",
|
||||
want: "http://barry.example.org/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ta := range slices.All(validProfileURLTestCases) {
|
||||
t.Run(ta.name, testValidProfileURLs(ta.name, ta.url, ta.want))
|
||||
}
|
||||
|
||||
invalidProfileURLTestCases := []struct {
|
||||
name string
|
||||
url string
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "URL using the mailto scheme",
|
||||
url: "mailto:barry@example.org",
|
||||
wantError: utilities.ErrInvalidURLScheme,
|
||||
},
|
||||
{
|
||||
name: "URL using a non-http scheme",
|
||||
url: "postgres://db_user:db_password@some_db_server:5432/db",
|
||||
wantError: utilities.ErrInvalidURLScheme,
|
||||
},
|
||||
{
|
||||
name: "URL containing a port",
|
||||
url: "http://barry.example.org:80/",
|
||||
wantError: utilities.ErrURLContainsPort,
|
||||
},
|
||||
{
|
||||
name: "URL containing a fragment",
|
||||
url: "https://barry.example.org/#fragment",
|
||||
wantError: utilities.ErrURLContainsFragment,
|
||||
},
|
||||
{
|
||||
name: "URL host is an IP address",
|
||||
url: "https://192.168.82.56/",
|
||||
wantError: utilities.ErrHostIsIPAddress,
|
||||
},
|
||||
{
|
||||
name: "URL with a missing host",
|
||||
url: "https:///",
|
||||
wantError: utilities.ErrMissingHostname,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tb := range slices.All(invalidProfileURLTestCases) {
|
||||
t.Run(tb.name, testInvalidProfileURL(tb.name, tb.url, tb.wantError))
|
||||
}
|
||||
}
|
||||
|
||||
func testValidProfileURLs(testName, url, wantURL string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
canonicalisedURL, err := utilities.ValidateProfileURL(url)
|
||||
if err != nil {
|
||||
t.Fatalf("FAILED test %q: %v", testName, err)
|
||||
}
|
||||
|
||||
if canonicalisedURL != wantURL {
|
||||
t.Errorf("FAILED test %q: want %s, got %s", testName, wantURL, canonicalisedURL)
|
||||
} else {
|
||||
t.Logf("PASSED test %q: got %s", testName, canonicalisedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInvalidProfileURL(testName, url string, wantError error) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if _, err := utilities.ValidateProfileURL(url); err == nil {
|
||||
t.Errorf(
|
||||
"FAILED test %q: The expected error was not received using invalid profile URL %q",
|
||||
testName,
|
||||
url,
|
||||
)
|
||||
} else {
|
||||
if !errors.Is(err, wantError) {
|
||||
t.Errorf(
|
||||
"FAILED test %q: Unexpected error received using profile URL %q: got %q",
|
||||
testName,
|
||||
url,
|
||||
err.Error(),
|
||||
)
|
||||
} else {
|
||||
t.Logf(
|
||||
"PASSED test %q: Expected error received using profile URL %q: got %q",
|
||||
testName,
|
||||
url,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue