diff --git a/.gitignore b/.gitignore index 42c0b9d..0e7ec86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .env dev.session.sql __debug_bin - +server .vscode # Binaries for programs and plugins diff --git a/go.mod b/go.mod index 511e25a..e489115 100644 --- a/go.mod +++ b/go.mod @@ -4,27 +4,42 @@ go 1.22 require ( github.com/PuerkitoBio/goquery v1.8.0 + github.com/glebarez/go-sqlite v1.22.0 github.com/go-chi/chi/v5 v5.0.7 github.com/go-rod/rod v0.107.1 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 + github.com/huandu/go-sqlbuilder v1.27.1 github.com/joho/godotenv v1.4.0 github.com/labstack/echo/v4 v4.12.0 github.com/mmcdole/gofeed v1.1.3 github.com/nicklaw5/helix/v2 v2.4.0 + github.com/pressly/goose/v3 v3.20.0 github.com/robfig/cron/v3 v3.0.1 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.8.12 + golang.org/x/crypto v0.22.0 ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sethvargo/go-retry v0.2.4 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.22.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sync v0.7.0 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.29.6 // indirect ) require ( @@ -34,7 +49,7 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.21.1 // indirect - github.com/golang-jwt/jwt/v4 v4.4.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lib/pq v1.10.6 @@ -48,6 +63,6 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 5b3b982..2d8d499 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,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/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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -29,11 +33,21 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-rod/rod v0.107.1 h1:wRxTTAXJ0JUnoSGcyGAOubpdrToWIKPCnLu3av8EDFY= github.com/go-rod/rod v0.107.1/go.mod h1:Au6ufsz7KyXUJVnw6Ljs1nFpsopy+9AJ/lBwGauYBVg= -github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= -github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +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/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-sqlbuilder v1.27.1 h1:7UU/3EMIQYYX8wn+L7BNcGVz1aEs5TPNOVFd7ryrPos= +github.com/huandu/go-sqlbuilder v1.27.1/go.mod h1:nUVmMitjOmn/zacMLXT0d3Yd3RHoO2K+vy906JzqxMI= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -62,6 +76,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o= github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= @@ -73,18 +89,27 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nicklaw5/helix/v2 v2.4.0 h1:ZvqCKVqza1eJYyqgTRrZ/xjDq0w/EQVFNkN067Utls0= github.com/nicklaw5/helix/v2 v2.4.0/go.mod h1:0ONzvVi1cH+k3a7EDIFNNqxfW0podhf+CqlmFvuexq8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/pressly/goose/v3 v3.20.0 h1:uPJdOxF/Ipj7ABVNOAMJXSxwFXZGwMGHNqjC8e61VA0= +github.com/pressly/goose/v3 v3.20.0/go.mod h1:BRfF2GcG4FTG12QfdBVy3q1yveaf4ckL9vWwEcIO3lA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -110,17 +135,21 @@ github.com/ysmood/gson v0.7.2 h1:1iWUvpi5DPvd2j59W7ifRPR9DiAZ3Ga+fmMl1mJrRbM= github.com/ysmood/gson v0.7.2/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -135,8 +164,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= @@ -148,3 +177,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= +modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +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= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/repository/refreshTokens.go b/internal/repository/refreshTokens.go new file mode 100644 index 0000000..3a46a8c --- /dev/null +++ b/internal/repository/refreshTokens.go @@ -0,0 +1,119 @@ +package repository + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +const ( + refreshTokenTableName = "RefreshTokens" +) + +type RefreshTokenTable interface { + Create(username string, token string) (int64, error) + GetByUsername(name string) (domain.RefreshTokenEntity, error) + DeleteById(id int64) (int64, error) +} + +type RefreshTokenRepository struct { + connection *sql.DB +} + +func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository { + return RefreshTokenRepository{ + connection: conn, + } +} + +func (rt RefreshTokenRepository) Create(username string, token string) (int64, error) { + dt := time.Now() + builder := sqlbuilder.NewInsertBuilder() + builder.InsertInto(refreshTokenTableName) + builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt") + builder.Values(username, token, dt, dt) + query, args := builder.Build() + + _, err := rt.connection.Exec(query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshTokenEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*").From(refreshTokenTableName).Where( + builder.E("Username", name), + ) + + query, args := builder.Build() + rows, err := rt.connection.Query(query, args...) + if err != nil { + return domain.RefreshTokenEntity{}, err + } + + data := rt.processRows(rows) + if len(data) == 0 { + return domain.RefreshTokenEntity{}, errors.New("no token found for user") + } + + return data[0], nil +} + +func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) { + builder := sqlbuilder.NewDeleteBuilder() + builder.DeleteFrom(refreshTokenTableName) + builder.Where( + builder.EQ("Id", id), + ) + + query, args := builder.Build() + rows, err := rt.connection.Exec(query, args...) + if err != nil { + return -1, err + } + + return rows.RowsAffected() +} + +func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTokenEntity { + items := []domain.RefreshTokenEntity{} + + for rows.Next() { + var id int64 + var username string + var token string + var createdAt time.Time + var updatedAt time.Time + var deletedAt sql.NullTime + + err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &token) + if err != nil { + fmt.Println(err) + } + + item := domain.RefreshTokenEntity{ + ID: id, + Username: username, + Token: token, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + if (deletedAt.Valid) { + item.DeletedAt = deletedAt.Time + } + + items = append(items, item) + } + + return items +} + +//func (rt RefreshTokenRepository) Delete() diff --git a/internal/repository/refreshTokens_test.go b/internal/repository/refreshTokens_test.go new file mode 100644 index 0000000..2e44ef3 --- /dev/null +++ b/internal/repository/refreshTokens_test.go @@ -0,0 +1,92 @@ +package repository_test + +import ( + "testing" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +func TestRefreshTokenCreate(t *testing.T) { + conn, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + + client := repository.NewRefreshTokenRepository(conn) + rows, err := client.Create("tester", "BadTokenDontUse") + if err != nil { + t.Log(err) + t.FailNow() + } + + if rows == 0 { + t.Log("expected one row to come back but got 0") + } +} + +func TestRefreshTokenGetByUsername(t *testing.T) { + conn, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + + client := repository.NewRefreshTokenRepository(conn) + rows, err := client.Create("tester", "BadTokenDoNotUse") + if err != nil { + t.Log(err) + t.FailNow() + } + + if rows != 1 { + t.Log("expected a row to be added but not the wrong value back") + t.FailNow() + } + + model, err := client.GetByUsername("tester") + if err != nil { + t.Log(err) + t.FailNow() + } + + if model.Username != "tester" { + t.Log("got the wrong user back") + t.FailNow() + } +} + +func TestRefreshTokenDeleteById(t *testing.T) { + conn, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + + client := repository.NewRefreshTokenRepository(conn) + created, err := client.Create("tester", "BadTokenDoNotUse") + if err != nil { + t.Log(err) + t.FailNow() + } + if created != 1 { + t.Log("Unexpected number back for rows created") + } + + model, err := client.GetByUsername("tester") + if err != nil { + t.Log(err) + t.FailNow() + } + + updated, err := client.DeleteById(model.ID) + if err != nil { + t.Log(err) + t.FailNow() + } + + if updated != 1 { + t.Log("deleted the wrong number of records") + t.FailNow() + } +} diff --git a/internal/repository/users.go b/internal/repository/users.go new file mode 100644 index 0000000..416a093 --- /dev/null +++ b/internal/repository/users.go @@ -0,0 +1,164 @@ +package repository + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + + "github.com/huandu/go-sqlbuilder" + "golang.org/x/crypto/bcrypt" +) + +const ( + TableName string = "users" + ErrUserNotFound string = "requested user was not found" +) + +type IUserTable interface { + GetByName(name string) (domain.UserEntity, error) + Create(name, password, scope string) (int64, error) + Update(id int, entity domain.UserEntity) error + UpdatePassword(name, password string) error + CheckUserHash(name, password string) error + UpdateScopes(name, scope string) error +} + +// Creates a new instance of UserRepository with the bound sql +func NewUserRepository(conn *sql.DB) UserRepository { + return UserRepository{ + connection: conn, + } +} + +type UserRepository struct { + connection *sql.DB +} + +func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*").From("users").Where( + builder.E("Name", name), + ) + query, args := builder.Build() + + rows, err := ur.connection.Query(query, args...) + if err != nil { + return domain.UserEntity{}, err + } + + data := ur.processRows(rows) + if len(data) == 0 { + return domain.UserEntity{}, errors.New(ErrUserNotFound) + } + + return data[0], nil +} + +func (ur UserRepository) Create(name, password, scope string) (int64, error) { + passwordBytes := []byte(password) + hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) + if err != nil { + return 0, err + } + + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("users") + queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "Scopes") + queryBuilder.Values(name, string(hash), dt, dt, scope) + query, args := queryBuilder.Build() + + _, err = ur.connection.Exec(query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (ur UserRepository) Update(id int, entity domain.UserEntity) error { + return errors.New("not implemented") +} + +func (ur UserRepository) UpdatePassword(name, password string) error { + _, err := ur.GetByName(name) + if err != nil { + return nil + } + + queryBuilder := sqlbuilder.NewUpdateBuilder() + queryBuilder.Update(TableName) + //queryBuilder.Set + return nil +} + +// If the hash matches what we have in the database, an error will not be returned. +// If the user does not exist or the hash does not match, an error will be returned +func (ur UserRepository) CheckUserHash(name, password string) error { + record, err := ur.GetByName(name) + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword([]byte(record.Hash), []byte(password)) + if err != nil { + return err + } + + return nil +} + +func (ur UserRepository) UpdateScopes(name, scope string) error { + builder := sqlbuilder.NewUpdateBuilder() + builder.Update("users") + builder.Set( + builder.Assign("Scopes", scope), + ) + builder.Where( + builder.Equal("Name", name), + ) + query, args := builder.Build() + + _, err := ur.connection.Exec(query, args...) + if err != nil { + return err + } + return nil +} + +func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity { + items := []domain.UserEntity{} + + for rows.Next() { + var id int64 + var username string + var hash string + var createdAt time.Time + var updatedAt time.Time + var deletedAt sql.NullTime + var scopes string + err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &hash, &scopes) + if err != nil { + fmt.Println(err) + } + + item := domain.UserEntity{ + ID: id, + UpdatedAt: updatedAt, + Username: username, + Hash: hash, + Scopes: scopes, + CreatedAt: createdAt, + } + if deletedAt.Valid { + item.DeletedAt = deletedAt.Time + } + + items = append(items, item) + } + + return items +} diff --git a/internal/repository/users_test.go b/internal/repository/users_test.go new file mode 100644 index 0000000..c5d18db --- /dev/null +++ b/internal/repository/users_test.go @@ -0,0 +1,88 @@ +package repository_test + +import ( + "database/sql" + "log" + "testing" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" + + _ "github.com/glebarez/go-sqlite" + "github.com/pressly/goose/v3" +) + +func TestCanCreateNewUser(t *testing.T) { + //t.Log(time.Now().String()) + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + repo := repository.NewUserRepository(db) + updated, err := repo.Create("testing", "NotSecure", "placeholder") + if err != nil { + log.Println(err) + t.FailNow() + } + log.Println(updated) +} + +func TestCanFindUserInTable(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + log.Println("unable to open connection") + t.FailNow() + } + defer db.Close() + + repo := repository.NewUserRepository(db) + updated, err := repo.Create("testing", "NotSecure", "placeholder") + if err != nil { + t.Log(err) + t.FailNow() + } + + if updated != 1 { + t.Log("expected a row to come back") + t.FailNow() + } + + user, err := repo.GetByName("testing") + if err != nil { + log.Println(err) + t.FailNow() + } + log.Println(user) +} + +func TestCheckUserHash(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + log.Println("unable to open connection") + t.FailNow() + } + defer db.Close() + + repo := repository.NewUserRepository(db) + repo.CheckUserHash("testing", "NotSecure") +} + +func setupInMemoryDb() (*sql.DB, error) { + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + return nil, err + } + + err = goose.SetDialect("sqlite3") + if err != nil { + return nil, err + } + + err = goose.Up(db, "../database/migrations") + if err != nil { + return nil, err + } + return db, nil +}