mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-22 00:21:55 +00:00
- 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
This commit is contained in:
parent
c83c086cdd
commit
5fd9d41069
@ -3,7 +3,8 @@ MYSQL_USER=
|
|||||||
MYSQL_PASS=
|
MYSQL_PASS=
|
||||||
MYSQL_DB=
|
MYSQL_DB=
|
||||||
|
|
||||||
REDIS_URL=
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
REDIS_DB=
|
REDIS_DB=
|
||||||
REDIS_PREFIX="gs:"
|
REDIS_PREFIX="gs:"
|
||||||
REDIS_AUTH=""
|
REDIS_AUTH=""
|
||||||
@ -14,4 +15,4 @@ JWT_SECRET=
|
|||||||
JWT_EXPIRY=86400
|
JWT_EXPIRY=86400
|
||||||
|
|
||||||
REVERSE_PROXIES=127.0.0.1
|
REVERSE_PROXIES=127.0.0.1
|
||||||
PORT=42069
|
PORT=42069
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,6 +6,9 @@
|
|||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
.env
|
.env
|
||||||
|
web/.env.production
|
||||||
|
web/.env.development
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ stages:
|
|||||||
- bundle
|
- bundle
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
VERSION: 0.0.1
|
VERSION: 0.0.2
|
||||||
|
|
||||||
build-go:
|
build-go:
|
||||||
image: golang:1.16.2
|
image: golang:1.16.2
|
||||||
|
@ -43,10 +43,14 @@ func main() {
|
|||||||
port = "42069"
|
port = "42069"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boot up DB connection for life of application
|
// Boot up DB connection
|
||||||
goscrobble.InitDb()
|
goscrobble.InitDb()
|
||||||
defer goscrobble.CloseDbConn()
|
defer goscrobble.CloseDbConn()
|
||||||
|
|
||||||
|
// Boot up Redis connection
|
||||||
|
goscrobble.InitRedis()
|
||||||
|
defer goscrobble.CloseRedisConn()
|
||||||
|
|
||||||
// Boot up API webserver \o/
|
// Boot up API webserver \o/
|
||||||
goscrobble.HandleRequests(port)
|
goscrobble.HandleRequests(port)
|
||||||
}
|
}
|
||||||
|
@ -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
|
# 0.0.1
|
||||||
- Initial commit
|
- Initial commit
|
||||||
- Added basic registration/login flow
|
- Added basic registration/login flow
|
||||||
|
@ -11,12 +11,13 @@ These are stored in `web/.env.production` and `web/.env.development`
|
|||||||
MYSQL_PASS= // MySQL Password
|
MYSQL_PASS= // MySQL Password
|
||||||
MYSQL_DB= // MySQL Database
|
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_DB=4 // Redis DB
|
||||||
REDIS_PREFIX="gs:" // Redis key prefix
|
REDIS_PREFIX="gs:" // Redis key prefix
|
||||||
REDIS_AUTH="" // Redis password
|
REDIS_AUTH="" // Redis password
|
||||||
|
|
||||||
TIMEZONE= // Used for MySQL connection
|
TIMEZONE= // Unix Timezone. Used for MySQL connection
|
||||||
|
|
||||||
JWT_SECRET= // 32+ Char JWT secret
|
JWT_SECRET= // 32+ Char JWT secret
|
||||||
JWT_EXPIRY=86400 // JWT expiry
|
JWT_EXPIRY=86400 // JWT expiry
|
||||||
|
4
go.mod
4
go.mod
@ -10,20 +10,20 @@ require (
|
|||||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect
|
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-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units 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/go-sql-driver/mysql v1.5.0
|
||||||
github.com/gogo/protobuf v1.3.1 // indirect
|
github.com/gogo/protobuf v1.3.1 // indirect
|
||||||
github.com/golang-migrate/migrate v3.5.4+incompatible
|
github.com/golang-migrate/migrate v3.5.4+incompatible
|
||||||
github.com/golang/protobuf v1.4.3 // indirect
|
github.com/golang/protobuf v1.4.3 // indirect
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/joho/godotenv v1.3.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/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rs/cors v1.7.0
|
github.com/rs/cors v1.7.0
|
||||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
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
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||||
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect
|
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect
|
||||||
google.golang.org/grpc v1.33.1 // indirect
|
google.golang.org/grpc v1.33.1 // indirect
|
||||||
|
56
go.sum
56
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 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
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/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/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/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 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
|
||||||
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
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/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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
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 h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
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=
|
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.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/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/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 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
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-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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
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 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
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.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.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.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/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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
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 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
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/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/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/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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
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=
|
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.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 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
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/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.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-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 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
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-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-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-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-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-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 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-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/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-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-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-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-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-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-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-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-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-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-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 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
|
||||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
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-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-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-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-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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
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=
|
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.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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
@ -29,7 +29,7 @@ func InitDb() {
|
|||||||
dbTz = "&loc=" + strings.Replace(timeZone, "/", fmt.Sprintf("%%2F"), 1)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package goscrobble
|
package goscrobble
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
@ -13,34 +12,51 @@ var JwtToken []byte
|
|||||||
// JwtExpiry - Expiry in seconds
|
// JwtExpiry - Expiry in seconds
|
||||||
var JwtExpiry time.Duration
|
var JwtExpiry time.Duration
|
||||||
|
|
||||||
// Store custom claims here
|
type CustomClaims struct {
|
||||||
type Claims struct {
|
Username string `json:"username"`
|
||||||
UUID string `json:"uuid"`
|
Email string `json:"email"`
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyToken - Verifies the JWT is valid
|
func generateJWTToken(user User) (string, error) {
|
||||||
func verifyToken(token string, w http.ResponseWriter) bool {
|
atClaims := jwt.MapClaims{}
|
||||||
// Initialize a new instance of `Claims`
|
atClaims["sub"] = user.UUID
|
||||||
claims := &Claims{}
|
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
|
return JwtToken, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Verify Signature
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == jwt.ErrSignatureInvalid {
|
return claims, err
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !tkn.Valid {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
75
internal/goscrobble/redis.go
Normal file
75
internal/goscrobble/redis.go
Normal file
@ -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
|
||||||
|
}
|
@ -16,6 +16,25 @@ type Scrobble struct {
|
|||||||
Track string `json:"track"`
|
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
|
// 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 {
|
func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
||||||
err := insertNewScrobble(user, track, ip, tx)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchScrobble(col string, val string, tx *sql.Tx) Scrobble {
|
func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
||||||
var scrobble Scrobble
|
scrobbleReq := ScrobbleRequest{}
|
||||||
err := tx.QueryRow(
|
var count int
|
||||||
"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)
|
// 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 != nil {
|
||||||
if err != sql.ErrNoRows {
|
log.Printf("Failed to fetch scrobble count: %+v", err)
|
||||||
log.Printf("Error fetching scrobbles: %+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 {
|
func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
||||||
|
@ -46,16 +46,14 @@ func HandleRequests(port string) {
|
|||||||
v1 := r.PathPrefix("/api/v1").Subrouter()
|
v1 := r.PathPrefix("/api/v1").Subrouter()
|
||||||
|
|
||||||
// Static Token for /ingress
|
// Static Token for /ingress
|
||||||
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress))
|
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
||||||
|
|
||||||
// JWT Auth
|
// JWT Auth
|
||||||
// v1.HandleFunc("/profile/{id}", jwtMiddleware(handleIngress))
|
v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
|
||||||
|
|
||||||
// No Auth
|
// No Auth
|
||||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).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
|
// This just prevents it serving frontend stuff over /api
|
||||||
r.PathPrefix("/api")
|
r.PathPrefix("/api")
|
||||||
@ -65,9 +63,10 @@ func HandleRequests(port string) {
|
|||||||
r.PathPrefix("/").Handler(spa)
|
r.PathPrefix("/").Handler(spa)
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
// Grrrr CORS
|
// Grrrr CORS. To clean up at a later date
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
|
AllowedHeaders: []string{"*"},
|
||||||
})
|
})
|
||||||
handler := c.Handler(r)
|
handler := c.Handler(r)
|
||||||
|
|
||||||
@ -97,14 +96,24 @@ func throwBadReq(w http.ResponseWriter, m string) {
|
|||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
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
|
// throwOkMessage - Throws a happy 200
|
||||||
func throwOkMessage(w http.ResponseWriter, m string) {
|
func throwOkMessage(w http.ResponseWriter, m string) {
|
||||||
jr := jsonResponse{
|
jr := jsonResponse{
|
||||||
Msg: m,
|
Msg: m,
|
||||||
}
|
}
|
||||||
js, _ := json.Marshal(&jr)
|
js, _ := json.Marshal(&jr)
|
||||||
err := errors.New(string(js))
|
w.WriteHeader(http.StatusOK)
|
||||||
http.Error(w, err.Error(), http.StatusOK)
|
w.Write(js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateJsonMessage - Generates a message:str response
|
// generateJsonMessage - Generates a message:str response
|
||||||
@ -126,7 +135,7 @@ func generateJsonError(m string) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tokenMiddleware - Validates token to a user
|
// 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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
fullToken := r.Header.Get("Authorization")
|
fullToken := r.Header.Get("Authorization")
|
||||||
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
|
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
|
||||||
@ -140,17 +149,29 @@ func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lets tack this on the request for now..
|
next(w, r, userUuid)
|
||||||
r.Header.Set("UserUUID", userUuid)
|
|
||||||
next(w, r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// jwtMiddleware - Validates middleware to a user
|
// 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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
throwUnauthorized(w, "Invalid JWT Token")
|
fullToken := r.Header.Get("Authorization")
|
||||||
next(w, r)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := generateJsonMessage("User created succesfully. You may now login")
|
throwOkMessage(w, "User created succesfully. You may now login")
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLogin - Does as it says!
|
// handleLogin - Does as it says!
|
||||||
@ -206,7 +225,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
ip := getUserIp(r)
|
ip := getUserIp(r)
|
||||||
data, err := loginUser(&logReq, ip)
|
data, err := loginUser(&logReq, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
throwOkMessage(w, err.Error())
|
throwOkError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,26 +234,29 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serveEndpoint - API stuffs
|
// 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)
|
bodyJson, err := decodeJson(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If we can't decode. Lets tell them nicely.
|
// If we can't decode. Lets tell them nicely.
|
||||||
http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest)
|
http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ingressType := strings.Replace(r.URL.Path, "/api/v1/ingress/", "", 1)
|
ingressType := strings.Replace(r.URL.Path, "/api/v1/ingress/", "", 1)
|
||||||
|
|
||||||
switch ingressType {
|
switch ingressType {
|
||||||
case "jellyfin":
|
case "jellyfin":
|
||||||
tx, _ := db.Begin()
|
tx, _ := db.Begin()
|
||||||
|
|
||||||
ip := getUserIp(r)
|
ip := getUserIp(r)
|
||||||
err := ParseJellyfinInput(r.Header.Get("UserUUID"), bodyJson, ip, tx)
|
err := ParseJellyfinInput(userUuid, bodyJson, ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting track: %+v", err)
|
log.Printf("Error inserting track: %+v", err)
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
throwBadReq(w, err.Error())
|
throwBadReq(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
throwBadReq(w, err.Error())
|
throwBadReq(w, err.Error())
|
||||||
@ -248,6 +270,20 @@ func handleIngress(w http.ResponseWriter, r *http.Request) {
|
|||||||
throwBadReq(w, "Unknown ingress type")
|
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
|
// FRONTEND HANDLING
|
||||||
|
|
||||||
// ServerHTTP - Frontend server
|
// ServerHTTP - Frontend server
|
||||||
|
@ -17,9 +17,16 @@ func generateToken(n int) string {
|
|||||||
|
|
||||||
func getUserForToken(token string) (string, error) {
|
func getUserForToken(token string) (string, error) {
|
||||||
var uuid string
|
var uuid string
|
||||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true) FROM `users` WHERE `token` = ? AND `active` = 1", token).Scan(&uuid)
|
cachedKey := getRedisVal("user_token:" + token)
|
||||||
if err != nil {
|
if cachedKey == "" {
|
||||||
return "", errors.New("Invalid Token")
|
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
|
return uuid, nil
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,14 +103,16 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(logReq.Username, "@") {
|
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 != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return resp, errors.New("Invalid Username or Password")
|
return resp, errors.New("Invalid Username or Password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err == sql.ErrNoRows {
|
||||||
return resp, errors.New("Invalid Username or Password")
|
return resp, errors.New("Invalid Username or Password")
|
||||||
}
|
}
|
||||||
@ -122,7 +123,7 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Issue JWT + Response
|
// Issue JWT + Response
|
||||||
token, err := generateJwt(user)
|
token, err := generateJWTToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error generating JWT: %v", err)
|
log.Printf("Error generating JWT: %v", err)
|
||||||
return resp, errors.New("Error logging in")
|
return resp, errors.New("Error logging in")
|
||||||
@ -136,21 +137,6 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) {
|
|||||||
return resp, nil
|
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!
|
// insertUser - Does the dirtywork!
|
||||||
func insertUser(username string, email string, password []byte, ip net.IP) error {
|
func insertUser(username string, email string, password []byte, ip net.IP) error {
|
||||||
token := generateToken(32)
|
token := generateToken(32)
|
||||||
|
@ -3,6 +3,7 @@ package goscrobble
|
|||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
@ -86,3 +87,7 @@ func Inet6_Aton(ip net.IP) string {
|
|||||||
ipHex := hex.EncodeToString(ipInt.Bytes())
|
ipHex := hex.EncodeToString(ipInt.Bytes())
|
||||||
return ipHex
|
return ipHex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calcPageOffsetString(page int, offset int) string {
|
||||||
|
return fmt.Sprintf("%d", page*offset)
|
||||||
|
}
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
REACT_APP_API_URL=http://127.0.0.1:42069
|
|
||||||
REACT_APP_REGISTRATION_DISABLED=false
|
|
@ -1,2 +0,0 @@
|
|||||||
REACT_APP_API_URL=https://goscrobble.com
|
|
||||||
REACT_APP_REGISTRATION_DISABLED=true
|
|
81
web/package-lock.json
generated
81
web/package-lock.json
generated
@ -12,8 +12,10 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
"@testing-library/react": "^11.2.5",
|
"@testing-library/react": "^11.2.5",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.6",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
"react-cookie": "^4.0.3",
|
"react-cookie": "^4.0.3",
|
||||||
@ -24,11 +26,15 @@
|
|||||||
"react-spinners": "^0.10.6",
|
"react-spinners": "^0.10.6",
|
||||||
"react-toast": "^1.0.1",
|
"react-toast": "^1.0.1",
|
||||||
"react-toast-notifications": "^2.4.3",
|
"react-toast-notifications": "^2.4.3",
|
||||||
|
"react-toastify": "^7.0.3",
|
||||||
"reactstrap": "^8.9.0",
|
"reactstrap": "^8.9.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"web-vitals": "^1.1.1"
|
"web-vitals": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
"webpack-dev-server": "^3.11.1"
|
"webpack-dev-server": "^3.11.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -4175,6 +4181,14 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
@ -5560,6 +5574,14 @@
|
|||||||
"wrap-ansi": "^6.2.0"
|
"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": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@ -12841,6 +12863,11 @@
|
|||||||
"node": ">=4.0"
|
"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": {
|
"node_modules/killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
@ -16713,6 +16740,18 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"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": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||||
@ -16937,6 +16976,15 @@
|
|||||||
"symbol-observable": "^1.2.0"
|
"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": {
|
"node_modules/redux-persist": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz",
|
||||||
"integrity": "sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ=="
|
"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": {
|
"axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
@ -26548,6 +26604,11 @@
|
|||||||
"wrap-ansi": "^6.2.0"
|
"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": {
|
"co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@ -32139,6 +32200,11 @@
|
|||||||
"object.assign": "^4.1.2"
|
"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": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
@ -35246,6 +35312,14 @@
|
|||||||
"react-transition-group": "^4.4.1"
|
"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": {
|
"react-transition-group": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||||
@ -35422,6 +35496,13 @@
|
|||||||
"symbol-observable": "^1.2.0"
|
"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": {
|
"redux-persist": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
||||||
|
@ -7,8 +7,10 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
"@testing-library/react": "^11.2.5",
|
"@testing-library/react": "^11.2.5",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.6",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
"react-cookie": "^4.0.3",
|
"react-cookie": "^4.0.3",
|
||||||
@ -19,8 +21,11 @@
|
|||||||
"react-spinners": "^0.10.6",
|
"react-spinners": "^0.10.6",
|
||||||
"react-toast": "^1.0.1",
|
"react-toast": "^1.0.1",
|
||||||
"react-toast-notifications": "^2.4.3",
|
"react-toast-notifications": "^2.4.3",
|
||||||
|
"react-toastify": "^7.0.3",
|
||||||
"reactstrap": "^8.9.0",
|
"reactstrap": "^8.9.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"web-vitals": "^1.1.1"
|
"web-vitals": "^1.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -48,6 +53,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
"webpack-dev-server": "^3.11.1"
|
"webpack-dev-server": "^3.11.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
87
web/src/Actions/auth.js
Normal file
87
web/src/Actions/auth.js
Normal file
@ -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();
|
||||||
|
};
|
10
web/src/Actions/message.js
Normal file
10
web/src/Actions/message.js
Normal file
@ -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,
|
||||||
|
});
|
8
web/src/Actions/types.js
Normal file
8
web/src/Actions/types.js
Normal file
@ -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";
|
102
web/src/App.js
102
web/src/App.js
@ -1,44 +1,82 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import Home from './Components/Pages/Home';
|
import Home from './Pages/Home';
|
||||||
import About from './Components/Pages/About';
|
import About from './Pages/About';
|
||||||
import Help from './Components/Pages/Help';
|
|
||||||
import Login from './Components/Pages/Login';
|
import Dashboard from './Pages/Dashboard';
|
||||||
import Settings from './Components/Pages/Settings';
|
import Admin from './Pages/Admin';
|
||||||
import Register from './Components/Pages/Register';
|
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 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 { Route, Switch, withRouter } from 'react-router-dom';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
import { Component } from 'react';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
const { user } = state.auth;
|
||||||
return {
|
return {
|
||||||
isLoggedIn: state
|
user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
class App extends Component {
|
||||||
return {
|
constructor(props) {
|
||||||
logIn: () => dispatch({type: true}),
|
super(props);
|
||||||
logOut: () => dispatch({type: false})
|
this.logOut = this.logOut.bind(this);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = () => {
|
this.state = {
|
||||||
let exact = true
|
// showAdminBoard: false,
|
||||||
return (
|
currentUser: undefined,
|
||||||
<div>
|
// Don't even ask.. apparently you can't pass
|
||||||
<Navigation />
|
// exact="true".. it has to be a bool :|
|
||||||
<Switch>
|
true: true,
|
||||||
<Route exact={exact} path="/" component={Home} />
|
};
|
||||||
<Route path="/about" component={About} />
|
|
||||||
<Route path="/settings" component={Settings} />
|
|
||||||
<Route path="/help" component={Help} />
|
|
||||||
<Route path="/login" component={Login} />
|
|
||||||
<Route path="/register" component={Register} />
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
||||||
|
<div>
|
||||||
|
<Navigation />
|
||||||
|
<Switch>
|
||||||
|
<Route exact={this.state.true} path="/" component={Home} />
|
||||||
|
<Route path="/about" component={About} />
|
||||||
|
|
||||||
|
<Route path="/dashboard" component={Dashboard} />
|
||||||
|
<Route path="/profile" component={Profile} />
|
||||||
|
<Route path="/admin" component={Admin} />
|
||||||
|
|
||||||
|
<Route path="/settings" component={Settings} />
|
||||||
|
<Route path="/login" component={Login} />
|
||||||
|
<Route path="/register" component={Register} />
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default withRouter(connect(mapStateToProps)(App));
|
@ -1 +0,0 @@
|
|||||||
// https://stackoverflow.com/questions/38397653/redux-what-is-the-correct-place-to-save-cookie-after-login-request
|
|
@ -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 (
|
|
||||||
<Provider store={this.props.store}>
|
|
||||||
{this.props.children}
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppProvider.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
children: PropTypes.node
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppProvider;
|
|
@ -3,70 +3,106 @@ import { Navbar, NavbarBrand } from 'reactstrap';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import logo from '../logo.png';
|
import logo from '../logo.png';
|
||||||
import './Navigation.css';
|
import './Navigation.css';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { logout } from '../Actions/auth';
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
'Home',
|
'Home',
|
||||||
'Help',
|
|
||||||
'About',
|
'About',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const loggedInMenuItems = [
|
||||||
|
'Dashboard',
|
||||||
|
'About',
|
||||||
|
]
|
||||||
class Navigation extends Component {
|
class Navigation extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
// Yeah I know you might not hit home.. but I can't get the
|
// Yeah I know you might not hit home.. but I can't get the
|
||||||
// path based finder thing working on initial load :sweatsmile:
|
// path based finder thing working on initial load :sweatsmile:
|
||||||
console.log(this.props.initLocation)
|
this.state = { active: "Home" };
|
||||||
this.state = { isLoggedIn: false, active: "Home" };
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toggleLogin() {
|
componentDidMount() {
|
||||||
this.setState({ isLoggedIn: !this.state.isLoggedIn })
|
const isLoggedIn = this.props.isLoggedIn;
|
||||||
}
|
|
||||||
|
|
||||||
_handleClick(menuItem) {
|
if (isLoggedIn) {
|
||||||
this.setState({ active: menuItem });
|
this.setState({
|
||||||
}
|
isLoggedIn: true,
|
||||||
|
});
|
||||||
render() {
|
|
||||||
const activeStyle = { color: '#FFFFFF' };
|
|
||||||
|
|
||||||
const renderAuthButtons = () => {
|
|
||||||
if (this.state.isLoggedIn) {
|
|
||||||
return <div className="navLinkLogin">
|
|
||||||
<Link to="/profile" className="navLink">Profile</Link>
|
|
||||||
<Link to="/" className="navLink" onClick={this.toggleLogin.bind(this)}>Logout</Link>
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
return <div className="navLinkLogin">
|
|
||||||
<Link to="/login" className="navLink">Login</Link>
|
|
||||||
<Link to="/register" className="navLink" history={this.props.history}>Register</Link>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Navbar color="dark" dark fixed="top">
|
|
||||||
<NavbarBrand href="/" className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
|
||||||
{menuItems.map(menuItem =>
|
|
||||||
<Link
|
|
||||||
key={menuItem}
|
|
||||||
className="navLink"
|
|
||||||
style={this.state.active === menuItem ? activeStyle : {}}
|
|
||||||
onClick={this._handleClick.bind(this, menuItem)}
|
|
||||||
to={menuItem === "Home" ? "/" : menuItem}
|
|
||||||
>
|
|
||||||
{menuItem}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{renderAuthButtons()}
|
|
||||||
</Navbar>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Navigation;
|
_handleClick(menuItem) {
|
||||||
|
this.setState({ active: menuItem });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const activeStyle = { color: '#FFFFFF' };
|
||||||
|
|
||||||
|
const renderAuthButtons = () => {
|
||||||
|
if (this.state.isLoggedIn) {
|
||||||
|
return <div className="navLinkLogin">
|
||||||
|
<Link to="/profile" className="navLink">Profile</Link>
|
||||||
|
<Link to="/" className="navLink" onClick={logout()}>Logout</Link>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
return <div className="navLinkLogin">
|
||||||
|
<Link to="/login" className="navLink">Login</Link>
|
||||||
|
<Link to="/register" className="navLink" history={this.props.history}>Register</Link>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMenuButtons = () => {
|
||||||
|
if (this.state.isLoggedIn) {
|
||||||
|
return <div>
|
||||||
|
{loggedInMenuItems.map(menuItem =>
|
||||||
|
<Link
|
||||||
|
key={menuItem}
|
||||||
|
className="navLink"
|
||||||
|
style={this.state.active === menuItem ? activeStyle : {}}
|
||||||
|
onClick={this._handleClick.bind(this, menuItem)}
|
||||||
|
to={menuItem}
|
||||||
|
>
|
||||||
|
{menuItem}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
return <div>
|
||||||
|
{menuItems.map(menuItem =>
|
||||||
|
<Link
|
||||||
|
key={menuItem}
|
||||||
|
className="navLink"
|
||||||
|
style={this.state.active === menuItem ? activeStyle : {}}
|
||||||
|
onClick={this._handleClick.bind(this, menuItem)}
|
||||||
|
to={menuItem === "Home" ? "/" : menuItem}
|
||||||
|
>
|
||||||
|
{menuItem}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar color="dark" dark fixed="top">
|
||||||
|
<NavbarBrand href="/" className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
||||||
|
{renderMenuButtons()}
|
||||||
|
{renderAuthButtons()}
|
||||||
|
</Navbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const { isLoggedIn } = state.auth;
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Navigation);
|
@ -1,4 +0,0 @@
|
|||||||
.helpBody {
|
|
||||||
padding: 20px 5px 5px 5px;
|
|
||||||
font-size: 16pt;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import '../../App.css';
|
|
||||||
import './Help.css';
|
|
||||||
|
|
||||||
function Help() {
|
|
||||||
return (
|
|
||||||
<div className="pageWrapper">
|
|
||||||
<h1>
|
|
||||||
Help Docs
|
|
||||||
</h1>
|
|
||||||
<p className="helpBody">
|
|
||||||
Jellyfin Configuration<br/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Help;
|
|
@ -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 <Component {...props} {...toastFuncs} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="pageWrapper">
|
|
||||||
<h1>
|
|
||||||
Login
|
|
||||||
</h1>
|
|
||||||
<div className="loginBody">
|
|
||||||
<Formik
|
|
||||||
initialValues={{ username: '', password: '' }}
|
|
||||||
onSubmit={async values => this.handleSubmit(values)}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<label>
|
|
||||||
Email / Username<br/>
|
|
||||||
<Field
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
required={trueBool}
|
|
||||||
className="loginFields"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<br/>
|
|
||||||
<label>
|
|
||||||
Password<br/>
|
|
||||||
<Field
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required={trueBool}
|
|
||||||
className="loginFields"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<br/><br/>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
className="loginButton"
|
|
||||||
disabled={this.state.loading}
|
|
||||||
>{this.state.loading ? <ScaleLoader color="#FFF" size={35} /> : "Login"}</Button>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withToast(Login);
|
|
3
web/src/Helpers/history.js
Normal file
3
web/src/Helpers/history.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createBrowserHistory } from "history";
|
||||||
|
|
||||||
|
export const history = createBrowserHistory();
|
@ -1,4 +1,4 @@
|
|||||||
import '../../App.css';
|
import '../App.css';
|
||||||
import './About.css';
|
import './About.css';
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
@ -8,7 +8,7 @@ function About() {
|
|||||||
About GoScrobble.com
|
About GoScrobble.com
|
||||||
</h1>
|
</h1>
|
||||||
<p className="aboutBody">
|
<p className="aboutBody">
|
||||||
Go-Scrobble is an open source music scorbbling service written in Go and React.<br/>
|
Go-Scrobble is an open source music scorbbling service written in Go and React.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
45
web/src/Pages/Admin.js
Normal file
45
web/src/Pages/Admin.js
Normal file
@ -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 (
|
||||||
|
<div className="container">
|
||||||
|
<header className="jumbotron">
|
||||||
|
<h3>{this.state.content}</h3>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Admin;
|
4
web/src/Pages/Dashboard.css
Normal file
4
web/src/Pages/Dashboard.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.dashboardBody {
|
||||||
|
padding: 20px 5px 5px 5px;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
35
web/src/Pages/Dashboard.js
Normal file
35
web/src/Pages/Dashboard.js
Normal file
@ -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 (
|
||||||
|
<div className="pageWrapper">
|
||||||
|
<h1>
|
||||||
|
Hai Dashboard!
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const { isLoggedIn } = state.auth;
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Dashboard);
|
@ -1,5 +1,5 @@
|
|||||||
import logo from '../../logo.png';
|
import logo from '../logo.png';
|
||||||
import '../../App.css';
|
import '../App.css';
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
92
web/src/Pages/Login.js
Normal file
92
web/src/Pages/Login.js
Normal file
@ -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 (
|
||||||
|
<div className="pageWrapper">
|
||||||
|
<h1>
|
||||||
|
Login
|
||||||
|
</h1>
|
||||||
|
<div className="loginBody">
|
||||||
|
<Formik
|
||||||
|
initialValues={{ username: '', password: '' }}
|
||||||
|
onSubmit={async values => this.handleLogin(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<label>
|
||||||
|
Email / Username<br/>
|
||||||
|
<Field
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
required={trueBool}
|
||||||
|
className="loginFields"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
<label>
|
||||||
|
Password<br/>
|
||||||
|
<Field
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required={trueBool}
|
||||||
|
className="loginFields"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
className="loginButton"
|
||||||
|
disabled={this.state.loading}
|
||||||
|
>{this.state.loading ? <ScaleLoader color="#FFF" size={35} /> : "Login"}</Button>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const { isLoggedIn } = state.auth;
|
||||||
|
const { message } = state.message;
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Login);
|
4
web/src/Pages/Profile.css
Normal file
4
web/src/Pages/Profile.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.profileBody {
|
||||||
|
padding: 20px 5px 5px 5px;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
35
web/src/Pages/Profile.js
Normal file
35
web/src/Pages/Profile.js
Normal file
@ -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 (
|
||||||
|
<div className="pageWrapper">
|
||||||
|
<h1>
|
||||||
|
Hai User
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const { isLoggedIn } = state.auth;
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Profile);
|
@ -1,18 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../../App.css';
|
import '../App.css';
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import ScaleLoader from "react-spinners/ScaleLoader";
|
import ScaleLoader from "react-spinners/ScaleLoader";
|
||||||
import { withRouter } from 'react-router-dom'
|
import { withRouter } from 'react-router-dom'
|
||||||
|
|
||||||
function withToast(Component) {
|
|
||||||
return function WrappedComponent(props) {
|
|
||||||
const toastFuncs = useToasts()
|
|
||||||
return <Component {...props} {...toastFuncs} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Register extends React.Component {
|
class Register extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -165,4 +157,4 @@ class Register extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(withToast(Register));
|
export default withRouter(Register);
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../../App.css';
|
import '../App.css';
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
|
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
50
web/src/Reducers/auth.js
Normal file
50
web/src/Reducers/auth.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
8
web/src/Reducers/index.js
Normal file
8
web/src/Reducers/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { combineReducers } from "redux";
|
||||||
|
import auth from "./auth";
|
||||||
|
import message from "./message";
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
auth,
|
||||||
|
message,
|
||||||
|
});
|
18
web/src/Reducers/message.js
Normal file
18
web/src/Reducers/message.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
0
web/src/Services/Actions.js
Normal file
0
web/src/Services/Actions.js
Normal file
9
web/src/Services/auth-header.js
Normal file
9
web/src/Services/auth-header.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function authHeader() {
|
||||||
|
const token = JSON.parse(localStorage.getItem('jwt'));
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
return { Authorization: 'Bearer ' + token };
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
35
web/src/Services/auth.service.js
Normal file
35
web/src/Services/auth.service.js
Normal file
@ -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();
|
18
web/src/Services/user.service.js
Normal file
18
web/src/Services/user.service.js
Normal file
@ -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();
|
@ -2,25 +2,29 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { ToastProvider } from 'react-toast-notifications';
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.min.css'
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { Provider } from 'react-redux'
|
import store from "./store";
|
||||||
import { createStore } from 'redux'
|
|
||||||
|
|
||||||
const goScorbbleStore = (state = false, logIn) => {
|
|
||||||
return state = logIn
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore(goScorbbleStore);
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<HashRouter>
|
<Provider store={store}>
|
||||||
<ToastProvider autoDismiss="true" autoDismissTimeout="6000" placement="bottom-right">
|
<HashRouter>
|
||||||
<Provider store={store}>
|
<ToastContainer
|
||||||
<App />
|
position="bottom-right"
|
||||||
</Provider>
|
autoClose={5000}
|
||||||
</ToastProvider>
|
hideProgressBar={false}
|
||||||
</HashRouter>,
|
newestOnTop={true}
|
||||||
|
closeOnClick
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss={false}
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
/>
|
||||||
|
<App />
|
||||||
|
</HashRouter>
|
||||||
|
</Provider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user