diff --git a/go.mod b/go.mod index 4d7dca9d..9c4b4492 100644 --- a/go.mod +++ b/go.mod @@ -1,48 +1,51 @@ module github.com/lukewhrit/spacebin -go 1.22.4 +go 1.23 + +toolchain go1.23.3 require ( github.com/caarlos0/env/v9 v9.0.0 - github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.14.1 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/lib/pq v1.10.9 github.com/lukewhrit/phrase v1.0.0 github.com/rs/zerolog v1.33.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/tools v0.26.0 // indirect modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/strutil v1.2.0 // indirect - modernc.org/token v1.1.0 // indirect ) require ( - github.com/alecthomas/chroma/v2 v2.14.0 + github.com/alecthomas/chroma/v2 v2.15.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-sql-driver/mysql v1.8.1 + github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/crypto v0.33.0 + golang.org/x/sys v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/sqlite v1.32.0 + modernc.org/sqlite v1.34.5 ) diff --git a/go.sum b/go.sum index 4146f655..ac3ea92b 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -18,12 +18,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs= @@ -33,12 +33,12 @@ github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRi github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 h1:5lyLWsV+qCkoYqsKUDuycESh9DEIPVKN6iCFeL7ag50= +github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -73,21 +73,23 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -102,8 +104,6 @@ modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= @@ -114,8 +114,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= -modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/database/database.go b/internal/database/database.go index 6f2cd50e..0b4cec08 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -30,6 +30,19 @@ type Document struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +type Account struct { + ID int `db:"id" json:"id"` + Username string `db:"username" json:"username"` + Password string `db:"password" json:"password"` + // Documents []Document `db:"documents" json:"documents"` +} + +type Session struct { + Public string `db:"public" json:"public"` + Token string `db:"token" json:"token"` + Secret string `db:"secret" json:"secret"` +} + //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Database type Database interface { Migrate(ctx context.Context) error @@ -37,4 +50,13 @@ type Database interface { GetDocument(ctx context.Context, id string) (Document, error) CreateDocument(ctx context.Context, id, content string) error + + GetAccount(ctx context.Context, id string) (Account, error) + GetAccountByUsername(ctx context.Context, username string) (Account, error) + CreateAccount(ctx context.Context, username, password string) error + // UpdateAccount(ctx context.Context, id, username, password string) error + DeleteAccount(ctx context.Context, id string) error + + GetSession(ctx context.Context, id string) (Session, error) + CreateSession(ctx context.Context, public, token, secret string) error } diff --git a/internal/database/database_mysql.go b/internal/database/database_mysql.go index b97392af..fb332fa9 100644 --- a/internal/database/database_mysql.go +++ b/internal/database/database_mysql.go @@ -24,6 +24,7 @@ import ( "time" _ "github.com/go-sql-driver/mysql" + "github.com/lukewhrit/spacebin/internal/util" ) type MySQL struct { @@ -44,11 +45,28 @@ func NewMySQL(uri *url.URL) (Database, error) { func (m *MySQL) Migrate(ctx context.Context) error { _, err := m.Exec(` CREATE TABLE IF NOT EXISTS documents ( - id VARCHAR(255) PRIMARY KEY, + id VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -)`) + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS accounts ( + id INT NOT NULL AUTO_INCREMENT, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + + PRIMARY_KEY(id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + public VARCHAR(255) NOT NULL, + token VARCHAR(255) NOT NULL, + secret TEXT NOT NULL, + PRIMARY_KEY(public) +);`) return err } @@ -77,3 +95,79 @@ func (m *MySQL) CreateDocument(ctx context.Context, id, content string) error { return tx.Commit() } + +func (m *MySQL) GetAccount(ctx context.Context, id string) (Account, error) { + acc := new(Account) + row := m.QueryRow("SELECT * FROM accounts WHERE id=?", id) + err := row.Scan(&acc.ID, &acc.Username, &acc.Password) + + return *acc, err +} + +func (m *MySQL) GetAccountByUsername(ctx context.Context, username string) (Account, error) { + account := new(Account) + row := m.QueryRow("SELECT * FROM accounts WHERE username=$1", username) + err := row.Scan(&account.ID, &account.Username, &account.Password) + + return *account, err +} + +func (m *MySQL) CreateAccount(ctx context.Context, username, password string) error { + tx, err := m.Begin() + + if err != nil { + return err + } + + // Add account to database + // Hash and salt the password + _, err = tx.Exec("INSERT INTO accounts (username, password) VALUES ($1, $2)", + username, util.HashAndSalt([]byte(password))) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (m *MySQL) DeleteAccount(ctx context.Context, id string) error { + tx, err := m.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("DELETE FROM accounts WHERE id=$1", id) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (m *MySQL) GetSession(ctx context.Context, id string) (Session, error) { + session := new(Session) + row := m.QueryRow("SELECT * FROM sessions WHERE id=$1", id) + err := row.Scan(&session.Public, &session.Token, &session.Secret) + + return *session, err +} + +func (m *MySQL) CreateSession(ctx context.Context, public, token, secret string) error { + tx, err := m.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO sessions (public, token, secret) VALUES ($1, $2, $3)", + public, token, secret) + + if err != nil { + return err + } + + return tx.Commit() +} diff --git a/internal/database/database_pg.go b/internal/database/database_pg.go index 83ed2fd6..27b5cabd 100644 --- a/internal/database/database_pg.go +++ b/internal/database/database_pg.go @@ -22,6 +22,7 @@ import ( "net/url" _ "github.com/lib/pq" + "github.com/lukewhrit/spacebin/internal/util" ) type Postgres struct { @@ -41,7 +42,19 @@ CREATE TABLE IF NOT EXISTS documents ( content text NOT NULL, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now() -)`) +); + +CREATE TABLE IF NOT EXISTS accounts ( + id SERIAL PRIMARY KEY, + username varchar(255) NOT NULL, + password varchar(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions ( + public varchar(255) PRIMARY KEY, + token varchar(255) NOT NULL, + secret varchar +);`) return err } @@ -70,3 +83,79 @@ func (p *Postgres) CreateDocument(ctx context.Context, id, content string) error return tx.Commit() } + +func (p *Postgres) GetAccount(ctx context.Context, id string) (Account, error) { + account := new(Account) + row := p.QueryRow("SELECT * FROM accounts WHERE id=$1", id) + err := row.Scan(&account.ID, &account.Username, &account.Password) + + return *account, err +} + +func (p *Postgres) GetAccountByUsername(ctx context.Context, username string) (Account, error) { + account := new(Account) + row := p.QueryRow("SELECT * FROM accounts WHERE username=$1", username) + err := row.Scan(&account.ID, &account.Username, &account.Password) + + return *account, err +} + +func (p *Postgres) CreateAccount(ctx context.Context, username, password string) error { + tx, err := p.Begin() + + if err != nil { + return err + } + + // Add account to database + // Hash and salt the password + _, err = tx.Exec("INSERT INTO accounts (username, password) VALUES ($1, $2)", + username, util.HashAndSalt([]byte(password))) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (p *Postgres) DeleteAccount(ctx context.Context, id string) error { + tx, err := p.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("DELETE FROM accounts WHERE id=$1", id) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (p *Postgres) GetSession(ctx context.Context, id string) (Session, error) { + session := new(Session) + row := p.QueryRow("SELECT * FROM sessions WHERE id=$1", id) + err := row.Scan(&session.Public, &session.Token, &session.Secret) + + return *session, err +} + +func (p *Postgres) CreateSession(ctx context.Context, public, token, secret string) error { + tx, err := p.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO sessions (public, token, secret) VALUES ($1, $2, $3)", + public, token, secret) + + if err != nil { + return err + } + + return tx.Commit() +} diff --git a/internal/database/database_sqlite.go b/internal/database/database_sqlite.go index 288bbcf9..82a502d8 100644 --- a/internal/database/database_sqlite.go +++ b/internal/database/database_sqlite.go @@ -22,6 +22,7 @@ import ( "net/url" "sync" + "github.com/lukewhrit/spacebin/internal/util" _ "modernc.org/sqlite" ) @@ -42,7 +43,19 @@ CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - usdated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions ( + public TEXT PRIMARY KEY, + token TEXT NOT NULL, + secret TEXT NOT NULL );`) return err @@ -78,3 +91,94 @@ func (s *SQLite) CreateDocument(ctx context.Context, id, content string) error { return tx.Commit() } + +func (s *SQLite) GetAccount(ctx context.Context, id string) (Account, error) { + s.RLock() + defer s.RUnlock() + + acc := new(Account) + row := s.QueryRow("SELECT * FROM accounts WHERE id=$1", id) + err := row.Scan(&acc.ID, &acc.Username, &acc.Password) + + return *acc, err +} + +func (s *SQLite) GetAccountByUsername(ctx context.Context, username string) (Account, error) { + account := new(Account) + row := s.QueryRow("SELECT * FROM accounts WHERE username=$1", username) + err := row.Scan(&account.ID, &account.Username, &account.Password) + + return *account, err +} + +func (s *SQLite) CreateAccount(ctx context.Context, username, password string) error { + s.Lock() + defer s.Unlock() + + tx, err := s.Begin() + + if err != nil { + return err + } + + // Add account to database + // Hash and salt the password + _, err = tx.Exec("INSERT INTO accounts (username, password) VALUES ($1, $2)", + username, util.HashAndSalt([]byte(password))) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (s *SQLite) DeleteAccount(ctx context.Context, id string) error { + s.Lock() + defer s.Unlock() + + tx, err := s.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("DELETE FROM accounts WHERE id=$1", id) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (s *SQLite) GetSession(ctx context.Context, id string) (Session, error) { + s.RLock() + defer s.RUnlock() + + session := new(Session) + row := s.QueryRow("SELECT * FROM sessions WHERE id=?", id) + err := row.Scan(&session.Public, &session.Token, &session.Secret) + + return *session, err +} + +func (s *SQLite) CreateSession(ctx context.Context, public, token, secret string) error { + s.Lock() + defer s.Unlock() + + tx, err := s.Begin() + + if err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO sessions (public, token, secret) VALUES ($1, $2, $3)", + public, token, secret) + + if err != nil { + return err + } + + return tx.Commit() +} diff --git a/internal/database/databasefakes/fake_database.go b/internal/database/databasefakes/fake_database.go index 24214144..09329063 100644 --- a/internal/database/databasefakes/fake_database.go +++ b/internal/database/databasefakes/fake_database.go @@ -19,6 +19,19 @@ type FakeDatabase struct { closeReturnsOnCall map[int]struct { result1 error } + CreateAccountStub func(context.Context, string, string) error + createAccountMutex sync.RWMutex + createAccountArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + createAccountReturns struct { + result1 error + } + createAccountReturnsOnCall map[int]struct { + result1 error + } CreateDocumentStub func(context.Context, string, string) error createDocumentMutex sync.RWMutex createDocumentArgsForCall []struct { @@ -32,6 +45,60 @@ type FakeDatabase struct { createDocumentReturnsOnCall map[int]struct { result1 error } + CreateSessionStub func(context.Context, string, string, string) error + createSessionMutex sync.RWMutex + createSessionArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + } + createSessionReturns struct { + result1 error + } + createSessionReturnsOnCall map[int]struct { + result1 error + } + DeleteAccountStub func(context.Context, string) error + deleteAccountMutex sync.RWMutex + deleteAccountArgsForCall []struct { + arg1 context.Context + arg2 string + } + deleteAccountReturns struct { + result1 error + } + deleteAccountReturnsOnCall map[int]struct { + result1 error + } + GetAccountStub func(context.Context, string) (database.Account, error) + getAccountMutex sync.RWMutex + getAccountArgsForCall []struct { + arg1 context.Context + arg2 string + } + getAccountReturns struct { + result1 database.Account + result2 error + } + getAccountReturnsOnCall map[int]struct { + result1 database.Account + result2 error + } + GetAccountByUsernameStub func(context.Context, string) (database.Account, error) + getAccountByUsernameMutex sync.RWMutex + getAccountByUsernameArgsForCall []struct { + arg1 context.Context + arg2 string + } + getAccountByUsernameReturns struct { + result1 database.Account + result2 error + } + getAccountByUsernameReturnsOnCall map[int]struct { + result1 database.Account + result2 error + } GetDocumentStub func(context.Context, string) (database.Document, error) getDocumentMutex sync.RWMutex getDocumentArgsForCall []struct { @@ -46,6 +113,20 @@ type FakeDatabase struct { result1 database.Document result2 error } + GetSessionStub func(context.Context, string) (database.Session, error) + getSessionMutex sync.RWMutex + getSessionArgsForCall []struct { + arg1 context.Context + arg2 string + } + getSessionReturns struct { + result1 database.Session + result2 error + } + getSessionReturnsOnCall map[int]struct { + result1 database.Session + result2 error + } MigrateStub func(context.Context) error migrateMutex sync.RWMutex migrateArgsForCall []struct { @@ -114,6 +195,69 @@ func (fake *FakeDatabase) CloseReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeDatabase) CreateAccount(arg1 context.Context, arg2 string, arg3 string) error { + fake.createAccountMutex.Lock() + ret, specificReturn := fake.createAccountReturnsOnCall[len(fake.createAccountArgsForCall)] + fake.createAccountArgsForCall = append(fake.createAccountArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.CreateAccountStub + fakeReturns := fake.createAccountReturns + fake.recordInvocation("CreateAccount", []interface{}{arg1, arg2, arg3}) + fake.createAccountMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDatabase) CreateAccountCallCount() int { + fake.createAccountMutex.RLock() + defer fake.createAccountMutex.RUnlock() + return len(fake.createAccountArgsForCall) +} + +func (fake *FakeDatabase) CreateAccountCalls(stub func(context.Context, string, string) error) { + fake.createAccountMutex.Lock() + defer fake.createAccountMutex.Unlock() + fake.CreateAccountStub = stub +} + +func (fake *FakeDatabase) CreateAccountArgsForCall(i int) (context.Context, string, string) { + fake.createAccountMutex.RLock() + defer fake.createAccountMutex.RUnlock() + argsForCall := fake.createAccountArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeDatabase) CreateAccountReturns(result1 error) { + fake.createAccountMutex.Lock() + defer fake.createAccountMutex.Unlock() + fake.CreateAccountStub = nil + fake.createAccountReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeDatabase) CreateAccountReturnsOnCall(i int, result1 error) { + fake.createAccountMutex.Lock() + defer fake.createAccountMutex.Unlock() + fake.CreateAccountStub = nil + if fake.createAccountReturnsOnCall == nil { + fake.createAccountReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.createAccountReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeDatabase) CreateDocument(arg1 context.Context, arg2 string, arg3 string) error { fake.createDocumentMutex.Lock() ret, specificReturn := fake.createDocumentReturnsOnCall[len(fake.createDocumentArgsForCall)] @@ -177,6 +321,262 @@ func (fake *FakeDatabase) CreateDocumentReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeDatabase) CreateSession(arg1 context.Context, arg2 string, arg3 string, arg4 string) error { + fake.createSessionMutex.Lock() + ret, specificReturn := fake.createSessionReturnsOnCall[len(fake.createSessionArgsForCall)] + fake.createSessionArgsForCall = append(fake.createSessionArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.CreateSessionStub + fakeReturns := fake.createSessionReturns + fake.recordInvocation("CreateSession", []interface{}{arg1, arg2, arg3, arg4}) + fake.createSessionMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDatabase) CreateSessionCallCount() int { + fake.createSessionMutex.RLock() + defer fake.createSessionMutex.RUnlock() + return len(fake.createSessionArgsForCall) +} + +func (fake *FakeDatabase) CreateSessionCalls(stub func(context.Context, string, string, string) error) { + fake.createSessionMutex.Lock() + defer fake.createSessionMutex.Unlock() + fake.CreateSessionStub = stub +} + +func (fake *FakeDatabase) CreateSessionArgsForCall(i int) (context.Context, string, string, string) { + fake.createSessionMutex.RLock() + defer fake.createSessionMutex.RUnlock() + argsForCall := fake.createSessionArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeDatabase) CreateSessionReturns(result1 error) { + fake.createSessionMutex.Lock() + defer fake.createSessionMutex.Unlock() + fake.CreateSessionStub = nil + fake.createSessionReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeDatabase) CreateSessionReturnsOnCall(i int, result1 error) { + fake.createSessionMutex.Lock() + defer fake.createSessionMutex.Unlock() + fake.CreateSessionStub = nil + if fake.createSessionReturnsOnCall == nil { + fake.createSessionReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.createSessionReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeDatabase) DeleteAccount(arg1 context.Context, arg2 string) error { + fake.deleteAccountMutex.Lock() + ret, specificReturn := fake.deleteAccountReturnsOnCall[len(fake.deleteAccountArgsForCall)] + fake.deleteAccountArgsForCall = append(fake.deleteAccountArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.DeleteAccountStub + fakeReturns := fake.deleteAccountReturns + fake.recordInvocation("DeleteAccount", []interface{}{arg1, arg2}) + fake.deleteAccountMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDatabase) DeleteAccountCallCount() int { + fake.deleteAccountMutex.RLock() + defer fake.deleteAccountMutex.RUnlock() + return len(fake.deleteAccountArgsForCall) +} + +func (fake *FakeDatabase) DeleteAccountCalls(stub func(context.Context, string) error) { + fake.deleteAccountMutex.Lock() + defer fake.deleteAccountMutex.Unlock() + fake.DeleteAccountStub = stub +} + +func (fake *FakeDatabase) DeleteAccountArgsForCall(i int) (context.Context, string) { + fake.deleteAccountMutex.RLock() + defer fake.deleteAccountMutex.RUnlock() + argsForCall := fake.deleteAccountArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeDatabase) DeleteAccountReturns(result1 error) { + fake.deleteAccountMutex.Lock() + defer fake.deleteAccountMutex.Unlock() + fake.DeleteAccountStub = nil + fake.deleteAccountReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeDatabase) DeleteAccountReturnsOnCall(i int, result1 error) { + fake.deleteAccountMutex.Lock() + defer fake.deleteAccountMutex.Unlock() + fake.DeleteAccountStub = nil + if fake.deleteAccountReturnsOnCall == nil { + fake.deleteAccountReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteAccountReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeDatabase) GetAccount(arg1 context.Context, arg2 string) (database.Account, error) { + fake.getAccountMutex.Lock() + ret, specificReturn := fake.getAccountReturnsOnCall[len(fake.getAccountArgsForCall)] + fake.getAccountArgsForCall = append(fake.getAccountArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.GetAccountStub + fakeReturns := fake.getAccountReturns + fake.recordInvocation("GetAccount", []interface{}{arg1, arg2}) + fake.getAccountMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeDatabase) GetAccountCallCount() int { + fake.getAccountMutex.RLock() + defer fake.getAccountMutex.RUnlock() + return len(fake.getAccountArgsForCall) +} + +func (fake *FakeDatabase) GetAccountCalls(stub func(context.Context, string) (database.Account, error)) { + fake.getAccountMutex.Lock() + defer fake.getAccountMutex.Unlock() + fake.GetAccountStub = stub +} + +func (fake *FakeDatabase) GetAccountArgsForCall(i int) (context.Context, string) { + fake.getAccountMutex.RLock() + defer fake.getAccountMutex.RUnlock() + argsForCall := fake.getAccountArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeDatabase) GetAccountReturns(result1 database.Account, result2 error) { + fake.getAccountMutex.Lock() + defer fake.getAccountMutex.Unlock() + fake.GetAccountStub = nil + fake.getAccountReturns = struct { + result1 database.Account + result2 error + }{result1, result2} +} + +func (fake *FakeDatabase) GetAccountReturnsOnCall(i int, result1 database.Account, result2 error) { + fake.getAccountMutex.Lock() + defer fake.getAccountMutex.Unlock() + fake.GetAccountStub = nil + if fake.getAccountReturnsOnCall == nil { + fake.getAccountReturnsOnCall = make(map[int]struct { + result1 database.Account + result2 error + }) + } + fake.getAccountReturnsOnCall[i] = struct { + result1 database.Account + result2 error + }{result1, result2} +} + +func (fake *FakeDatabase) GetAccountByUsername(arg1 context.Context, arg2 string) (database.Account, error) { + fake.getAccountByUsernameMutex.Lock() + ret, specificReturn := fake.getAccountByUsernameReturnsOnCall[len(fake.getAccountByUsernameArgsForCall)] + fake.getAccountByUsernameArgsForCall = append(fake.getAccountByUsernameArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.GetAccountByUsernameStub + fakeReturns := fake.getAccountByUsernameReturns + fake.recordInvocation("GetAccountByUsername", []interface{}{arg1, arg2}) + fake.getAccountByUsernameMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeDatabase) GetAccountByUsernameCallCount() int { + fake.getAccountByUsernameMutex.RLock() + defer fake.getAccountByUsernameMutex.RUnlock() + return len(fake.getAccountByUsernameArgsForCall) +} + +func (fake *FakeDatabase) GetAccountByUsernameCalls(stub func(context.Context, string) (database.Account, error)) { + fake.getAccountByUsernameMutex.Lock() + defer fake.getAccountByUsernameMutex.Unlock() + fake.GetAccountByUsernameStub = stub +} + +func (fake *FakeDatabase) GetAccountByUsernameArgsForCall(i int) (context.Context, string) { + fake.getAccountByUsernameMutex.RLock() + defer fake.getAccountByUsernameMutex.RUnlock() + argsForCall := fake.getAccountByUsernameArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeDatabase) GetAccountByUsernameReturns(result1 database.Account, result2 error) { + fake.getAccountByUsernameMutex.Lock() + defer fake.getAccountByUsernameMutex.Unlock() + fake.GetAccountByUsernameStub = nil + fake.getAccountByUsernameReturns = struct { + result1 database.Account + result2 error + }{result1, result2} +} + +func (fake *FakeDatabase) GetAccountByUsernameReturnsOnCall(i int, result1 database.Account, result2 error) { + fake.getAccountByUsernameMutex.Lock() + defer fake.getAccountByUsernameMutex.Unlock() + fake.GetAccountByUsernameStub = nil + if fake.getAccountByUsernameReturnsOnCall == nil { + fake.getAccountByUsernameReturnsOnCall = make(map[int]struct { + result1 database.Account + result2 error + }) + } + fake.getAccountByUsernameReturnsOnCall[i] = struct { + result1 database.Account + result2 error + }{result1, result2} +} + func (fake *FakeDatabase) GetDocument(arg1 context.Context, arg2 string) (database.Document, error) { fake.getDocumentMutex.Lock() ret, specificReturn := fake.getDocumentReturnsOnCall[len(fake.getDocumentArgsForCall)] @@ -242,6 +642,71 @@ func (fake *FakeDatabase) GetDocumentReturnsOnCall(i int, result1 database.Docum }{result1, result2} } +func (fake *FakeDatabase) GetSession(arg1 context.Context, arg2 string) (database.Session, error) { + fake.getSessionMutex.Lock() + ret, specificReturn := fake.getSessionReturnsOnCall[len(fake.getSessionArgsForCall)] + fake.getSessionArgsForCall = append(fake.getSessionArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.GetSessionStub + fakeReturns := fake.getSessionReturns + fake.recordInvocation("GetSession", []interface{}{arg1, arg2}) + fake.getSessionMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeDatabase) GetSessionCallCount() int { + fake.getSessionMutex.RLock() + defer fake.getSessionMutex.RUnlock() + return len(fake.getSessionArgsForCall) +} + +func (fake *FakeDatabase) GetSessionCalls(stub func(context.Context, string) (database.Session, error)) { + fake.getSessionMutex.Lock() + defer fake.getSessionMutex.Unlock() + fake.GetSessionStub = stub +} + +func (fake *FakeDatabase) GetSessionArgsForCall(i int) (context.Context, string) { + fake.getSessionMutex.RLock() + defer fake.getSessionMutex.RUnlock() + argsForCall := fake.getSessionArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeDatabase) GetSessionReturns(result1 database.Session, result2 error) { + fake.getSessionMutex.Lock() + defer fake.getSessionMutex.Unlock() + fake.GetSessionStub = nil + fake.getSessionReturns = struct { + result1 database.Session + result2 error + }{result1, result2} +} + +func (fake *FakeDatabase) GetSessionReturnsOnCall(i int, result1 database.Session, result2 error) { + fake.getSessionMutex.Lock() + defer fake.getSessionMutex.Unlock() + fake.GetSessionStub = nil + if fake.getSessionReturnsOnCall == nil { + fake.getSessionReturnsOnCall = make(map[int]struct { + result1 database.Session + result2 error + }) + } + fake.getSessionReturnsOnCall[i] = struct { + result1 database.Session + result2 error + }{result1, result2} +} + func (fake *FakeDatabase) Migrate(arg1 context.Context) error { fake.migrateMutex.Lock() ret, specificReturn := fake.migrateReturnsOnCall[len(fake.migrateArgsForCall)] @@ -308,10 +773,22 @@ func (fake *FakeDatabase) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.closeMutex.RLock() defer fake.closeMutex.RUnlock() + fake.createAccountMutex.RLock() + defer fake.createAccountMutex.RUnlock() fake.createDocumentMutex.RLock() defer fake.createDocumentMutex.RUnlock() + fake.createSessionMutex.RLock() + defer fake.createSessionMutex.RUnlock() + fake.deleteAccountMutex.RLock() + defer fake.deleteAccountMutex.RUnlock() + fake.getAccountMutex.RLock() + defer fake.getAccountMutex.RUnlock() + fake.getAccountByUsernameMutex.RLock() + defer fake.getAccountByUsernameMutex.RUnlock() fake.getDocumentMutex.RLock() defer fake.getDocumentMutex.RUnlock() + fake.getSessionMutex.RLock() + defer fake.getSessionMutex.RUnlock() fake.migrateMutex.RLock() defer fake.migrateMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/internal/server/authentication.go b/internal/server/authentication.go new file mode 100644 index 00000000..b0482b6e --- /dev/null +++ b/internal/server/authentication.go @@ -0,0 +1,107 @@ +package server + +import ( + "encoding/base64" + "errors" + "fmt" + "log" + "net/http" + + "github.com/lukewhrit/spacebin/internal/util" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/sha3" +) + +func (s *Server) SignUp(w http.ResponseWriter, r *http.Request) { + body := util.SignupRequest{ + Username: "luke", + Password: "password", + } + + // Do validation + // Make sure password is secure, make sure username does not exist + + // Create account + // Encryption handled in Database function + err := s.Database.CreateAccount(r.Context(), body.Username, body.Password) + + if err != nil { + util.WriteError(w, http.StatusInternalServerError, err) + } + + // Respond on success with account ID and username + account, err := s.Database.GetAccountByUsername(r.Context(), body.Username) + + if err != nil { + util.WriteError(w, http.StatusInternalServerError, err) + } + + util.WriteJSON(w, http.StatusOK, map[string]interface{}{ + "id": account.ID, + "username": account.Username, + }) + +} + +func (s *Server) SignIn(w http.ResponseWriter, r *http.Request) { + body := &util.SigninRequest{ + Username: "luke", + Password: "password", + } + + // if err != nil { + // util.WriteError(w, http.StatusBadRequest, err) + // } + + // Get user from database + acc, err := s.Database.GetAccountByUsername(r.Context(), body.Username) + + if err != nil { + util.WriteError(w, http.StatusInternalServerError, err) + } + + // Compare passwords + if bcrypt.CompareHashAndPassword([]byte(acc.Password), []byte(body.Password)) == nil { + // Generate public, secret keys and salt + pub, sec, salt, err := util.GenerateStrings([]int{64, 64, 32}) + + if err != nil { + log.Fatal(err) + } + + // Salt secret key + buf := []byte(sec + salt) + secret := make([]byte, 64) + sha3.ShakeSum256(secret, buf) + + // Create user and server tokens for later comparison + userToken := util.MakeToken(util.Token{ + Version: "v1", + Public: pub, + Secret: base64.URLEncoding.EncodeToString([]byte(sec)), + Salt: salt, + }) + + serverToken := util.MakeToken(util.Token{ + Version: "v1", + Public: pub, + Secret: fmt.Sprintf("%x", secret), + Salt: salt, + }) + + if err != nil { + util.WriteError(w, http.StatusInternalServerError, err) + } + + // Add session to Postgres + if err := s.Database.CreateSession(r.Context(), pub, userToken, serverToken); err != nil { + util.WriteError(w, http.StatusInternalServerError, err) + } + + util.WriteJSON(w, http.StatusOK, map[string]string{ + "token": userToken, + }) + } else { + util.WriteError(w, http.StatusUnauthorized, errors.New("invalid username or password")) + } +} diff --git a/internal/server/fetch.go b/internal/server/fetch.go index a3376caa..a85b0735 100644 --- a/internal/server/fetch.go +++ b/internal/server/fetch.go @@ -62,37 +62,59 @@ func (s *Server) StaticDocument(w http.ResponseWriter, r *http.Request) { return } - t, err := template.ParseFS(resources, "web/document.html") + // Reader mode or code mode? + if r.URL.Query().Get("reader") == "true" { + t, err := template.ParseFS(resources, "web/reader.html") - if err != nil { - util.RenderError(&resources, w, http.StatusInternalServerError, err) - return - } + if err != nil { + util.RenderError(&resources, w, http.StatusInternalServerError, err) + return + } - extension := "" + content := util.ParseMarkdown([]byte(document.Content)) - if len(params) == 2 { - extension = params[1] - } + data := map[string]interface{}{ + "Content": template.HTML(string(content)), + "Analytics": template.HTML(config.Config.Analytics), + } - highlighted, css, err := util.Highlight(document.Content, extension) + if err := t.Execute(w, data); err != nil { + util.RenderError(&resources, w, http.StatusInternalServerError, err) + return + } + } else { + t, err := template.ParseFS(resources, "web/document.html") - if err != nil { - util.RenderError(&resources, w, http.StatusInternalServerError, err) - return - } + if err != nil { + util.RenderError(&resources, w, http.StatusInternalServerError, err) + return + } - data := map[string]interface{}{ - "Stylesheet": template.CSS(css), - "Content": document.Content, - "Highlighted": template.HTML(highlighted), - "Extension": extension, - "Analytics": template.HTML(config.Config.Analytics), - } + extension := "" - if err := t.Execute(w, data); err != nil { - util.RenderError(&resources, w, http.StatusInternalServerError, err) - return + if len(params) == 2 { + extension = params[1] + } + + highlighted, css, err := util.Highlight(document.Content, extension) + + if err != nil { + util.RenderError(&resources, w, http.StatusInternalServerError, err) + return + } + + data := map[string]interface{}{ + "Stylesheet": template.CSS(css), + "Content": document.Content, + "Highlighted": template.HTML(highlighted), + "Extension": extension, + "Analytics": template.HTML(config.Config.Analytics), + } + + if err := t.Execute(w, data); err != nil { + util.RenderError(&resources, w, http.StatusInternalServerError, err) + return + } } } diff --git a/internal/server/server.go b/internal/server/server.go index cb4e82db..73c8c2c0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -173,10 +173,16 @@ func (s *Server) MountHandlers() { // Register routes s.Router.Get("/config", s.GetConfig) + // Document routes s.Router.Post("/api/", s.CreateDocument) s.Router.Get("/api/{document}", s.FetchDocument) s.Router.Get("/api/{document}/raw", s.FetchRawDocument) + // Account routes + s.Router.Post("/api/signin", s.SignIn) + s.Router.Post("/api/signup", s.SignUp) + + // Static routes s.Router.Post("/", s.StaticCreateDocument) s.Router.Get("/{document}", s.StaticDocument) s.Router.Get("/{document}/raw", s.FetchRawDocument) diff --git a/internal/server/web/account.html b/internal/server/web/account.html new file mode 100644 index 00000000..e69de29b diff --git a/internal/server/web/document.html b/internal/server/web/document.html index d91c7359..0993b8c2 100644 --- a/internal/server/web/document.html +++ b/internal/server/web/document.html @@ -48,11 +48,11 @@ - - - - + + + + @@ -81,4 +81,4 @@ - \ No newline at end of file + diff --git a/internal/server/web/index.html b/internal/server/web/index.html index 787c3e5a..da72b9de 100644 --- a/internal/server/web/index.html +++ b/internal/server/web/index.html @@ -45,15 +45,6 @@ - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+ {{.Content}} +
+
+ + + + + diff --git a/internal/server/web/static/app.js b/internal/server/web/static/app.js index 741d9dd4..8fbc21f2 100644 --- a/internal/server/web/static/app.js +++ b/internal/server/web/static/app.js @@ -13,3 +13,21 @@ document.querySelector('textarea')?.addEventListener('keydown', function (e) { this.selectionStart = this.selectionEnd = start + 1; } }); + +function switchFont(to) { + const main = document.querySelector('.wysiwyg'); + + if (to === 'sans') { + main.classList.remove('font-serif', 'font-sans'); + main.classList.add('font-sans'); + + document.querySelector('#serif').classList.remove('active'); + document.querySelector('#sans').classList.add('active'); + } else if (to === 'serif') { + main.classList.remove('font-serif', 'font-sans'); + main.classList.add('font-serif'); + + document.querySelector('#sans').classList.remove('active'); + document.querySelector('#serif').classList.add('active'); + } +} diff --git a/internal/server/web/static/global.css b/internal/server/web/static/global.css index 0191c8ae..4e146c49 100644 --- a/internal/server/web/static/global.css +++ b/internal/server/web/static/global.css @@ -9,6 +9,7 @@ --color-links-dark: #7a98d8; --color-foreground: #dedede; --color-background: #121212; + --color-buttons: #1d1c1c; } * { diff --git a/internal/server/web/static/reader.css b/internal/server/web/static/reader.css new file mode 100644 index 00000000..23b184ef --- /dev/null +++ b/internal/server/web/static/reader.css @@ -0,0 +1,289 @@ + +.wysiwyg { + font-size: 1rem; + line-height: 2; + color: var(--color-foreground); +} + +#reader { + margin: 0 auto; + padding: 0 25%; +} + +.font-sans { + font-family: -apple-system, BlinkMacSystemFont, "Avenir Next", "Avenir", "Segoe UI", "Helvetica Neue", "Helvetica", "Cantarell", "Ubuntu", "Roboto", "Noto", "Arial", sans-serif; +} + +.font-serif { + font-family: "Iowan Old Style", "Apple Garamond", "Baskerville", "Times New Roman", "Droid Serif", "Times", "Source Serif Pro", serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +} + +#font-button-group { + display: flex; + width: fit-content; + gap: 1px; + background-color: var(--color-buttons); + padding: 5px; + margin-bottom: 24px; +} + +#font-button-group button { + padding: 5px 10px; + margin: 0; + border: none; + color: var(--color-links); + font-size: var(--font-size); + cursor: pointer; + text-decoration: none; +} + +button.active { + background-color: var(--color-background); +} + +/*! wysiwyg.css v0.0.3 | MIT License | github.com/jgthms/wysiwyg.css */ +.wysiwyg { + line-height: 1.6; +} +.wysiwyg a { + text-decoration: none; +} +.wysiwyg a:hover { + border-bottom: 1px solid; +} +.wysiwyg abbr { + border-bottom: 1px dotted; + cursor: help; +} +.wysiwyg cite { + font-style: italic; +} +.wysiwyg hr { + background: #e6e6e6; + border: none; + display: block; + height: 1px; + margin-bottom: 1.4em; + margin-top: 1.4em; +} +.wysiwyg img { + vertical-align: text-bottom; +} +.wysiwyg ins { + background-color: lime; + text-decoration: none; +} +.wysiwyg mark { + background-color: #ff0; +} +.wysiwyg small { + font-size: 0.8em; +} +.wysiwyg strong { + font-weight: 700; +} +.wysiwyg sub, +.wysiwyg sup { + font-size: 0.8em; +} +.wysiwyg sub { + vertical-align: sub; +} +.wysiwyg sup { + vertical-align: super; +} +.wysiwyg p, +.wysiwyg dl, +.wysiwyg ol, +.wysiwyg ul, +.wysiwyg blockquote, +.wysiwyg pre, +.wysiwyg table { + margin-bottom: 1.4em; +} +.wysiwyg p:last-child, +.wysiwyg dl:last-child, +.wysiwyg ol:last-child, +.wysiwyg ul:last-child, +.wysiwyg blockquote:last-child, +.wysiwyg pre:last-child, +.wysiwyg table:last-child { + margin-bottom: 0; +} +.wysiwyg p:empty { + display: none; +} +.wysiwyg h1, +.wysiwyg h2, +.wysiwyg h3, +.wysiwyg h4, +.wysiwyg h5, +.wysiwyg h6 { + font-weight: 700; + line-height: 1.2; +} +.wysiwyg h1:first-child, +.wysiwyg h2:first-child, +.wysiwyg h3:first-child, +.wysiwyg h4:first-child, +.wysiwyg h5:first-child, +.wysiwyg h6:first-child { + margin-top: 0; +} +.wysiwyg h1 { + font-size: 2.4em; + margin-bottom: 0.58333em; + margin-top: 0.58333em; + line-height: 1; +} +.wysiwyg h2 { + font-size: 1.6em; + margin-bottom: 0.875em; + margin-top: 1.75em; + line-height: 1.1; +} +.wysiwyg h3 { + font-size: 1.3em; + margin-bottom: 1.07692em; + margin-top: 1.07692em; +} +.wysiwyg h4 { + font-size: 1.2em; + margin-bottom: 1.16667em; + margin-top: 1.16667em; +} +.wysiwyg h5 { + font-size: 1.1em; + margin-bottom: 1.27273em; + margin-top: 1.27273em; +} +.wysiwyg h6 { + font-size: 1em; + margin-bottom: 1.4em; + margin-top: 1.4em; +} +.wysiwyg dd { + margin-left: 1.4em; +} +.wysiwyg ol, +.wysiwyg ul { + list-style-position: outside; + margin-left: 1.4em; +} +.wysiwyg ol { + list-style-type: decimal; +} +.wysiwyg ol ol { + list-style-type: lower-alpha; +} +.wysiwyg ol ol ol { + list-style-type: lower-roman; +} +.wysiwyg ol ol ol ol { + list-style-type: lower-greek; +} +.wysiwyg ol ol ol ol ol { + list-style-type: decimal; +} +.wysiwyg ol ol ol ol ol ol { + list-style-type: lower-alpha; +} +.wysiwyg ul { + list-style-type: disc; +} +.wysiwyg ul ul { + list-style-type: circle; +} +.wysiwyg ul ul ul { + list-style-type: square; +} +.wysiwyg ul ul ul ul { + list-style-type: circle; +} +.wysiwyg ul ul ul ul ul { + list-style-type: disc; +} +.wysiwyg ul ul ul ul ul ul { + list-style-type: circle; +} +.wysiwyg blockquote { + border-left: 4px solid #e6e6e6; + padding: 0.6em 1.2em; +} +.wysiwyg blockquote p { + margin-bottom: 0; +} +.wysiwyg code, +.wysiwyg kbd, +.wysiwyg samp, +.wysiwyg pre { + -moz-osx-font-smoothing: auto; + -webkit-font-smoothing: auto; + background-color: #f2f2f2; + color: #333; + font-size: 0.9em; +} +.wysiwyg code, +.wysiwyg kbd, +.wysiwyg samp { + border-radius: 3px; + line-height: 1.77778; + padding: 0.1em 0.4em 0.2em; + vertical-align: baseline; +} +.wysiwyg pre { + overflow: auto; + padding: 1em 1.2em; +} +.wysiwyg pre code { + background: none; + font-size: 1em; + line-height: 1em; +} +.wysiwyg figure { + margin-bottom: 2.8em; + text-align: center; +} +.wysiwyg figure:first-child { + margin-top: 0; +} +.wysiwyg figure:last-child { + margin-bottom: 0; +} +.wysiwyg figcaption { + font-size: 0.8em; + margin-top: 0.875em; +} +.wysiwyg table { + width: 100%; +} +.wysiwyg table pre { + white-space: pre-wrap; +} +.wysiwyg th, +.wysiwyg td { + font-size: 1em; + padding: 0.7em; + border: 1px solid #e6e6e6; + line-height: 1.4; +} +.wysiwyg thead tr, +.wysiwyg tfoot tr { + background-color: #f5f5f5; +} +.wysiwyg thead th, +.wysiwyg thead td, +.wysiwyg tfoot th, +.wysiwyg tfoot td { + font-size: 0.9em; + padding: 0.77778em; +} +.wysiwyg thead th code, +.wysiwyg thead td code, +.wysiwyg tfoot th code, +.wysiwyg tfoot td code { + background-color: #fff; +} +.wysiwyg tbody tr { + background-color: #fff; +} diff --git a/internal/util/authentication.go b/internal/util/authentication.go new file mode 100644 index 00000000..08a576fd --- /dev/null +++ b/internal/util/authentication.go @@ -0,0 +1,69 @@ +package util + +import ( + "crypto/rand" + "fmt" + "log" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +func HashAndSalt(pwd []byte) string { + // Use GenerateFromPassword to hash & salt pwd. + // MinCost is just an integer constant provided by the bcrypt + // package along with DefaultCost & MaxCost. + // The cost can be any value you want provided it isn't lower + // than the MinCost (4) + hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost) + if err != nil { + log.Fatalln(err) + } // GenerateFromPassword returns a byte slice so we need to + // convert the bytes to a string and return it + return string(hash) +} + +func PrngString() (string, error) { + b := make([]byte, 10) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return fmt.Sprintf("%x", b), nil +} + +func GenerateStrings(bits []int) (a, b, c string, err error) { + if a, err = PrngString(); err != nil { + return "", "", "", err + } + + if b, err = PrngString(); err != nil { + return "", "", "", err + } + + if c, err = PrngString(); err != nil { + return "", "", "", err + } + + return a, b, c, err +} + +func ParseToken(token string) (Token, error) { + var tok Token + toks := strings.Split(token, ".") + + tok.Version = toks[0] + tok.Public = toks[1] + tok.Secret = toks[2] + + if len(toks) == 4 { + tok.Salt = toks[3] + } + + return tok, nil +} + +func MakeToken(token Token) string { + return fmt.Sprintf("%s.%s.%s.%s", token.Version, token.Public, token.Secret, token.Salt) +} diff --git a/internal/util/domain.go b/internal/util/domain.go index efbf6ad0..52e5ac92 100644 --- a/internal/util/domain.go +++ b/internal/util/domain.go @@ -28,3 +28,28 @@ type DocumentResponse struct { UpdatedAt int64 `json:"updated_at,omitempty"` // The Unix timestamp of when the document was last modified. Exists bool `json:"exists,omitempty"` // Whether the document does or does not exist. } + +// Token is an authentication token object +type Token struct { + Version string + Public string + Secret string + Salt string +} + +// CreateRequest represents a POST request to create a document +type CreateRequest struct { + Content string +} + +// SigninRequest represents a POST request to authenticate an account +type SigninRequest struct { + Username string + Password string +} + +// SignupRequest represents a POST request to register an account +type SignupRequest struct { + Username string + Password string +} diff --git a/internal/util/helpers.go b/internal/util/helpers.go index 7c70fc84..2bc125fd 100644 --- a/internal/util/helpers.go +++ b/internal/util/helpers.go @@ -29,15 +29,26 @@ import ( "github.com/rs/zerolog/log" ) -type CreateRequest struct { - Content string -} +func ValidateBody[T CreateRequest | SigninRequest | SignupRequest](maxSize int, body T) error { + switch v := any(body).(type) { + case CreateRequest: + return validation.ValidateStruct(&v, + validation.Field(&v.Content, validation.Required, validation.Length(2, maxSize)), + ) + case SigninRequest: + return validation.ValidateStruct(&v, + validation.Field(&v.Username, validation.Required), + validation.Field(&v.Password, validation.Required, validation.Length(16, 128)), + ) + case SignupRequest: + return validation.ValidateStruct(&v, + validation.Field(&v.Username, validation.Required), + validation.Field(&v.Password, validation.Required, validation.Length(16, 128)), + ) + default: + return validation.Errors{"body": validation.NewError("validation_error", "unsupported request type")} + } -func ValidateBody(maxSize int, body CreateRequest) error { - return validation.ValidateStruct(&body, - validation.Field(&body.Content, validation.Required, - validation.Length(2, maxSize)), - ) } // HandleBody figures out whether a incoming request is in JSON or multipart/form-data and decodes it appropriately diff --git a/internal/util/markdown.go b/internal/util/markdown.go new file mode 100644 index 00000000..2ed52433 --- /dev/null +++ b/internal/util/markdown.go @@ -0,0 +1,20 @@ +package util + +import ( + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func ParseMarkdown(md []byte) []byte { + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +}