From 5fd9d41069433b948b2ec604b4f140d6742cc1ac Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Mon, 29 Mar 2021 20:56:34 +1300 Subject: [PATCH] - Login flow working.. - Jellyfin scrobble working - Returns scrobbles via API for authed users /api/v1/user/{uuid}/scrobble - Add redis handler + funcs - Move middleware to pass in uuid as needed --- .env.example | 5 +- .gitignore | 3 + .gitlab-ci.yml | 2 +- cmd/go-scrobble/main.go | 6 +- docs/changelog.md | 7 + docs/config.md | 5 +- go.mod | 4 +- go.sum | 56 ++++++++ internal/goscrobble/db.go | 2 +- internal/goscrobble/jwt.go | 58 +++++--- internal/goscrobble/redis.go | 75 ++++++++++ internal/goscrobble/scrobble.go | 84 +++++++++-- internal/goscrobble/server.go | 76 +++++++--- internal/goscrobble/tokens.go | 13 +- internal/goscrobble/user.go | 24 +--- internal/goscrobble/utils.go | 5 + web/.env.development | 2 - web/.env.production | 2 - web/package-lock.json | 81 +++++++++++ web/package.json | 6 + web/src/Actions/auth.js | 87 ++++++++++++ web/src/Actions/message.js | 10 ++ web/src/Actions/types.js | 8 ++ web/src/App.js | 102 +++++++++----- web/src/Components/Api.js | 1 - web/src/Components/AppProvider.js | 45 ------ web/src/Components/Navigation.js | 146 ++++++++++++-------- web/src/Components/Pages/Help.css | 4 - web/src/Components/Pages/Help.js | 17 --- web/src/Components/Pages/Login.js | 115 --------------- web/src/Helpers/history.js | 3 + web/src/{Components => }/Pages/About.css | 0 web/src/{Components => }/Pages/About.js | 4 +- web/src/Pages/Admin.js | 45 ++++++ web/src/Pages/Dashboard.css | 4 + web/src/Pages/Dashboard.js | 35 +++++ web/src/{Components => }/Pages/Home.js | 4 +- web/src/{Components => }/Pages/Login.css | 0 web/src/Pages/Login.js | 92 ++++++++++++ web/src/Pages/Profile.css | 4 + web/src/Pages/Profile.js | 35 +++++ web/src/{Components => }/Pages/Register.css | 0 web/src/{Components => }/Pages/Register.js | 12 +- web/src/{Components => }/Pages/Settings.css | 0 web/src/{Components => }/Pages/Settings.js | 2 +- web/src/Reducers/auth.js | 50 +++++++ web/src/Reducers/index.js | 8 ++ web/src/Reducers/message.js | 18 +++ web/src/Services/Actions.js | 0 web/src/Services/auth-header.js | 9 ++ web/src/Services/auth.service.js | 35 +++++ web/src/Services/user.service.js | 18 +++ web/src/index.js | 38 ++--- web/src/store.js | 12 ++ 54 files changed, 1093 insertions(+), 386 deletions(-) create mode 100644 internal/goscrobble/redis.go delete mode 100644 web/.env.development delete mode 100644 web/.env.production create mode 100644 web/src/Actions/auth.js create mode 100644 web/src/Actions/message.js create mode 100644 web/src/Actions/types.js delete mode 100644 web/src/Components/Api.js delete mode 100644 web/src/Components/AppProvider.js delete mode 100644 web/src/Components/Pages/Help.css delete mode 100644 web/src/Components/Pages/Help.js delete mode 100644 web/src/Components/Pages/Login.js create mode 100644 web/src/Helpers/history.js rename web/src/{Components => }/Pages/About.css (100%) rename web/src/{Components => }/Pages/About.js (84%) create mode 100644 web/src/Pages/Admin.js create mode 100644 web/src/Pages/Dashboard.css create mode 100644 web/src/Pages/Dashboard.js rename web/src/{Components => }/Pages/Home.js (87%) rename web/src/{Components => }/Pages/Login.css (100%) create mode 100644 web/src/Pages/Login.js create mode 100644 web/src/Pages/Profile.css create mode 100644 web/src/Pages/Profile.js rename web/src/{Components => }/Pages/Register.css (100%) rename web/src/{Components => }/Pages/Register.js (94%) rename web/src/{Components => }/Pages/Settings.css (100%) rename web/src/{Components => }/Pages/Settings.js (96%) create mode 100644 web/src/Reducers/auth.js create mode 100644 web/src/Reducers/index.js create mode 100644 web/src/Reducers/message.js create mode 100644 web/src/Services/Actions.js create mode 100644 web/src/Services/auth-header.js create mode 100644 web/src/Services/auth.service.js create mode 100644 web/src/Services/user.service.js diff --git a/.env.example b/.env.example index b75930d0..ca0c2360 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,8 @@ MYSQL_USER= MYSQL_PASS= MYSQL_DB= -REDIS_URL= +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 REDIS_DB= REDIS_PREFIX="gs:" REDIS_AUTH="" @@ -14,4 +15,4 @@ JWT_SECRET= JWT_EXPIRY=86400 REVERSE_PROXIES=127.0.0.1 -PORT=42069 \ No newline at end of file +PORT=42069 diff --git a/.gitignore b/.gitignore index fc1cf7ae..eda2b7ed 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ *.so *.dylib .env +web/.env.production +web/.env.development + # Test binary, built with `go test -c` *.test diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c352f67a..1c332231 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - bundle variables: - VERSION: 0.0.1 + VERSION: 0.0.2 build-go: image: golang:1.16.2 diff --git a/cmd/go-scrobble/main.go b/cmd/go-scrobble/main.go index f88e131f..a17f3291 100644 --- a/cmd/go-scrobble/main.go +++ b/cmd/go-scrobble/main.go @@ -43,10 +43,14 @@ func main() { port = "42069" } - // Boot up DB connection for life of application + // Boot up DB connection goscrobble.InitDb() defer goscrobble.CloseDbConn() + // Boot up Redis connection + goscrobble.InitRedis() + defer goscrobble.CloseRedisConn() + // Boot up API webserver \o/ goscrobble.HandleRequests(port) } diff --git a/docs/changelog.md b/docs/changelog.md index eb96f457..b60495cf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,10 @@ +# 0.0.2 +- Login flow working.. +- Jellyfin scrobble working +- Returns scrobbles via API for authed users /api/v1/user/{uuid}/scrobble +- Add redis handler + funcs +- Move middleware to pass in uuid as needed + # 0.0.1 - Initial commit - Added basic registration/login flow diff --git a/docs/config.md b/docs/config.md index 00672e1b..f74f2eca 100644 --- a/docs/config.md +++ b/docs/config.md @@ -11,12 +11,13 @@ These are stored in `web/.env.production` and `web/.env.development` MYSQL_PASS= // MySQL Password MYSQL_DB= // MySQL Database - REDIS_URL= // Redis host + REDIS_HOST=127.0.0.1 // Redis host + REDIS_PORT= // Redis port (defaults 6379) REDIS_DB=4 // Redis DB REDIS_PREFIX="gs:" // Redis key prefix REDIS_AUTH="" // Redis password - TIMEZONE= // Used for MySQL connection + TIMEZONE= // Unix Timezone. Used for MySQL connection JWT_SECRET= // 32+ Char JWT secret JWT_EXPIRY=86400 // JWT expiry diff --git a/go.mod b/go.mod index 0effb27f..356355e9 100644 --- a/go.mod +++ b/go.mod @@ -10,20 +10,20 @@ require ( github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/go-redis/redis/v8 v8.8.0 // indirect github.com/go-sql-driver/mysql v1.5.0 github.com/gogo/protobuf v1.3.1 // indirect github.com/golang-migrate/migrate v3.5.4+incompatible github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/mux v1.8.0 github.com/joho/godotenv v1.3.0 + github.com/mitchellh/mapstructure v1.4.1 github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.7.0 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d // indirect - golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect google.golang.org/grpc v1.33.1 // indirect diff --git a/go.sum b/go.sum index 3ea418fd..fd1e7837 100644 --- a/go.sum +++ b/go.sum @@ -3,13 +3,18 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= @@ -22,6 +27,10 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.8.0 h1:fDZP58UN/1RD3DjtTXP/fFZ04TFohSYhjZDkcDe2dnw= +github.com/go-redis/redis/v8 v8.8.0/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= @@ -39,6 +48,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -46,14 +56,25 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -68,35 +89,59 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= +go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= +go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= +go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= +go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= +go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= +go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= 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-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM= golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM= golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= @@ -107,7 +152,12 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -132,5 +182,11 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/goscrobble/db.go b/internal/goscrobble/db.go index 210a169f..f413800a 100644 --- a/internal/goscrobble/db.go +++ b/internal/goscrobble/db.go @@ -29,7 +29,7 @@ func InitDb() { dbTz = "&loc=" + strings.Replace(timeZone, "/", fmt.Sprintf("%%2F"), 1) } - dbConn, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+")/"+dbName+"?multiStatements=true"+dbTz) + dbConn, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+")/"+dbName+"?multiStatements=true&parseTime=true"+dbTz) if err != nil { panic(err) } diff --git a/internal/goscrobble/jwt.go b/internal/goscrobble/jwt.go index 7b0948fb..34796dcf 100644 --- a/internal/goscrobble/jwt.go +++ b/internal/goscrobble/jwt.go @@ -1,7 +1,6 @@ package goscrobble import ( - "net/http" "time" "github.com/dgrijalva/jwt-go" @@ -13,34 +12,51 @@ var JwtToken []byte // JwtExpiry - Expiry in seconds var JwtExpiry time.Duration -// Store custom claims here -type Claims struct { - UUID string `json:"uuid"` +type CustomClaims struct { + Username string `json:"username"` + Email string `json:"email"` jwt.StandardClaims } -// verifyToken - Verifies the JWT is valid -func verifyToken(token string, w http.ResponseWriter) bool { - // Initialize a new instance of `Claims` - claims := &Claims{} +func generateJWTToken(user User) (string, error) { + atClaims := jwt.MapClaims{} + atClaims["sub"] = user.UUID + atClaims["username"] = user.Username + atClaims["email"] = user.Email + atClaims["iat"] = time.Now().Unix() + atClaims["exp"] = time.Now().Add(JwtExpiry).Unix() + at := jwt.NewWithClaims(jwt.SigningMethodHS512, atClaims) + token, err := at.SignedString(JwtToken) + if err != nil { + return "", err + } - tkn, err := jwt.ParseWithClaims(token, claims, func(JwtToken *jwt.Token) (interface{}, error) { + return token, nil +} + +// verifyToken - Verifies the JWT is valid +func verifyJWTToken(token string) (CustomClaims, error) { + // Initialize a new instance of `Claims` + claims := CustomClaims{} + _, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { return JwtToken, nil }) + // Verify Signature if err != nil { - if err == jwt.ErrSignatureInvalid { - w.WriteHeader(http.StatusUnauthorized) - return false - } - - w.WriteHeader(http.StatusBadRequest) - return false - } - if !tkn.Valid { - w.WriteHeader(http.StatusUnauthorized) - return false + return claims, err } - return true + // Verify expiry + err = claims.Valid() + if err != nil { + return claims, err + } + + return claims, err +} + +func getClaims(token *jwt.Token) CustomClaims { + claims, _ := token.Claims.(CustomClaims) + return claims } diff --git a/internal/goscrobble/redis.go b/internal/goscrobble/redis.go new file mode 100644 index 00000000..a85a4454 --- /dev/null +++ b/internal/goscrobble/redis.go @@ -0,0 +1,75 @@ +package goscrobble + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/go-redis/redis/v8" +) + +var redisDb *redis.Client +var redisPrefix string + +var ctx = context.Background() + +// InitRedis - Boot redis connection! +func InitRedis() { + redisHost := os.Getenv("REDIS_HOST") + redisPort := os.Getenv("REDIS_PORT") + redisDatabase := os.Getenv("REDIS_DB") + redisAuth := os.Getenv("REDIS_AUTH") + redisPrefix = os.Getenv("REDIS_PREFIX") + + redisDbNum := 0 + if redisDatabase != "" { + redisDbNum, _ = strconv.Atoi(redisDatabase) + } + + // Create new connection + redisDb = redis.NewClient(&redis.Options{ + Addr: redisHost + ":" + redisPort, + Password: redisAuth, + DB: redisDbNum, + }) + + // Lets just check it's active.. + err := redisDb.Set(ctx, "testSetKey", "value", 0).Err() + if err != nil { + panic(err) + } + + redisDb.Del(ctx, "testSetKey") + fmt.Println("Redis connected") +} + +func CloseRedisConn() { + redisDb.Close() +} + +// setRedis - Uses default 24 hour TTL +func setRedisVal(key string, val string) error { + ttl := time.Hour * time.Duration(24) + return setRedisValTtl(key, val, ttl) +} + +// setRedisTtl - Allows custom TTL +func setRedisValTtl(key string, val string, ttl time.Duration) error { + return redisDb.Set(ctx, redisPrefix+key, val, 0).Err() +} + +// getRedisVal - Returns value if exists +func getRedisVal(key string) string { + val, err := redisDb.Get(ctx, redisPrefix+key).Result() + if err != nil { + if err == redis.Nil { + return "" + } + log.Printf("Failed to fetch redis key (%+v) Error: %+v", key, err) + } + + return val +} diff --git a/internal/goscrobble/scrobble.go b/internal/goscrobble/scrobble.go index 48e63981..9de9999c 100644 --- a/internal/goscrobble/scrobble.go +++ b/internal/goscrobble/scrobble.go @@ -16,6 +16,25 @@ type Scrobble struct { Track string `json:"track"` } +type ScrobbleRequest struct { + Meta ScrobbleRequestMeta `json:"meta"` + Items []ScrobbleRequestItem `json:"items"` +} + +type ScrobbleRequestMeta struct { + Count int `json:"count"` + Total int `json:"total"` + Page int `json:"page"` +} + +type ScrobbleRequestItem struct { + UUID string `json:"uuid"` + Timestamp time.Time `json:"time"` + Artist string `json:"artist"` + Album string `json:"album"` + Track string `json:"track"` +} + // insertScrobble - This will return if it exists or create it based on MBID > Name func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error { err := insertNewScrobble(user, track, ip, tx) @@ -27,19 +46,66 @@ func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error { return nil } -func fetchScrobble(col string, val string, tx *sql.Tx) Scrobble { - var scrobble Scrobble - err := tx.QueryRow( - "SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `user`, `track` FROM `scrobbles` WHERE `"+col+"` = ?", - val).Scan(&scrobble.Uuid, &scrobble.CreatedAt, &scrobble.CreatedIp, &scrobble.User, &scrobble.Track) +func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) { + scrobbleReq := ScrobbleRequest{} + var count int + + // Yeah this isn't great. But for now.. it works! Cache later + total, err := getDbCount( + "SELECT COUNT(*) FROM `scrobbles` "+ + "JOIN tracks ON scrobbles.track = tracks.uuid "+ + "JOIN track_artist ON track_artist.track = tracks.uuid "+ + "JOIN track_album ON track_album.track = tracks.uuid "+ + "JOIN artists ON track_artist.artist = artists.uuid "+ + "JOIN albums ON track_album.album = albums.uuid "+ + "JOIN users ON scrobbles.user = users.uuid "+ + "WHERE user = UUID_TO_BIN(?, true)", + userUuid) if err != nil { - if err != sql.ErrNoRows { - log.Printf("Error fetching scrobbles: %+v", err) - } + log.Printf("Failed to fetch scrobble count: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") } - return scrobble + rows, err := db.Query( + "SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, `artists`.`name`, `albums`.`name`,`tracks`.`name` FROM `scrobbles` "+ + "JOIN tracks ON scrobbles.track = tracks.uuid "+ + "JOIN track_artist ON track_artist.track = tracks.uuid "+ + "JOIN track_album ON track_album.track = tracks.uuid "+ + "JOIN artists ON track_artist.artist = artists.uuid "+ + "JOIN albums ON track_album.album = albums.uuid "+ + "JOIN users ON scrobbles.user = users.uuid "+ + "WHERE user = UUID_TO_BIN(?, true) "+ + "ORDER BY scrobbles.created_at DESC LIMIT 500", + userUuid) + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + defer rows.Close() + + for rows.Next() { + item := ScrobbleRequestItem{} + err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist, &item.Album, &item.Track) + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + count++ + scrobbleReq.Items = append(scrobbleReq.Items, item) + } + + err = rows.Err() + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + + scrobbleReq.Meta.Count = count + scrobbleReq.Meta.Total = total + scrobbleReq.Meta.Page = page + + return scrobbleReq, nil } func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error { diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index 5e08cdd1..fb8007ce 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -46,16 +46,14 @@ func HandleRequests(port string) { v1 := r.PathPrefix("/api/v1").Subrouter() // Static Token for /ingress - v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)) + v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST") // JWT Auth - // v1.HandleFunc("/profile/{id}", jwtMiddleware(handleIngress)) + v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET") // No Auth v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST") v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST") - // For now just trash JWT in frontend until we have full state management "Good enough" - // v1.HandleFunc("/logout", handleIngress).Methods("POST") // This just prevents it serving frontend stuff over /api r.PathPrefix("/api") @@ -65,9 +63,10 @@ func HandleRequests(port string) { r.PathPrefix("/").Handler(spa) c := cors.New(cors.Options{ - // Grrrr CORS + // Grrrr CORS. To clean up at a later date AllowedOrigins: []string{"*"}, AllowCredentials: true, + AllowedHeaders: []string{"*"}, }) handler := c.Handler(r) @@ -97,14 +96,24 @@ func throwBadReq(w http.ResponseWriter, m string) { http.Error(w, err.Error(), http.StatusBadRequest) } +// throwOkError - Throws a 403 +func throwOkError(w http.ResponseWriter, m string) { + jr := jsonResponse{ + Err: m, + } + js, _ := json.Marshal(&jr) + w.WriteHeader(http.StatusOK) + w.Write(js) +} + // throwOkMessage - Throws a happy 200 func throwOkMessage(w http.ResponseWriter, m string) { jr := jsonResponse{ Msg: m, } js, _ := json.Marshal(&jr) - err := errors.New(string(js)) - http.Error(w, err.Error(), http.StatusOK) + w.WriteHeader(http.StatusOK) + w.Write(js) } // generateJsonMessage - Generates a message:str response @@ -126,7 +135,7 @@ func generateJsonError(m string) []byte { } // tokenMiddleware - Validates token to a user -func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc { +func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fullToken := r.Header.Get("Authorization") authToken := strings.Replace(fullToken, "Bearer ", "", 1) @@ -140,17 +149,29 @@ func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc { return } - // Lets tack this on the request for now.. - r.Header.Set("UserUUID", userUuid) - next(w, r) + next(w, r, userUuid) } } // jwtMiddleware - Validates middleware to a user -func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc { +func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - throwUnauthorized(w, "Invalid JWT Token") - next(w, r) + fullToken := r.Header.Get("Authorization") + authToken := strings.Replace(fullToken, "Bearer ", "", 1) + claims, err := verifyJWTToken(authToken) + if err != nil { + throwUnauthorized(w, "Invalid JWT Token") + return + } + + var v string + for k, v := range mux.Vars(r) { + if k == "id" { + log.Printf("key=%v, value=%v", k, v) + } + } + + next(w, r, claims.Subject, v) } } @@ -188,9 +209,7 @@ func handleRegister(w http.ResponseWriter, r *http.Request) { return } - msg := generateJsonMessage("User created succesfully. You may now login") - w.WriteHeader(http.StatusCreated) - w.Write(msg) + throwOkMessage(w, "User created succesfully. You may now login") } // handleLogin - Does as it says! @@ -206,7 +225,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { ip := getUserIp(r) data, err := loginUser(&logReq, ip) if err != nil { - throwOkMessage(w, err.Error()) + throwOkError(w, err.Error()) return } @@ -215,26 +234,29 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { } // serveEndpoint - API stuffs -func handleIngress(w http.ResponseWriter, r *http.Request) { +func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) { bodyJson, err := decodeJson(r.Body) if err != nil { // If we can't decode. Lets tell them nicely. http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest) return } + ingressType := strings.Replace(r.URL.Path, "/api/v1/ingress/", "", 1) switch ingressType { case "jellyfin": tx, _ := db.Begin() + ip := getUserIp(r) - err := ParseJellyfinInput(r.Header.Get("UserUUID"), bodyJson, ip, tx) + err := ParseJellyfinInput(userUuid, bodyJson, ip, tx) if err != nil { log.Printf("Error inserting track: %+v", err) tx.Rollback() throwBadReq(w, err.Error()) return } + err = tx.Commit() if err != nil { throwBadReq(w, err.Error()) @@ -248,6 +270,20 @@ func handleIngress(w http.ResponseWriter, r *http.Request) { throwBadReq(w, "Unknown ingress type") } +// fetchScrobbles - Return an array of scrobbles +func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) { + resp, err := fetchScrobblesForUser(reqUser, 1) + if err != nil { + throwBadReq(w, "Failed to fetch scrobbles") + return + } + + // Fetch last 500 scrobbles + json, _ := json.Marshal(&resp) + w.WriteHeader(http.StatusOK) + w.Write(json) +} + // FRONTEND HANDLING // ServerHTTP - Frontend server diff --git a/internal/goscrobble/tokens.go b/internal/goscrobble/tokens.go index 8505500d..5c6f1364 100644 --- a/internal/goscrobble/tokens.go +++ b/internal/goscrobble/tokens.go @@ -17,9 +17,16 @@ func generateToken(n int) string { func getUserForToken(token string) (string, error) { var uuid string - err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true) FROM `users` WHERE `token` = ? AND `active` = 1", token).Scan(&uuid) - if err != nil { - return "", errors.New("Invalid Token") + cachedKey := getRedisVal("user_token:" + token) + if cachedKey == "" { + err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true) FROM `users` WHERE `token` = ? AND `active` = 1", token).Scan(&uuid) + if err != nil { + return "", errors.New("Invalid Token") + } + setRedisVal("user_token:"+token, uuid) + } else { + uuid = cachedKey } + return uuid, nil } diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index dd5f0340..c06a28cf 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "github.com/dgrijalva/jwt-go" "golang.org/x/crypto/bcrypt" ) @@ -104,14 +103,16 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) { } if strings.Contains(logReq.Username, "@") { - err := db.QueryRow("SELECT BIN_TO_UUID(uuid), username, email, password FROM users WHERE email = ? AND active = 1", logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) + err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password` FROM `users` WHERE `email` = ? AND `active` = 1", + logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) if err != nil { if err == sql.ErrNoRows { return resp, errors.New("Invalid Username or Password") } } } else { - err := db.QueryRow("SELECT BIN_TO_UUID(uuid), username, email, password FROM users WHERE username = ? AND active = 1", logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) + err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password` FROM `users` WHERE `username` = ? AND `active` = 1", + logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) if err == sql.ErrNoRows { return resp, errors.New("Invalid Username or Password") } @@ -122,7 +123,7 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) { } // Issue JWT + Response - token, err := generateJwt(user) + token, err := generateJWTToken(user) if err != nil { log.Printf("Error generating JWT: %v", err) return resp, errors.New("Error logging in") @@ -136,21 +137,6 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) { return resp, nil } -func generateJwt(user User) (string, error) { - atClaims := jwt.MapClaims{} - atClaims["sub"] = user.UUID - atClaims["username"] = user.Username - atClaims["email"] = user.Email - atClaims["exp"] = time.Now().Add(JwtExpiry).Unix() - at := jwt.NewWithClaims(jwt.SigningMethodHS512, atClaims) - token, err := at.SignedString(JwtToken) - if err != nil { - return "", err - } - - return token, nil -} - // insertUser - Does the dirtywork! func insertUser(username string, email string, password []byte, ip net.IP) error { token := generateToken(32) diff --git a/internal/goscrobble/utils.go b/internal/goscrobble/utils.go index 29c11c9e..c07554d0 100644 --- a/internal/goscrobble/utils.go +++ b/internal/goscrobble/utils.go @@ -3,6 +3,7 @@ package goscrobble import ( "encoding/hex" "encoding/json" + "fmt" "io" "math/big" "net" @@ -86,3 +87,7 @@ func Inet6_Aton(ip net.IP) string { ipHex := hex.EncodeToString(ipInt.Bytes()) return ipHex } + +func calcPageOffsetString(page int, offset int) string { + return fmt.Sprintf("%d", page*offset) +} diff --git a/web/.env.development b/web/.env.development deleted file mode 100644 index 1fd637b1..00000000 --- a/web/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_API_URL=http://127.0.0.1:42069 -REACT_APP_REGISTRATION_DISABLED=false \ No newline at end of file diff --git a/web/.env.production b/web/.env.production deleted file mode 100644 index 18f758e9..00000000 --- a/web/.env.production +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_API_URL=https://goscrobble.com -REACT_APP_REGISTRATION_DISABLED=true \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index b6987644..a531c8b1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,8 +12,10 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.3", + "axios": "^0.21.1", "bootstrap": "^4.6.0", "formik": "^2.2.6", + "jwt-decode": "^3.1.2", "react": "^17.0.2", "react-bootstrap": "^1.5.2", "react-cookie": "^4.0.3", @@ -24,11 +26,15 @@ "react-spinners": "^0.10.6", "react-toast": "^1.0.1", "react-toast-notifications": "^2.4.3", + "react-toastify": "^7.0.3", "reactstrap": "^8.9.0", + "redux": "^4.0.5", "redux-persist": "^6.0.0", + "redux-thunk": "^2.3.0", "web-vitals": "^1.1.1" }, "devDependencies": { + "redux-devtools-extension": "^2.13.9", "webpack-dev-server": "^3.11.1" } }, @@ -4175,6 +4181,14 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -5560,6 +5574,14 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -12841,6 +12863,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -16713,6 +16740,18 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, + "node_modules/react-toastify": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.3.tgz", + "integrity": "sha512-cxZ5rfurC8LzcZQMTYc8RHIkQTs+BFur18Pzk6Loz6uS8OXUWm6nXVlH/wqglz4Z7UAE8xxcF5mRjfE13487uQ==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -16937,6 +16976,15 @@ "symbol-observable": "^1.2.0" } }, + "node_modules/redux-devtools-extension": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==", + "dev": true, + "peerDependencies": { + "redux": "^3.1.0 || ^4.0.0" + } + }, "node_modules/redux-persist": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", @@ -25420,6 +25468,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz", "integrity": "sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -26548,6 +26604,11 @@ "wrap-ansi": "^6.2.0" } }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -32139,6 +32200,11 @@ "object.assign": "^4.1.2" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -35246,6 +35312,14 @@ "react-transition-group": "^4.4.1" } }, + "react-toastify": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.3.tgz", + "integrity": "sha512-cxZ5rfurC8LzcZQMTYc8RHIkQTs+BFur18Pzk6Loz6uS8OXUWm6nXVlH/wqglz4Z7UAE8xxcF5mRjfE13487uQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -35422,6 +35496,13 @@ "symbol-observable": "^1.2.0" } }, + "redux-devtools-extension": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==", + "dev": true, + "requires": {} + }, "redux-persist": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", diff --git a/web/package.json b/web/package.json index 920e3084..4ee6a3ed 100644 --- a/web/package.json +++ b/web/package.json @@ -7,8 +7,10 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.3", + "axios": "^0.21.1", "bootstrap": "^4.6.0", "formik": "^2.2.6", + "jwt-decode": "^3.1.2", "react": "^17.0.2", "react-bootstrap": "^1.5.2", "react-cookie": "^4.0.3", @@ -19,8 +21,11 @@ "react-spinners": "^0.10.6", "react-toast": "^1.0.1", "react-toast-notifications": "^2.4.3", + "react-toastify": "^7.0.3", "reactstrap": "^8.9.0", + "redux": "^4.0.5", "redux-persist": "^6.0.0", + "redux-thunk": "^2.3.0", "web-vitals": "^1.1.1" }, "scripts": { @@ -48,6 +53,7 @@ ] }, "devDependencies": { + "redux-devtools-extension": "^2.13.9", "webpack-dev-server": "^3.11.1" } } diff --git a/web/src/Actions/auth.js b/web/src/Actions/auth.js new file mode 100644 index 00000000..d33e8c9c --- /dev/null +++ b/web/src/Actions/auth.js @@ -0,0 +1,87 @@ +import { + REGISTER_SUCCESS, + REGISTER_FAIL, + LOGIN_SUCCESS, + LOGIN_FAIL, + SET_MESSAGE, + } from "./types"; + import { toast } from 'react-toastify' + + import AuthService from "../Services/auth.service"; + + export const register = (username, email, password) => (dispatch) => { + return AuthService.register(username, email, password).then( + (response) => { + dispatch({ + type: REGISTER_SUCCESS, + }); + + return Promise.resolve(); + }, + (error) => { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + + dispatch({ + type: REGISTER_FAIL, + }); + + dispatch({ + type: SET_MESSAGE, + payload: message, + }); + + return Promise.reject(); + } + ); + }; + + export const login = (username, password) => (dispatch) => { + return AuthService.login(username, password).then( + (data) => { + if (data.token) { + toast.success('Login Success'); + dispatch({ + type: LOGIN_SUCCESS, + payload: { user: data }, + }); + return Promise.resolve(); + } + + toast.error(data.error ? data.error: 'An Unknown Error has occurred') + dispatch({ + type: LOGIN_FAIL, + }); + return Promise.reject(); + }, + (error) => { + const message = + (error.response && + error.response.data && + error.response.data.error) || + error.message || + error.toString(); + + toast.error('Error: ' + message) + dispatch({ + type: LOGIN_FAIL, + }); + + // dispatch({ + // type: SET_MESSAGE, + // payload: message, + // }); + + return Promise.reject(); + } + ); + }; + + export const logout = () => () => { + AuthService.logout(); + window.location.reload(); + }; diff --git a/web/src/Actions/message.js b/web/src/Actions/message.js new file mode 100644 index 00000000..c14afb2a --- /dev/null +++ b/web/src/Actions/message.js @@ -0,0 +1,10 @@ +import { SET_MESSAGE, CLEAR_MESSAGE } from "./types"; + +export const setMessage = (message) => ({ + type: SET_MESSAGE, + payload: message, +}); + +export const clearMessage = () => ({ + type: CLEAR_MESSAGE, +}); diff --git a/web/src/Actions/types.js b/web/src/Actions/types.js new file mode 100644 index 00000000..4a7dd4cf --- /dev/null +++ b/web/src/Actions/types.js @@ -0,0 +1,8 @@ +export const REGISTER_SUCCESS = "REGISTER_SUCCESS"; +export const REGISTER_FAIL = "REGISTER_FAIL"; +export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; +export const LOGIN_FAIL = "LOGIN_FAIL"; +export const LOGOUT = "LOGOUT"; + +export const SET_MESSAGE = "SET_MESSAGE"; +export const CLEAR_MESSAGE = "CLEAR_MESSAGE"; diff --git a/web/src/App.js b/web/src/App.js index 661ad89a..c31fdac6 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,44 +1,82 @@ import './App.css'; -import Home from './Components/Pages/Home'; -import About from './Components/Pages/About'; -import Help from './Components/Pages/Help'; -import Login from './Components/Pages/Login'; -import Settings from './Components/Pages/Settings'; -import Register from './Components/Pages/Register'; +import Home from './Pages/Home'; +import About from './Pages/About'; + +import Dashboard from './Pages/Dashboard'; +import Admin from './Pages/Admin'; +import Profile from './Pages/Profile'; +import Login from './Pages/Login'; +import Settings from './Pages/Settings'; +import Register from './Pages/Register'; import Navigation from './Components/Navigation'; +import { logout } from './Actions/auth'; +import { clearMessage } from './Actions/message'; +import { history } from './Helpers/history'; import { Route, Switch, withRouter } from 'react-router-dom'; -import { connect } from "react-redux"; -import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; +import { connect } from 'react-redux'; +import { Component } from 'react'; +import 'bootstrap/dist/css/bootstrap.min.css'; function mapStateToProps(state) { + const { user } = state.auth; return { - isLoggedIn: state + user, }; } -function mapDispatchToProps(dispatch) { - return { - logIn: () => dispatch({type: true}), - logOut: () => dispatch({type: false}) - }; -} +class App extends Component { + constructor(props) { + super(props); + this.logOut = this.logOut.bind(this); -const App = () => { - let exact = true - return ( -
- - - - - - - - - -
- ); -} + this.state = { + // showAdminBoard: false, + currentUser: undefined, + // Don't even ask.. apparently you can't pass + // exact="true".. it has to be a bool :| + true: true, + }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App)); + history.listen((location) => { + props.dispatch(clearMessage()); // clear message when changing location + }); + } + + componentDidMount() { + const user = this.props.user; + + if (user) { + this.setState({ + currentUser: user, + // showAdminBoard: user.roles.includes("ROLE_ADMIN"), + }); + } + } + + logOut() { + this.props.dispatch(logout()); + } + + render() { + // const { currentUser, showAdminBoard } = this.state; + return ( +
+ + + + + + + + + + + + + +
+ ); + } +} +export default withRouter(connect(mapStateToProps)(App)); \ No newline at end of file diff --git a/web/src/Components/Api.js b/web/src/Components/Api.js deleted file mode 100644 index 96aca770..00000000 --- a/web/src/Components/Api.js +++ /dev/null @@ -1 +0,0 @@ -// https://stackoverflow.com/questions/38397653/redux-what-is-the-correct-place-to-save-cookie-after-login-request \ No newline at end of file diff --git a/web/src/Components/AppProvider.js b/web/src/Components/AppProvider.js deleted file mode 100644 index aac876a1..00000000 --- a/web/src/Components/AppProvider.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { Provider } from 'react-redux'; -import { persistStore } from 'redux-persist'; - -class AppProvider extends Component { - static propTypes = { - store: PropTypes.object.isRequired, - children: PropTypes.node - } - - constructor(props) { - super(props); - - this.state = { rehydrated: false }; - } - - componentWillMount() { - const opts = { - whitelist: ['user'] // <-- Your auth/user reducer storing the cookie - }; - - persistStore(this.props.store, opts, () => { - this.setState({ rehydrated: true }); - }); - } - - render() { - if (!this.state.rehydrated) { - return null; - } - - return ( - - {this.props.children} - - ); - } -} - -AppProvider.propTypes = { - store: PropTypes.object.isRequired, - children: PropTypes.node -} - -export default AppProvider; \ No newline at end of file diff --git a/web/src/Components/Navigation.js b/web/src/Components/Navigation.js index 4ca9245b..0514b4c5 100644 --- a/web/src/Components/Navigation.js +++ b/web/src/Components/Navigation.js @@ -3,70 +3,106 @@ import { Navbar, NavbarBrand } from 'reactstrap'; import { Link } from 'react-router-dom'; import logo from '../logo.png'; import './Navigation.css'; +import { connect } from 'react-redux'; +import { logout } from '../Actions/auth'; const menuItems = [ 'Home', - 'Help', 'About', ]; +const loggedInMenuItems = [ + 'Dashboard', + 'About', +] class Navigation extends Component { - constructor(props) { - super(props); - // Yeah I know you might not hit home.. but I can't get the - // path based finder thing working on initial load :sweatsmile: - console.log(this.props.initLocation) - this.state = { isLoggedIn: false, active: "Home" }; - } + constructor(props) { + super(props); + // Yeah I know you might not hit home.. but I can't get the + // path based finder thing working on initial load :sweatsmile: + this.state = { active: "Home" }; + } - toggleLogin() { - this.setState({ isLoggedIn: !this.state.isLoggedIn }) - } + componentDidMount() { + const isLoggedIn = this.props.isLoggedIn; - _handleClick(menuItem) { - this.setState({ active: menuItem }); - } - - render() { - const activeStyle = { color: '#FFFFFF' }; - - const renderAuthButtons = () => { - if (this.state.isLoggedIn) { - return
- Profile - Logout -
; - } else { - return
- Login - Register -
; - } - } - - - - - return ( -
- - logo GoScrobble - {menuItems.map(menuItem => - - {menuItem} - - )} - {renderAuthButtons()} - -
- ); + if (isLoggedIn) { + this.setState({ + isLoggedIn: true, + }); } } -export default Navigation; \ No newline at end of file + _handleClick(menuItem) { + this.setState({ active: menuItem }); + } + + render() { + const activeStyle = { color: '#FFFFFF' }; + + const renderAuthButtons = () => { + if (this.state.isLoggedIn) { + return
+ Profile + Logout +
; + } else { + return
+ Login + Register +
; + } + } + + const renderMenuButtons = () => { + if (this.state.isLoggedIn) { + return
+ {loggedInMenuItems.map(menuItem => + + {menuItem} + + )} +
; + } else { + return
+ {menuItems.map(menuItem => + + {menuItem} + + )} +
; + } + } + + return ( +
+ + logo GoScrobble + {renderMenuButtons()} + {renderAuthButtons()} + +
+ ); + } +} + +function mapStateToProps(state) { + const { isLoggedIn } = state.auth; + return { + isLoggedIn, + }; +} + +export default connect(mapStateToProps)(Navigation); \ No newline at end of file diff --git a/web/src/Components/Pages/Help.css b/web/src/Components/Pages/Help.css deleted file mode 100644 index 8a669070..00000000 --- a/web/src/Components/Pages/Help.css +++ /dev/null @@ -1,4 +0,0 @@ -.helpBody { - padding: 20px 5px 5px 5px; - font-size: 16pt; -} \ No newline at end of file diff --git a/web/src/Components/Pages/Help.js b/web/src/Components/Pages/Help.js deleted file mode 100644 index 425cecca..00000000 --- a/web/src/Components/Pages/Help.js +++ /dev/null @@ -1,17 +0,0 @@ -import '../../App.css'; -import './Help.css'; - -function Help() { - return ( -
-

- Help Docs -

-

- Jellyfin Configuration
-

-
- ); -} - -export default Help; diff --git a/web/src/Components/Pages/Login.js b/web/src/Components/Pages/Login.js deleted file mode 100644 index b455a618..00000000 --- a/web/src/Components/Pages/Login.js +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import '../../App.css'; -import './Login.css'; -import { Button } from 'reactstrap'; -import { Formik, Form, Field } from 'formik'; -import { useToasts } from 'react-toast-notifications'; -import ScaleLoader from "react-spinners/ScaleLoader"; - -function withToast(Component) { - return function WrappedComponent(props) { - const toastFuncs = useToasts() - return ; - } -} - -class Login extends React.Component { - constructor(props) { - super(props); - this.state = {username: '', password: '', loading: false}; - this.handleUsernameChange = this.handleUsernameChange.bind(this); - this.handlePasswordChange = this.handlePasswordChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleUsernameChange(event) { - this.setState({username: event.target.value}); - } - - handlePasswordChange(event) { - this.setState({password: event.target.value}); - } - - handleSubmit(values) { - this.setState({loading: true}); - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - timeout: 5000, - body: JSON.stringify({ - username: values.username, - password: values.password, - }) - }; - const apiUrl = process.env.REACT_APP_API_URL + '/api/v1/login'; - fetch(apiUrl, requestOptions) - .then((response) => { - if (response.status === 429) { - this.props.addToast("Rate limited. Please try again soon", { appearance: 'error' }); - return "{}" - } else { - return response.json() - } - }) - .then((function(data) { - if (data.error) { - this.props.addToast(data.error, { appearance: 'error' }); - } else if (data.token) { - this.props.addToast(data.token, { appearance: 'success' }); - } - this.setState({loading: false}); - }).bind(this)) - .catch(() => { - this.props.addToast('Error submitting form. Please try again', { appearance: 'error' }); - this.setState({loading: false}); - }); - } - - render() { - let trueBool = true; - return ( -
-

- Login -

-
- this.handleSubmit(values)} - > -
- -
- -

- -
-
-
-
- ); - } -} - -export default withToast(Login); diff --git a/web/src/Helpers/history.js b/web/src/Helpers/history.js new file mode 100644 index 00000000..7001ceef --- /dev/null +++ b/web/src/Helpers/history.js @@ -0,0 +1,3 @@ +import { createBrowserHistory } from "history"; + +export const history = createBrowserHistory(); diff --git a/web/src/Components/Pages/About.css b/web/src/Pages/About.css similarity index 100% rename from web/src/Components/Pages/About.css rename to web/src/Pages/About.css diff --git a/web/src/Components/Pages/About.js b/web/src/Pages/About.js similarity index 84% rename from web/src/Components/Pages/About.js rename to web/src/Pages/About.js index a5898837..e789ccfc 100644 --- a/web/src/Components/Pages/About.js +++ b/web/src/Pages/About.js @@ -1,4 +1,4 @@ -import '../../App.css'; +import '../App.css'; import './About.css'; function About() { @@ -8,7 +8,7 @@ function About() { About GoScrobble.com

- Go-Scrobble is an open source music scorbbling service written in Go and React.
+ Go-Scrobble is an open source music scorbbling service written in Go and React.

); diff --git a/web/src/Pages/Admin.js b/web/src/Pages/Admin.js new file mode 100644 index 00000000..e2927dfb --- /dev/null +++ b/web/src/Pages/Admin.js @@ -0,0 +1,45 @@ +import React, { Component } from "react"; + +import UserService from "../Services/user.service"; + +class Admin extends Component { + constructor(props) { + super(props); + + this.state = { + content: "" + }; + } + + componentDidMount() { + UserService.getAdminBoard().then( + response => { + this.setState({ + content: response.data + }); + }, + error => { + this.setState({ + content: + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString() + }); + } + ); + } + + render() { + return ( +
+
+

{this.state.content}

+
+
+ ); + } +} + +export default Admin; \ No newline at end of file diff --git a/web/src/Pages/Dashboard.css b/web/src/Pages/Dashboard.css new file mode 100644 index 00000000..5c221551 --- /dev/null +++ b/web/src/Pages/Dashboard.css @@ -0,0 +1,4 @@ +.dashboardBody { + padding: 20px 5px 5px 5px; + font-size: 16pt; +} \ No newline at end of file diff --git a/web/src/Pages/Dashboard.js b/web/src/Pages/Dashboard.js new file mode 100644 index 00000000..2b2ee512 --- /dev/null +++ b/web/src/Pages/Dashboard.js @@ -0,0 +1,35 @@ +import React from 'react'; +import '../App.css'; +import './Dashboard.css'; +import { connect } from 'react-redux'; + +class Dashboard extends React.Component { + componentDidMount() { + const { history } = this.props; + const isLoggedIn = this.props.isLoggedIn; + + if (!isLoggedIn) { + history.push("/login") + window.location.reload() + } + } + + render() { + return ( +
+

+ Hai Dashboard! +

+
+ ); + } +} + +function mapStateToProps(state) { + const { isLoggedIn } = state.auth; + return { + isLoggedIn, + }; +} + +export default connect(mapStateToProps)(Dashboard); diff --git a/web/src/Components/Pages/Home.js b/web/src/Pages/Home.js similarity index 87% rename from web/src/Components/Pages/Home.js rename to web/src/Pages/Home.js index 3abd5043..11b22fd8 100644 --- a/web/src/Components/Pages/Home.js +++ b/web/src/Pages/Home.js @@ -1,5 +1,5 @@ -import logo from '../../logo.png'; -import '../../App.css'; +import logo from '../logo.png'; +import '../App.css'; function Home() { return ( diff --git a/web/src/Components/Pages/Login.css b/web/src/Pages/Login.css similarity index 100% rename from web/src/Components/Pages/Login.css rename to web/src/Pages/Login.css diff --git a/web/src/Pages/Login.js b/web/src/Pages/Login.js new file mode 100644 index 00000000..274297c6 --- /dev/null +++ b/web/src/Pages/Login.js @@ -0,0 +1,92 @@ +import React from 'react'; +import '../App.css'; +import './Login.css'; +import { Button } from 'reactstrap'; +import { Formik, Form, Field } from 'formik'; +import ScaleLoader from 'react-spinners/ScaleLoader'; +import { connect } from 'react-redux'; +import { login } from '../Actions/auth'; + +class Login extends React.Component { + constructor(props) { + super(props); + this.state = {username: '', password: '', loading: false}; + } + + handleLogin(values) { + this.setState({loading: true}); + + const { dispatch, history } = this.props; + + dispatch(login(values.username, values.password)) + .then(() => { + this.setState({ + loading: false, + }); + history.push("/dashboard"); + window.location.reload(); + }) + .catch(() => { + this.setState({ + loading: false + }); + }); + } + + render() { + let trueBool = true; + return ( +
+

+ Login +

+
+ this.handleLogin(values)} + > +
+ +
+ +

+ +
+
+
+
+ ); + } +} + +function mapStateToProps(state) { + const { isLoggedIn } = state.auth; + const { message } = state.message; + return { + isLoggedIn, + message + }; +} + +export default connect(mapStateToProps)(Login); diff --git a/web/src/Pages/Profile.css b/web/src/Pages/Profile.css new file mode 100644 index 00000000..8a1acb4f --- /dev/null +++ b/web/src/Pages/Profile.css @@ -0,0 +1,4 @@ +.profileBody { + padding: 20px 5px 5px 5px; + font-size: 16pt; +} \ No newline at end of file diff --git a/web/src/Pages/Profile.js b/web/src/Pages/Profile.js new file mode 100644 index 00000000..66901513 --- /dev/null +++ b/web/src/Pages/Profile.js @@ -0,0 +1,35 @@ +import React from 'react'; +import '../App.css'; +import './Dashboard.css'; +import { connect } from 'react-redux'; + +class Profile extends React.Component { + componentDidMount() { + const { history } = this.props; + const isLoggedIn = this.props.isLoggedIn; + + if (!isLoggedIn) { + history.push("/login") + window.location.reload() + } + } + + render() { + return ( +
+

+ Hai User +

+
+ ); + } +} + +function mapStateToProps(state) { + const { isLoggedIn } = state.auth; + return { + isLoggedIn, + }; +} + +export default connect(mapStateToProps)(Profile); \ No newline at end of file diff --git a/web/src/Components/Pages/Register.css b/web/src/Pages/Register.css similarity index 100% rename from web/src/Components/Pages/Register.css rename to web/src/Pages/Register.css diff --git a/web/src/Components/Pages/Register.js b/web/src/Pages/Register.js similarity index 94% rename from web/src/Components/Pages/Register.js rename to web/src/Pages/Register.js index 8d32c3b3..f81a9454 100644 --- a/web/src/Components/Pages/Register.js +++ b/web/src/Pages/Register.js @@ -1,18 +1,10 @@ import React from 'react'; -import '../../App.css'; +import '../App.css'; import './Login.css'; import { Button } from 'reactstrap'; -import { useToasts } from 'react-toast-notifications'; import ScaleLoader from "react-spinners/ScaleLoader"; import { withRouter } from 'react-router-dom' -function withToast(Component) { - return function WrappedComponent(props) { - const toastFuncs = useToasts() - return ; - } -} - class Register extends React.Component { constructor(props) { super(props); @@ -165,4 +157,4 @@ class Register extends React.Component { } } -export default withRouter(withToast(Register)); +export default withRouter(Register); diff --git a/web/src/Components/Pages/Settings.css b/web/src/Pages/Settings.css similarity index 100% rename from web/src/Components/Pages/Settings.css rename to web/src/Pages/Settings.css diff --git a/web/src/Components/Pages/Settings.js b/web/src/Pages/Settings.js similarity index 96% rename from web/src/Components/Pages/Settings.js rename to web/src/Pages/Settings.js index 6b3b9f72..e517b361 100644 --- a/web/src/Components/Pages/Settings.js +++ b/web/src/Pages/Settings.js @@ -1,5 +1,5 @@ import React from 'react'; -import '../../App.css'; +import '../App.css'; import './Settings.css'; import { useToasts } from 'react-toast-notifications'; diff --git a/web/src/Reducers/auth.js b/web/src/Reducers/auth.js new file mode 100644 index 00000000..68b6ff77 --- /dev/null +++ b/web/src/Reducers/auth.js @@ -0,0 +1,50 @@ +import { + REGISTER_SUCCESS, + REGISTER_FAIL, + LOGIN_SUCCESS, + LOGIN_FAIL, + LOGOUT, + } from "../Actions/types"; + + const jwt = localStorage.getItem("jwt"); + + const initialState = jwt + ? { isLoggedIn: true, jwt } + : { isLoggedIn: false, jwt }; + + export default function authReducer(state = initialState, action) { + const { type, payload } = action; + + switch (type) { + case REGISTER_SUCCESS: + return { + ...state, + isLoggedIn: false, + }; + case REGISTER_FAIL: + return { + ...state, + isLoggedIn: false, + }; + case LOGIN_SUCCESS: + return { + ...state, + isLoggedIn: true, + user: payload.user, + }; + case LOGIN_FAIL: + return { + ...state, + isLoggedIn: false, + user: null, + }; + case LOGOUT: + return { + ...state, + isLoggedIn: false, + user: null, + }; + default: + return state; + } + } diff --git a/web/src/Reducers/index.js b/web/src/Reducers/index.js new file mode 100644 index 00000000..8bb57879 --- /dev/null +++ b/web/src/Reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import auth from "./auth"; +import message from "./message"; + +export default combineReducers({ + auth, + message, +}); diff --git a/web/src/Reducers/message.js b/web/src/Reducers/message.js new file mode 100644 index 00000000..edc6ed5d --- /dev/null +++ b/web/src/Reducers/message.js @@ -0,0 +1,18 @@ +import { SET_MESSAGE, CLEAR_MESSAGE } from "../Actions/types"; + +const initialState = {}; + +export default function message(state = initialState, action) { + const { type, payload } = action; + + switch (type) { + case SET_MESSAGE: + return { message: payload }; + + case CLEAR_MESSAGE: + return { message: "" }; + + default: + return state; + } +} diff --git a/web/src/Services/Actions.js b/web/src/Services/Actions.js new file mode 100644 index 00000000..e69de29b diff --git a/web/src/Services/auth-header.js b/web/src/Services/auth-header.js new file mode 100644 index 00000000..16eea59e --- /dev/null +++ b/web/src/Services/auth-header.js @@ -0,0 +1,9 @@ +export default function authHeader() { + const token = JSON.parse(localStorage.getItem('jwt')); + + if (token) { + return { Authorization: 'Bearer ' + token }; + } else { + return {}; + } +} diff --git a/web/src/Services/auth.service.js b/web/src/Services/auth.service.js new file mode 100644 index 00000000..40321255 --- /dev/null +++ b/web/src/Services/auth.service.js @@ -0,0 +1,35 @@ +import axios from "axios"; +import jwt from 'jwt-decode' // import dependency + +class AuthService { + login(username, password) { + return axios + .post(process.env.REACT_APP_API_URL + "login", { username, password }) + .then((response) => { + if (response.data.token) { + let user = jwt(response.data.token) + localStorage.setItem("jwt", response.data.token); + localStorage.setItem("uuid", user.sub); + localStorage.setItem("exp", user.exp); + } + + return response.data; + }); + } + + logout() { + localStorage.removeItem("jwt"); + localStorage.removeItem("uuid"); + localStorage.removeItem("exp"); + } + + register(username, email, password) { + return axios.post(process.env.REACT_APP_API_URL + "register", { + username, + email, + password, + }); + } +} + +export default new AuthService(); diff --git a/web/src/Services/user.service.js b/web/src/Services/user.service.js new file mode 100644 index 00000000..77b5391a --- /dev/null +++ b/web/src/Services/user.service.js @@ -0,0 +1,18 @@ +import axios from 'axios'; +import authHeader from './auth-header'; + +class UserService { + getPublicContent() { + return axios.get(process.env.REACT_APP_API_URL + 'all'); + } + + getUserBoard() { + return axios.get(process.env.REACT_APP_API_URL + 'user', { headers: authHeader() }); + } + + getAdminBoard() { + return axios.get(process.env.REACT_APP_API_URL + 'admin', { headers: authHeader() }); + } +} + +export default new UserService(); diff --git a/web/src/index.js b/web/src/index.js index 39c4a98a..516e9ac7 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -2,25 +2,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; -import { HashRouter } from 'react-router-dom' -import { ToastProvider } from 'react-toast-notifications'; +import { HashRouter } from 'react-router-dom'; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.min.css' +import { Provider } from 'react-redux'; -import { Provider } from 'react-redux' -import { createStore } from 'redux' - -const goScorbbleStore = (state = false, logIn) => { - return state = logIn -}; - -const store = createStore(goScorbbleStore); +import store from "./store"; ReactDOM.render( - - - - - - - , + + + + + + , document.getElementById('root') ); diff --git a/web/src/store.js b/web/src/store.js index 8b137891..03eff2c9 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -1 +1,13 @@ +import { createStore, applyMiddleware } from "redux"; +import { composeWithDevTools } from "redux-devtools-extension"; +import thunk from "redux-thunk"; +import rootReducer from "./Reducers"; +const middleware = [thunk]; + +const store = createStore( + rootReducer, + composeWithDevTools(applyMiddleware(...middleware)) +); + +export default store;