aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore27
-rw-r--r--Makefile12
-rw-r--r--go.mod24
-rw-r--r--go.sum74
-rw-r--r--walrss/internal/core/sessions.go40
-rw-r--r--walrss/internal/core/userError.go52
-rw-r--r--walrss/internal/core/users.go61
-rw-r--r--walrss/internal/core/util.go15
-rw-r--r--walrss/internal/core/validation.go21
-rw-r--r--walrss/internal/db/db.go39
-rw-r--r--walrss/internal/http/auth.go29
-rw-r--r--walrss/internal/http/http.go49
-rw-r--r--walrss/internal/state/state.go54
-rw-r--r--walrss/internal/urls/urls.go9
-rw-r--r--walrss/main.go55
15 files changed, 561 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..60759b8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+
+### Go ###
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+### Go Patch ###
+/vendor/
+/Godeps/
+
+### -----------------
+
+
+run/
+bin/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..60bc03c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+.PHONY: prebuild fmt
+
+build:
+ mkdir -p bin
+ go build -o bin/walrss github.com/codemicro/walrss/walrss
+
+run: build
+ mkdir -p run
+ cd run && ../bin/walrss
+
+fmt:
+ go fmt github.com/codemicro/walrss/...
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b358424
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,24 @@
+module github.com/codemicro/walrss
+
+go 1.18
+
+require (
+ github.com/andybalholm/brotli v1.0.4 // indirect
+ github.com/bwmarrin/go-alone v0.0.0-20190806015146-742bb55d1631 // indirect
+ github.com/gofiber/fiber/v2 v2.31.0 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/kkyr/fig v0.3.0 // indirect
+ github.com/klauspost/compress v1.15.0 // indirect
+ github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
+ github.com/mitchellh/mapstructure v1.4.1 // indirect
+ github.com/pelletier/go-toml v1.9.3 // indirect
+ github.com/rs/zerolog v1.26.1 // indirect
+ github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.34.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ go.etcd.io/bbolt v1.3.6 // indirect
+ golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
+ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c9bfcf0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,74 @@
+github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
+github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/bwmarrin/go-alone v0.0.0-20190806015146-742bb55d1631 h1:Xb5rra6jJt5Z1JsZhIMby+IP5T8aU+Uc2RC9RzSxs9g=
+github.com/bwmarrin/go-alone v0.0.0-20190806015146-742bb55d1631/go.mod h1:P86Dksd9km5HGX5UMIocXvX87sEp2xUARle3by+9JZ4=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofiber/fiber/v2 v2.31.0 h1:M2rWPQbD5fDVAjcoOLjKRXTIlHesI5Eq7I5FEQPt4Ow=
+github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kkyr/fig v0.3.0 h1:5bd1amYKp/gsK2bGEUJYzcCrQPKOZp6HZD9K21v9Guo=
+github.com/kkyr/fig v0.3.0/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
+github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
+github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
+github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
+github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
+github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a h1:oIi7H/bwFUYKYhzKbHc+3MvHRWqhQwXVB4LweLMiVy0=
+github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a/go.mod h1:iSvujNDmpZ6eQX+bg/0X3lF7LEmZ8N77g2a/J/+Zt2U=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
+github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
+go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/walrss/internal/core/sessions.go b/walrss/internal/core/sessions.go
new file mode 100644
index 0000000..8e1ab73
--- /dev/null
+++ b/walrss/internal/core/sessions.go
@@ -0,0 +1,40 @@
+package core
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ goalone "github.com/bwmarrin/go-alone"
+ "time"
+)
+
+var (
+ sessionSigner *goalone.Sword
+ sessionSalt = []byte("session")
+)
+
+func init() {
+ sessionSecret := make([]byte, 50)
+ if _, err := rand.Read(sessionSecret); err != nil {
+ panic(err)
+ }
+ sessionSigner = goalone.New(sessionSecret, goalone.Timestamp)
+}
+
+func GenerateSessionToken(userID string) string {
+ combined := combineStringAndSalt(userID, sessionSalt)
+ return hex.EncodeToString(sessionSigner.Sign(combined))
+}
+
+func ValidateSessionToken(input string) (string, time.Time, error) {
+ signed, err := hex.DecodeString(input)
+ if err != nil {
+ return "", time.Time{}, err
+ }
+
+ if _, err := sessionSigner.Unsign(signed); err != nil {
+ return "", time.Time{}, AsUserError(400, err)
+ }
+
+ parsed := sessionSigner.Parse(signed)
+ return string(parsed.Payload), parsed.Timestamp, nil
+}
diff --git a/walrss/internal/core/userError.go b/walrss/internal/core/userError.go
new file mode 100644
index 0000000..4938466
--- /dev/null
+++ b/walrss/internal/core/userError.go
@@ -0,0 +1,52 @@
+package core
+
+import "fmt"
+
+var ErrNotFound = NewUserErrorWithStatus(404, "item not found")
+
+type UserError struct {
+ Original error
+ Status int
+}
+
+func (ue *UserError) Error() string {
+ return ue.Original.Error()
+}
+
+func (ue *UserError) Unwrap() error {
+ return ue.Original
+}
+
+func AsUserError(status int, err error) error {
+ return &UserError{
+ Original: err,
+ Status: status,
+ }
+}
+
+func NewUserError(format string, args ...any) error {
+ return NewUserErrorWithStatus(400, format, args...)
+}
+
+func NewUserErrorWithStatus(status int, format string, args ...any) error {
+ return &UserError{
+ Original: fmt.Errorf(format, args...),
+ Status: status,
+ }
+}
+
+func IsUserError(err error) bool {
+ if err == nil {
+ return false
+ }
+ _, ok := err.(*UserError)
+ return ok
+}
+
+func GetUserErrorStatus(err error) int {
+ ue, ok := err.(*UserError)
+ if !ok {
+ return 0
+ }
+ return ue.Status
+}
diff --git a/walrss/internal/core/users.go b/walrss/internal/core/users.go
new file mode 100644
index 0000000..f909a1d
--- /dev/null
+++ b/walrss/internal/core/users.go
@@ -0,0 +1,61 @@
+package core
+
+import (
+ "errors"
+ "github.com/codemicro/walrss/walrss/internal/db"
+ "github.com/codemicro/walrss/walrss/internal/state"
+ "github.com/lithammer/shortuuid/v4"
+ bh "github.com/timshannon/bolthold"
+ "golang.org/x/crypto/bcrypt"
+)
+
+func RegisterUser(st *state.State, email, password string) (*db.User, error) {
+ if err := validateEmailAddress(email); err != nil {
+ return nil, err
+ }
+
+ if err := validatePassword(password); err != nil {
+ return nil, err
+ }
+
+ u := &db.User{
+ ID: shortuuid.New(),
+ Email: email,
+ Salt: generateRandomData(30),
+ }
+
+ hash, err := bcrypt.GenerateFromPassword(combineStringAndSalt(password, u.Salt), bcrypt.DefaultCost)
+ if err != nil {
+ return nil, err
+ }
+
+ u.Password = hash
+
+ if err := st.Data.Insert(bh.Key, u); err != nil {
+ if errors.Is(err, bh.ErrUniqueExists) {
+ return nil, NewUserError("email address in use")
+ }
+ return nil, err
+ }
+
+ return u, nil
+}
+
+func AreUserCredentialsCorrect(st *state.State, email, password string) (bool, error) {
+ user := new(db.User)
+ if err := st.Data.FindOne(user, bh.Where("Email").Eq(email)); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return false, ErrNotFound
+ }
+ return false, err
+ }
+
+ if err := bcrypt.CompareHashAndPassword(user.Password, combineStringAndSalt(password, user.Salt)); err != nil {
+ if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
+ return false, nil
+ }
+ return false, err
+ }
+
+ return true, nil
+}
diff --git a/walrss/internal/core/util.go b/walrss/internal/core/util.go
new file mode 100644
index 0000000..f92d365
--- /dev/null
+++ b/walrss/internal/core/util.go
@@ -0,0 +1,15 @@
+package core
+
+import (
+ "crypto/rand"
+)
+
+func generateRandomData(n int) []byte {
+ bytes := make([]byte, n)
+ _, _ = rand.Read(bytes)
+ return bytes
+}
+
+func combineStringAndSalt(password string, salt []byte) []byte {
+ return append([]byte(password), salt...)
+}
diff --git a/walrss/internal/core/validation.go b/walrss/internal/core/validation.go
new file mode 100644
index 0000000..501c023
--- /dev/null
+++ b/walrss/internal/core/validation.go
@@ -0,0 +1,21 @@
+package core
+
+import (
+ "regexp"
+)
+
+var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
+
+func validateEmailAddress(email string) error {
+ if !emailRegexp.MatchString(email) {
+ return NewUserError("invalid email address")
+ }
+ return nil
+}
+
+func validatePassword(password string) error {
+ if len(password) <= 3 {
+ return NewUserError("password must be at least three characters long")
+ }
+ return nil
+}
diff --git a/walrss/internal/db/db.go b/walrss/internal/db/db.go
new file mode 100644
index 0000000..efb3f46
--- /dev/null
+++ b/walrss/internal/db/db.go
@@ -0,0 +1,39 @@
+package db
+
+import (
+ bh "github.com/timshannon/bolthold"
+)
+
+func New(filename string) (*bh.Store, error) {
+ store, err := bh.Open(filename, 0644, nil)
+ if err != nil {
+ return nil, err
+ }
+ return store, nil
+}
+
+type User struct {
+ ID string `boldholdKey:""`
+ Email string `boltholdUnique:"UniqueEmail" boltholdIndex:"Email"`
+ Password []byte
+ Salt []byte
+
+ Schedule struct {
+ Day SendDay `boltholdIndex:"Day"`
+ Hour int `boltholdIndex:"Hour"`
+ }
+}
+
+type SendDay uint32
+
+const (
+ SendDayNever = iota
+ SendDaily
+ SendOnMonday
+ SendOnTuesday
+ SendOnWednesday
+ SendOnThursday
+ SendOnFriday
+ SendOnSaturday
+ SendOnSunday
+)
diff --git a/walrss/internal/http/auth.go b/walrss/internal/http/auth.go
new file mode 100644
index 0000000..7be4078
--- /dev/null
+++ b/walrss/internal/http/auth.go
@@ -0,0 +1,29 @@
+package http
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/gofiber/fiber/v2"
+ "time"
+)
+
+func (s *Server) authRegister(ctx *fiber.Ctx) error {
+ user, err := core.RegisterUser(s.state,
+ ctx.FormValue("email"),
+ ctx.FormValue("password"),
+ )
+ if err != nil {
+ return err
+ }
+
+ token := core.GenerateSessionToken(user.ID)
+
+ ctx.Cookie(&fiber.Cookie{
+ Name: sessionCookieKey,
+ Value: token,
+ Expires: time.Now().UTC().Add(sessionDuration),
+ Secure: !s.state.Config.Debug,
+ HTTPOnly: true,
+ })
+
+ return ctx.SendString("ok!")
+}
diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go
new file mode 100644
index 0000000..5af260e
--- /dev/null
+++ b/walrss/internal/http/http.go
@@ -0,0 +1,49 @@
+package http
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/state"
+ "github.com/codemicro/walrss/walrss/internal/urls"
+ "github.com/gofiber/fiber/v2"
+ "time"
+)
+
+const (
+ sessionCookieKey = "walrss-session"
+ sessionDuration = (time.Hour * 24) * 7 // 7 days
+)
+
+type Server struct {
+ state *state.State
+ app *fiber.App
+}
+
+func New(st *state.State) (*Server, error) {
+ app := fiber.New(fiber.Config{
+ DisableStartupMessage: !st.Config.Debug,
+ AppName: "Walrss",
+ })
+ // TODO: Add error handler with UserError support
+
+ s := &Server{
+ state: st,
+ app: app,
+ }
+
+ s.registerHandlers()
+
+ return s, nil
+}
+
+func (s *Server) registerHandlers() {
+ s.app.Post(urls.AuthRegister, s.authRegister)
+}
+
+func (s *Server) Run() error {
+ return s.app.Listen(s.state.Config.GetHTTPAddress())
+}
+
+func UserErrorToResponse(ctx *fiber.Ctx, ue core.UserError) error {
+ ctx.Status(ue.Status)
+ return ctx.SendString(ue.Error())
+}
diff --git a/walrss/internal/state/state.go b/walrss/internal/state/state.go
new file mode 100644
index 0000000..54badd7
--- /dev/null
+++ b/walrss/internal/state/state.go
@@ -0,0 +1,54 @@
+package state
+
+import (
+ "errors"
+ "fmt"
+ "github.com/kkyr/fig"
+ bh "github.com/timshannon/bolthold"
+ "io/ioutil"
+ "os"
+)
+
+type State struct {
+ Config *Config
+ Data *bh.Store
+}
+
+func New() *State {
+ return &State{}
+}
+
+type Config struct {
+ Server struct {
+ Host string `fig:"host" default:"127.0.0.1"`
+ Port int `fig:"port" default:"8080"`
+ }
+ DataDirectory string `fig:"dataDir" default:"./"`
+ Debug bool `fig:"debug"`
+}
+
+const configFilename = "config.yaml"
+
+func LoadConfig() (*Config, error) {
+ // If the file doesn't exist, Fig will throw a hissy fit, so we should create a blank one if it doesn't exist
+ if _, err := os.Stat(configFilename); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ // If the file doesn't have contents, Fig will throw an EOF, despite `touch config.yaml` working fine. idk lol
+ if err := ioutil.WriteFile(configFilename, []byte("{}"), 0777); err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, err
+ }
+ }
+
+ cfg := new(Config)
+ if err := fig.Load(cfg); err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
+
+func (cfg *Config) GetHTTPAddress() string {
+ return fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
+}
diff --git a/walrss/internal/urls/urls.go b/walrss/internal/urls/urls.go
new file mode 100644
index 0000000..1d38f96
--- /dev/null
+++ b/walrss/internal/urls/urls.go
@@ -0,0 +1,9 @@
+package urls
+
+const (
+ Index = "/"
+
+ Auth = "/auth"
+ AuthSignIn = Auth + "/signin"
+ AuthRegister = Auth + "/register"
+) \ No newline at end of file
diff --git a/walrss/main.go b/walrss/main.go
new file mode 100644
index 0000000..9cb951d
--- /dev/null
+++ b/walrss/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/db"
+ "github.com/codemicro/walrss/walrss/internal/http"
+ "github.com/codemicro/walrss/walrss/internal/state"
+ "github.com/rs/zerolog/log"
+ "os"
+)
+
+const dbFilename = "walrss.db"
+const walrssDirectoryEnv = "WALRSS_DIR"
+
+func run() error {
+ if err := switchToDataDirectory(); err != nil {
+ return err
+ }
+
+ st := state.New()
+ if config, err := state.LoadConfig(); err != nil {
+ return err
+ } else {
+ st.Config = config
+ }
+
+ if err := os.Chdir(st.Config.DataDirectory); err != nil {
+ return err
+ }
+
+ store, err := db.New(dbFilename)
+ if err != nil {
+ return err
+ }
+ st.Data = store
+
+ server, err := http.New(st)
+ if err != nil {
+ return err
+ }
+
+ return server.Run()
+}
+
+func main() {
+ if err := run(); err != nil {
+ log.Fatal().Err(err).Msg("could not start")
+ }
+}
+
+func switchToDataDirectory() error {
+ if dir := os.Getenv(walrssDirectoryEnv); dir != "" {
+ return os.Chdir(dir)
+ }
+ return nil
+}