- Fixed looking up invalid profiles
- Added valid error handling to bad request && rate limiting
- Add Sendgrid library (Will add SMTP later)
- Complete password reset process
This commit is contained in:
Daniel Mason 2021-04-02 01:56:08 +13:00
parent e570314ac2
commit fd615102a8
Signed by: idanoo
GPG Key ID: 387387CDBC02F132
28 changed files with 871 additions and 261 deletions

View File

@ -16,3 +16,9 @@ JWT_EXPIRY=86400
REVERSE_PROXIES=127.0.0.1
PORT=42069
SENDGRID_API_KEY=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
GOSCROBBLE_DOMAIN=""

View File

@ -3,7 +3,7 @@ stages:
- bundle
variables:
VERSION: 0.0.9
VERSION: 0.0.10
build-go:
image: golang:1.16.2

View File

@ -51,6 +51,9 @@ func main() {
goscrobble.InitRedis()
defer goscrobble.CloseRedisConn()
// Clear old reset tokens regularly
// go goscrobble.ClearTokenTimer()
// Boot up API webserver \o/
goscrobble.HandleRequests(port)
}

View File

@ -1,3 +1,9 @@
# 0.0.10
- Fixed looking up invalid profiles
- Added valid error handling to bad request && rate limiting
- Add Sendgrid library (Will add SMTP later)
- Complete password reset process
# 0.0.9
- Fix mobile menu auto collapse on select
- Add /u/ route for public user profiles (Added private flag to db - to implement later)

View File

@ -24,3 +24,9 @@ These are stored in `web/.env.production` and `web/.env.development`
REVERSE_PROXIES=127.0.0.1 // Comma separated list of servers to ignore for IP logs
PORT=42069 // Server port
SENDGRID_API_KEY= // API KEY
MAIL_FROM_ADDRESS= // FROM email
MAIL_FROM_NAME= // FROM name
GOSCROBBLE_DOMAIN="" // Full domain for email links (https://goscrobble.com))

5
go.mod
View File

@ -10,18 +10,19 @@ require (
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/go-redis/redis/v8 v8.8.0 // indirect
github.com/go-redis/redis/v8 v8.8.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang-migrate/migrate v3.5.4+incompatible
github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/mux v1.8.0
github.com/joho/godotenv v1.3.0
github.com/mitchellh/mapstructure v1.4.1
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/cors v1.7.0
github.com/sendgrid/rest v2.6.3+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.8.0+incompatible
github.com/sirupsen/logrus v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba

25
go.sum
View File

@ -10,6 +10,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@ -28,6 +29,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
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=
@ -56,6 +58,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
@ -66,14 +69,15 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
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 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
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 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@ -82,22 +86,29 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/sendgrid/rest v2.6.3+incompatible h1:h/uruXAzKxVyDDIQX/MkQI73p/gsdpEnb5q2wxSvTsA=
github.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.8.0+incompatible h1:7yoUFMwT+jDI2ArBpC6zvtuQj1RUyYfCDl7zZea3XV4=
github.com/sendgrid/sendgrid-go v3.8.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
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 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q=
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=
@ -119,8 +130,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -139,10 +149,10 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -184,9 +194,12 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4
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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,4 +0,0 @@
package goscrobble
func getImageLastFM(src string) {
}

View File

@ -10,6 +10,9 @@ import (
// ParseJellyfinInput - Transform API data into a common struct
func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error {
// Debugging
fmt.Printf("%+v", data)
if data["ItemType"] != "Audio" {
return errors.New("Media type not audio")
}

View File

@ -0,0 +1,66 @@
package goscrobble
import (
"database/sql"
"fmt"
"net"
)
// ParseMultiScrobblerInput - Transform API data
func ParseMultiScrobblerInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error {
// Debugging
fmt.Printf("%+v", data)
// if data["ItemType"] != "Audio" {
// return errors.New("Media type not audio")
// }
// // Safety Checks
// if data["Artist"] == nil {
// return errors.New("Missing artist data")
// }
// if data["Album"] == nil {
// return errors.New("Missing album data")
// }
// if data["Name"] == nil {
// return errors.New("Missing track data")
// }
// // Insert artist if not exist
// artist, err := insertArtist(fmt.Sprintf("%s", data["Artist"]), fmt.Sprintf("%s", data["Provider_musicbrainzartist"]), tx)
// if err != nil {
// log.Printf("%+v", err)
// return errors.New("Failed to map artist")
// }
// // Insert album if not exist
// artists := []string{artist.Uuid}
// album, err := insertAlbum(fmt.Sprintf("%s", data["Album"]), fmt.Sprintf("%s", data["Provider_musicbrainzalbum"]), artists, tx)
// if err != nil {
// log.Printf("%+v", err)
// return errors.New("Failed to map album")
// }
// // Insert album if not exist
// track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), fmt.Sprintf("%s", data["Provider_musicbrainztrack"]), album.Uuid, artists, tx)
// if err != nil {
// log.Printf("%+v", err)
// return errors.New("Failed to map track")
// }
// // Insert album if not exist
// err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
// if err != nil {
// log.Printf("%+v", err)
// return errors.New("Failed to map track")
// }
// _ = album
// _ = artist
// _ = track
// Insert track if not exist
return nil
}

View File

@ -2,38 +2,21 @@ package goscrobble
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/rs/cors"
)
// spaHandler - Handles Single Page Applications (React)
type spaHandler struct {
staticPath string
indexPath string
}
type jsonResponse struct {
Err string `json:"error,omitempty"`
Msg string `json:"message,omitempty"`
Err string `json:"error,omitempty"`
Msg string `json:"message,omitempty"`
Valid bool `json:"valid,omitempty"`
}
// Limits to 1 req / 4 sec
var heavyLimiter = NewIPRateLimiter(0.25, 2)
// Limits to 5 req / sec
var standardLimiter = NewIPRateLimiter(5, 5)
// Limits to 10 req / sec
var lightLimiter = NewIPRateLimiter(10, 10)
// List of Reverse proxies
var ReverseProxies []string
@ -50,6 +33,7 @@ func HandleRequests(port string) {
// Static Token for /ingress
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
v1.HandleFunc("/ingress/multiscrobbler", tokenMiddleware(handleIngress)).Methods("POST")
// JWT Auth - PWN PROFILE ONLY.
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
@ -66,6 +50,8 @@ func HandleRequests(port string) {
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
v1.HandleFunc("/sendreset", limitMiddleware(handleSendReset, heavyLimiter)).Methods("POST")
v1.HandleFunc("/resetpassword", limitMiddleware(handleResetPassword, heavyLimiter)).Methods("POST")
// This just prevents it serving frontend stuff over /api
r.PathPrefix("/api")
@ -87,161 +73,7 @@ func HandleRequests(port string) {
log.Fatal(http.ListenAndServe(":"+port, handler))
}
// MIDDLEWARE RESPONSES
// throwUnauthorized - Throws a 403
func throwUnauthorized(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
err := errors.New(string(js))
http.Error(w, err.Error(), http.StatusUnauthorized)
}
// throwUnauthorized - Throws a 403
func throwBadReq(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
err := errors.New(string(js))
http.Error(w, err.Error(), http.StatusBadRequest)
}
// throwOkError - Throws a 403
func throwOkError(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusOK)
w.Write(js)
}
// throwOkMessage - Throws a happy 200
func throwOkMessage(w http.ResponseWriter, m string) {
jr := jsonResponse{
Msg: m,
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusOK)
w.Write(js)
}
// throwOkMessage - Throws a happy 200
func throwInvalidJson(w http.ResponseWriter) {
jr := jsonResponse{
Err: "Invalid JSON",
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusBadRequest)
w.Write(js)
}
// generateJsonMessage - Generates a message:str response
func generateJsonMessage(m string) []byte {
jr := jsonResponse{
Msg: m,
}
js, _ := json.Marshal(&jr)
return js
}
// generateJsonError - Generates a err:str response
func generateJsonError(m string) []byte {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
return js
}
// MIDDLEWARE ACTIONS
// tokenMiddleware - Validates token to a user
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
if authToken == "" {
throwUnauthorized(w, "A token is required")
return
}
userUuid, err := getUserUuidForToken(authToken)
if err != nil {
throwUnauthorized(w, err.Error())
return
}
next(w, r, userUuid)
}
}
// jwtMiddleware - Validates middleware to a user
func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
claims, err := verifyJWTToken(authToken)
if err != nil {
throwUnauthorized(w, "Invalid JWT Token")
return
}
var reqUuid string
for k, v := range mux.Vars(r) {
if k == "uuid" {
reqUuid = v
}
}
next(w, r, claims.Subject, reqUuid)
}
}
// adminMiddleware - Validates user is admin
func adminMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
claims, err := verifyJWTToken(authToken)
if err != nil {
throwUnauthorized(w, "Invalid JWT Token")
return
}
user, err := getUser(claims.Subject)
if err != nil {
throwUnauthorized(w, err.Error())
return
}
if !user.Admin {
throwUnauthorized(w, "User is not admin")
return
}
next(w, r, claims.Subject)
}
}
// limitMiddleware - Rate limits important stuff
func limitMiddleware(next http.HandlerFunc, limiter *IPRateLimiter) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
limiter := limiter.GetLimiter(r.RemoteAddr)
if !limiter.Allow() {
msg := generateJsonMessage("Too many requests")
w.WriteHeader(http.StatusTooManyRequests)
w.Write(msg)
return
}
next(w, r)
})
}
// API ENDPOINT HANDLING
// handleRegister - Does as it says!
func handleRegister(w http.ResponseWriter, r *http.Request) {
regReq := RegisterRequest{}
@ -296,6 +128,86 @@ func handleStats(w http.ResponseWriter, r *http.Request) {
w.Write(js)
}
// handleSendReset - Does as it says!
func handleSendReset(w http.ResponseWriter, r *http.Request) {
req := RegisterRequest{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&req)
if err != nil {
throwBadReq(w, err.Error())
return
}
if req.Email == "" {
throwOkError(w, "Invalid Email")
return
}
_ = getUserIp(r)
user, err := getUserByEmail(req.Email)
if err != nil {
throwOkError(w, err.Error())
return
}
ip := getUserIp(r)
err = user.sendResetEmail(ip)
if err != nil {
throwOkError(w, err.Error())
return
}
throwOkMessage(w, "Password reset email sent")
}
// handleSendReset - Does as it says!
func handleResetPassword(w http.ResponseWriter, r *http.Request) {
bodyJson, err := decodeJson(r.Body)
if err != nil {
throwInvalidJson(w)
return
}
if bodyJson["password"] == nil {
// validating
valid, err := checkResetToken(fmt.Sprintf("%s", bodyJson["token"]))
if err != nil {
throwOkError(w, err.Error())
return
}
jr := jsonResponse{
Valid: valid,
}
msg, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusOK)
w.Write(msg)
return
} else {
// resetting
token := fmt.Sprintf("%s", bodyJson["token"])
pw := fmt.Sprintf("%s", bodyJson["password"])
if len(pw) < 8 {
throwOkError(w, "Password must be at least 8 characters")
return
}
ip := getUserIp(r)
user, err := getUserByResetToken(token)
if err != nil {
throwOkError(w, err.Error())
return
}
err = user.updatePassword(pw, ip)
if err != nil {
throwOkError(w, err.Error())
return
}
throwOkMessage(w, "Password updated successfully!")
return
}
}
// serveEndpoint - API stuffs
func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
bodyJson, err := decodeJson(r.Body)
@ -304,32 +216,39 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
return
}
ip := getUserIp(r)
tx, _ := db.Begin()
ingressType := strings.Replace(r.URL.Path, "/api/v1/ingress/", "", 1)
switch ingressType {
case "jellyfin":
tx, _ := db.Begin()
ip := getUserIp(r)
err := ParseJellyfinInput(userUuid, bodyJson, ip, tx)
if err != nil {
// log.Printf("Error inserting track: %+v", err)
tx.Rollback()
throwOkError(w, err.Error())
return
}
err = tx.Commit()
case "multiscrobbler":
err := ParseMultiScrobblerInput(userUuid, bodyJson, ip, tx)
if err != nil {
tx.Rollback()
throwOkError(w, err.Error())
return
}
default:
tx.Rollback()
throwBadReq(w, "Unknown ingress type")
}
throwOkMessage(w, "success")
err = tx.Commit()
if err != nil {
throwOkError(w, err.Error())
return
}
throwBadReq(w, "Unknown ingress type")
throwOkMessage(w, "success")
return
}
// fetchUser - Return personal userprofile
@ -432,35 +351,3 @@ func fetchProfile(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// FRONTEND HANDLING
// ServerHTTP - Frontend server
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the absolute path to prevent directory traversal
path, err := filepath.Abs(r.URL.Path)
if err != nil {
// If we failed to get the absolute path respond with a 400 bad request and return
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// prepend the path with the path to the static directory
path = filepath.Join(h.staticPath, path)
// check whether a file exists at the given path
_, err = os.Stat(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

View File

@ -0,0 +1,104 @@
package goscrobble
import (
"encoding/json"
"net/http"
"strings"
"github.com/gorilla/mux"
)
// Limits to 1 req / 4 sec
var heavyLimiter = NewIPRateLimiter(0.25, 2)
// Limits to 5 req / sec
var standardLimiter = NewIPRateLimiter(5, 5)
// Limits to 10 req / sec
var lightLimiter = NewIPRateLimiter(10, 10)
// tokenMiddleware - Validates token to a user
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
if authToken == "" {
throwUnauthorized(w, "A token is required")
return
}
userUuid, err := getUserUuidForToken(authToken)
if err != nil {
throwUnauthorized(w, err.Error())
return
}
next(w, r, userUuid)
}
}
// jwtMiddleware - Validates middleware to a user
func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
claims, err := verifyJWTToken(authToken)
if err != nil {
throwUnauthorized(w, "Invalid JWT Token")
return
}
var reqUuid string
for k, v := range mux.Vars(r) {
if k == "uuid" {
reqUuid = v
}
}
next(w, r, claims.Subject, reqUuid)
}
}
// adminMiddleware - Validates user is admin
func adminMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fullToken := r.Header.Get("Authorization")
authToken := strings.Replace(fullToken, "Bearer ", "", 1)
claims, err := verifyJWTToken(authToken)
if err != nil {
throwUnauthorized(w, "Invalid JWT Token")
return
}
user, err := getUser(claims.Subject)
if err != nil {
throwUnauthorized(w, err.Error())
return
}
if !user.Admin {
throwUnauthorized(w, "User is not admin")
return
}
next(w, r, claims.Subject)
}
}
// limitMiddleware - Rate limits important stuff
func limitMiddleware(next http.HandlerFunc, limiter *IPRateLimiter) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
limiter := limiter.GetLimiter(r.RemoteAddr)
if !limiter.Allow() {
jr := jsonResponse{
Msg: "Too many requests",
}
msg, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusTooManyRequests)
w.Write(msg)
return
}
next(w, r)
})
}

View File

@ -0,0 +1,58 @@
package goscrobble
import (
"encoding/json"
"errors"
"net/http"
)
// MIDDLEWARE RESPONSES
// throwUnauthorized - Throws a 403
func throwUnauthorized(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
err := errors.New(string(js))
http.Error(w, err.Error(), http.StatusUnauthorized)
}
// throwUnauthorized - Throws a 403
func throwBadReq(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
err := errors.New(string(js))
http.Error(w, err.Error(), http.StatusBadRequest)
}
// throwOkError - Throws a 403
func throwOkError(w http.ResponseWriter, m string) {
jr := jsonResponse{
Err: m,
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusOK)
w.Write(js)
}
// throwOkMessage - Throws a happy 200
func throwOkMessage(w http.ResponseWriter, m string) {
jr := jsonResponse{
Msg: m,
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusOK)
w.Write(js)
}
// throwOkMessage - Throws a happy 200
func throwInvalidJson(w http.ResponseWriter) {
jr := jsonResponse{
Err: "Invalid JSON",
}
js, _ := json.Marshal(&jr)
w.WriteHeader(http.StatusBadRequest)
w.Write(js)
}

View File

@ -0,0 +1,43 @@
package goscrobble
import (
"net/http"
"os"
"path/filepath"
)
// spaHandler - Handles Single Page Applications (React)
type spaHandler struct {
staticPath string
indexPath string
}
// ServerHTTP - Frontend React server
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the absolute path to prevent directory traversal
path, err := filepath.Abs(r.URL.Path)
if err != nil {
// If we failed to get the absolute path respond with a 400 bad request and return
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// prepend the path with the path to the static directory
path = filepath.Join(h.staticPath, path)
// check whether a file exists at the given path
_, err = os.Stat(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

View File

@ -0,0 +1,18 @@
package goscrobble
import (
"os"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
func sendEmail(destName string, destEmail string, subject string, content string) error {
from := mail.NewEmail(os.Getenv("MAIL_FROM_NAME"), os.Getenv("MAIL_FROM_ADDRESS"))
to := mail.NewEmail(destName, destEmail)
message := mail.NewSingleEmail(from, subject, to, content, "")
client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))
_, err := client.Send(message)
return err
}

View File

@ -0,0 +1,14 @@
package goscrobble
import (
"fmt"
"time"
)
func ClearTokenTimer() {
go func() {
for now := range time.Tick(time.Second) {
fmt.Println(now)
}
}()
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"log"
"net"
"os"
"strings"
"time"
@ -153,13 +154,13 @@ func insertUser(username string, email string, password []byte, ip net.IP) error
}
func updateUser(uuid string, field string, value string, ip net.IP) error {
_, err := db.Exec("UPDATE users SET ? = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = ?", field, value, uuid, ip)
_, err := db.Exec("UPDATE users SET `"+field+"` = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = ?", value, uuid, ip)
return err
}
func updateUserDirect(uuid string, field string, value string) error {
_, err := db.Exec("UPDATE users SET ? = ? WHERE uuid = ?", field, value, uuid)
_, err := db.Exec("UPDATE users SET `"+field+"` = ? WHERE uuid = ?", value, uuid)
return err
}
@ -228,3 +229,90 @@ func getUserByUsername(username string) (User, error) {
return user, nil
}
func getUserByEmail(email string) (User, error) {
var user User
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` WHERE `email` = ? AND `active` = 1",
email).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
if err == sql.ErrNoRows {
return user, errors.New("Invalid Email")
}
return user, nil
}
func getUserByResetToken(token string) (User, error) {
var user User
err := db.QueryRow("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` "+
"JOIN `resettoken` ON `resettoken`.`user` = `users`.`uuid` WHERE `resettoken`.`token` = ? AND `active` = 1",
token).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
fmt.Println(err)
if err == sql.ErrNoRows {
return user, errors.New("Invalid Token")
}
return user, nil
}
func (user *User) sendResetEmail(ip net.IP) error {
token := generateToken(16)
// 24 hours
exp := time.Now().AddDate(0, 0, 1)
err := user.saveResetToken(token, exp)
if err != nil {
return err
}
content := fmt.Sprintf(
"Someone at %s has request a password reset for %s. Click the following link to reset your password: %s/reset/%s",
ip, user.Username, os.Getenv("GOSCROBBLE_DOMAIN"), token)
return sendEmail(user.Username, user.Email, "GoScrobble - Password Reset", content)
}
func (user *User) saveResetToken(token string, expiry time.Time) error {
_, _ = db.Exec("DELETE FROM `resettoken` WHERE `user` = UUID_TO_BIN(?, true)", user.UUID)
_, err := db.Exec("INSERT INTO `resettoken` (`user`, `token`, `expiry`) "+
"VALUES (UUID_TO_BIN(?, true),?, ?)", user.UUID, token, expiry)
return err
}
func clearOldResetTokens() {
_, _ = db.Exec("DELETE FROM `resettoken` WHERE `expiry` < NOW()")
}
func clearResetToken(token string) error {
_, err := db.Exec("DELETE FROM `resettoken` WHERE `token` = ?", token)
return err
}
// checkResetToken - If a token exists check it
func checkResetToken(token string) (bool, error) {
count, err := getDbCount("SELECT COUNT(*) FROM `resettoken` WHERE `token` = ? ", token)
if err != nil {
return false, err
}
return count > 0, nil
}
func (user *User) updatePassword(newPassword string, ip net.IP) error {
hash, err := hashPassword(newPassword)
if err != nil {
return errors.New("Bad password")
}
_, err = db.Exec("UPDATE `users` SET `password` = ? WHERE `uuid` = UUID_TO_BIN(?, true)", hash, user.UUID)
if err != nil {
return errors.New("Failed to update password")
}
return nil
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `resettoken`;

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `resettoken` (
`user` BINARY(16) PRIMARY KEY,
`token` VARCHAR(64) NOT NULL,
`expiry` DATETIME NOT NULL,
KEY `tokenLookup` (`token`)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

View File

@ -32,9 +32,14 @@ export const PostLogin = (formValues) => {
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
return null
}
})
.catch(() => {
toast.error('Failed to connect');
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return Promise.resolve();
});
};
@ -51,20 +56,70 @@ export const PostRegister = (formValues) => {
return Promise.reject();
}
})
.catch(() => {
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
return Promise.resolve();
});
}
return Promise.resolve();
});
};
export const PostResetPassword = (formValues) => {
return axios.post(process.env.REACT_APP_API_URL + "resetpassword", formValues)
.then((response) => {
if (response.data.message) {
toast.success(response.data.message);
return Promise.resolve();
} else {
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
return Promise.reject();
}
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return Promise.resolve();
});
};
export const sendPasswordReset = (values) => {
return axios.post(process.env.REACT_APP_API_URL + "sendreset", values).then(
(data) => {
return data.data;
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
export const getStats = () => {
return axios.get(process.env.REACT_APP_API_URL + "stats").then(
(data) => {
return data.data;
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
).catch(() => {
toast.error('Failed to connect');
return {};
});
};
@ -73,8 +128,14 @@ export const getRecentScrobbles = (id) => {
return axios.get(process.env.REACT_APP_API_URL + "user/" + id + "/scrobbles", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch(() => {
toast.error('Failed to connect');
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
@ -83,8 +144,14 @@ export const getConfigs = () => {
return axios.get(process.env.REACT_APP_API_URL + "config", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch(() => {
toast.error('Failed to connect');
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
@ -103,18 +170,30 @@ export const postConfigs = (values, toggle) => {
} else if (data.data && data.data.error) {
toast.error(data.data.error);
}
})
.catch(() => {
toast.error('Error updating values');
});
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
export const getProfile = (userName) => {
return axios.get(process.env.REACT_APP_API_URL + "profile/" + userName, { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch(() => {
toast.error('Failed to connect');
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
@ -123,8 +202,35 @@ export const getUser = () => {
return axios.get(process.env.REACT_APP_API_URL + "user", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch(() => {
toast.error('Failed to connect');
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};
export const validateResetPassword = (tokenStr) => {
return axios.post(process.env.REACT_APP_API_URL + "resetpassword", { token: tokenStr })
.then((data) => {
if (data.error) {
toast.error(data.error);
return {valid: false}
}
return data.data;
}).catch((error) => {
if (error.response === 401) {
return {};
} else if (error.response === 429) {
toast.error('Rate limited. Please try again shortly')
} else {
toast.error('Failed to connect');
}
return {};
});
};

View File

@ -7,6 +7,7 @@ import User from './Pages/User';
import Admin from './Pages/Admin';
import Login from './Pages/Login';
import Register from './Pages/Register';
import Reset from './Pages/Reset';
import Navigation from './Components/Navigation';
@ -38,6 +39,9 @@ const App = () => {
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/reset/:token" component={Reset} />
<Route path="/reset" component={Reset} />
</Switch>
</div>
);

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import AuthContext from './AuthContext';
import { PostLogin, PostRegister } from '../Api/index';
import { PostLogin, PostRegister, PostResetPassword } from '../Api/index';
const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState();
@ -30,11 +30,14 @@ const AuthContextProvider = ({ children }) => {
const Register = (formValues) => {
setLoading(true);
return PostRegister(formValues).then(response => {
// Do stuff here?
setLoading(false);
});
};
const ResetPassword = (formValues) => {
return PostResetPassword(formValues)
}
const Logout = () => {
localStorage.removeItem("user");
setUser(null)
@ -47,6 +50,7 @@ const AuthContextProvider = ({ children }) => {
Logout,
Login,
Register,
ResetPassword,
loading,
user,
}}

View File

@ -1,4 +1,5 @@
import React, { useContext, useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import '../App.css';
import './Login.css';
import { Button } from 'reactstrap';
@ -9,6 +10,7 @@ import { Switch } from 'formik-material-ui';
import { getConfigs, postConfigs } from '../Api/index'
const Admin = () => {
const history = useHistory();
const { user } = useContext(AuthContext);
const [loading, setLoading] = useState(true);
const [configs, setConfigs] = useState({})
@ -38,11 +40,7 @@ const Admin = () => {
}
if (!user || !user.admin) {
return (
<div className="pageWrapper">
<h1>Unauthorized</h1>
</div>
)
history.push("/login")
}
return (

View File

@ -8,7 +8,7 @@ import ScrobbleTable from "../Components/ScrobbleTable";
import AuthContext from '../Contexts/AuthContext';
const Dashboard = () => {
const history = useHistory();
// const history = useHistory();
let { user } = useContext(AuthContext);
let [loading, setLoading] = useState(true);
let [dashboardData, setDashboardData] = useState({});

View File

@ -16,6 +16,10 @@ const Login = () => {
history.push("/dashboard");
}
const redirectReset = () => {
history.push("/reset")
}
return (
<div className="pageWrapper">
<h1>
@ -42,7 +46,6 @@ const Login = () => {
<Field
name="password"
type="password"
required={boolTrue}
className="loginFields"
/>
</label>
@ -53,6 +56,14 @@ const Login = () => {
className="loginButton"
disabled={loading}
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Login"}</Button>
<br/><br/>
<Button
color="secondary"
type="button"
className="loginButton"
onClick={redirectReset}
disabled={loading}
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset Password"}</Button>
</Form>
</Formik>
</div>

View File

@ -35,7 +35,7 @@ const Profile = (route) => {
)
}
if (!username || Object.keys(profile).length === 0) {
if (!username || !profile.username) {
return (
<div className="pageWrapper">
Unable to fetch user
@ -50,7 +50,7 @@ const Profile = (route) => {
</h1>
<div className="profileBody">
Last 10 scrobbles...<br/>
<ScrobbleTable data={profile.scrobbles}/>
<ScrobbleTable data={profile.scrobbles}/>
</div>
</div>
);

15
web/src/Pages/Reset.css Normal file
View File

@ -0,0 +1,15 @@
.resetBody {
padding: 20px 5px 5px 5px;
font-size: 16pt;
width: 300px;
}
.resetFields {
width: 100%;
}
.resetButton {
height: 50px;
width: 100%;
margin-top:-5px;
}

153
web/src/Pages/Reset.js Normal file
View File

@ -0,0 +1,153 @@
import React, { useState, useEffect, useContext } from 'react';
import '../App.css';
import './Reset.css';
import { Button } from 'reactstrap';
import { Formik, Form, Field } from 'formik';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { validateResetPassword, sendPasswordReset } from '../Api/index';
import AuthContext from '../Contexts/AuthContext';
const Reset = (route) => {
let boolTrue = true;
const [loading, setLoading] = useState(true);
const [reset, setReset] = useState({});
const [sent, setSent] = useState(false);
let { ResetPassword } = useContext(AuthContext);
let reqToken = false;
if (route && route.match && route.match.params && route.match.params.token) {
reqToken = route.match.params.token
}
const sendReset = (values) => {
sendPasswordReset(values).then(() => {
setSent(true);
});
}
useEffect(() => {
if (!reqToken) {
setLoading(false);
return false;
}
validateResetPassword(reqToken)
.then(data => {
setReset(data);
console.log(data)
setLoading(false);
})
}, [reqToken])
if (loading) {
return (
<div className="pageWrapper">
<ScaleLoader color="#6AD7E5" />
</div>
)
}
if (sent) {
return (
<div className="pageWrapper">
<h1>
Check your email!
</h1>
</div>
)
}
if (!reqToken) {
return (
<div className="pageWrapper">
<h1>
Reset Password
</h1>
<div className="loginBody">
<Formik
initialValues={{ email: '' }}
onSubmit={values => sendReset(values)}
>
<Form>
<label>
Email<br/>
<Field
name="email"
type="email"
required={boolTrue}
className="loginFields"
/>
</label>
<br/><br/>
<Button
color="primary"
type="submit"
className="loginButton"
disabled={loading}
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset"}</Button>
</Form>
</Formik>
</div>
</div>
)
}
if (reqToken && !reset.valid) {
return (
<div className="pageWrapper">
Invalid Reset Token or Token expired
</div>
)
}
return (
<div className="pageWrapper">
<h1>
Reset Password
</h1>
<div className="resetBody">
<Formik
initialValues={{ password: '', comfirmpassword: '', token: reqToken }}
onSubmit={values => ResetPassword(values)}
>
<Form>
<label>
New Password<br/>
<Field
name="password"
type="password"
required={boolTrue}
className="resetFields"
/>
</label>
<br/>
<label>
Confirm New Password<br/>
<Field
name="comfirmpassword"
type="password"
required={boolTrue}
className="resetFields"
/>
</label>
<Field
name="token"
type="hidden"
className="resetFields"
/>
<br/><br/>
<Button
color="primary"
type="submit"
className="loginButton"
disabled={loading}
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset"}</Button>
</Form>
</Formik>
</div>
</div>
);
}
export default Reset;