mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-12-25 16:19:00 +00:00
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
This commit is contained in:
parent
e570314ac2
commit
fd615102a8
@ -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=""
|
||||
|
@ -3,7 +3,7 @@ stages:
|
||||
- bundle
|
||||
|
||||
variables:
|
||||
VERSION: 0.0.9
|
||||
VERSION: 0.0.10
|
||||
|
||||
build-go:
|
||||
image: golang:1.16.2
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
5
go.mod
@ -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
25
go.sum
@ -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=
|
||||
|
@ -1,4 +0,0 @@
|
||||
package goscrobble
|
||||
|
||||
func getImageLastFM(src string) {
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
66
internal/goscrobble/ingress_multiscrobbler.go
Normal file
66
internal/goscrobble/ingress_multiscrobbler.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
104
internal/goscrobble/server_middleware.go
Normal file
104
internal/goscrobble/server_middleware.go
Normal 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)
|
||||
})
|
||||
}
|
58
internal/goscrobble/server_responses.go
Normal file
58
internal/goscrobble/server_responses.go
Normal 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)
|
||||
}
|
43
internal/goscrobble/server_static.go
Normal file
43
internal/goscrobble/server_static.go
Normal 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)
|
||||
}
|
18
internal/goscrobble/smtp.go
Normal file
18
internal/goscrobble/smtp.go
Normal 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
|
||||
}
|
14
internal/goscrobble/timers.go
Normal file
14
internal/goscrobble/timers.go
Normal file
@ -0,0 +1,14 @@
|
||||
package goscrobble
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ClearTokenTimer() {
|
||||
go func() {
|
||||
for now := range time.Tick(time.Second) {
|
||||
fmt.Println(now)
|
||||
}
|
||||
}()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
1
migrations/7_resettoken.down.sql
Normal file
1
migrations/7_resettoken.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS `resettoken`;
|
6
migrations/7_resettoken.up.sql
Normal file
6
migrations/7_resettoken.up.sql
Normal 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;
|
@ -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 {};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
}}
|
||||
|
@ -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 (
|
||||
|
@ -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({});
|
||||
|
@ -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>
|
||||
|
@ -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
15
web/src/Pages/Reset.css
Normal 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
153
web/src/Pages/Reset.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user