diff --git a/.env.example b/.env.example
index ca0c2360..9c662394 100644
--- a/.env.example
+++ b/.env.example
@@ -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=""
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 083d9d24..b750fe23 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,7 +3,7 @@ stages:
- bundle
variables:
- VERSION: 0.0.9
+ VERSION: 0.0.10
build-go:
image: golang:1.16.2
diff --git a/cmd/go-scrobble/main.go b/cmd/go-scrobble/main.go
index a17f3291..381c506c 100644
--- a/cmd/go-scrobble/main.go
+++ b/cmd/go-scrobble/main.go
@@ -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)
}
diff --git a/docs/changelog.md b/docs/changelog.md
index 8441ca50..7fa35a13 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -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)
diff --git a/docs/config.md b/docs/config.md
index f74f2eca..f7b42548 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -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))
diff --git a/go.mod b/go.mod
index 356355e9..7ed44571 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index fd1e7837..e97573db 100644
--- a/go.sum
+++ b/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=
diff --git a/internal/goscrobble/external_lastfm.go b/internal/goscrobble/external_lastfm.go
deleted file mode 100644
index 5c810364..00000000
--- a/internal/goscrobble/external_lastfm.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package goscrobble
-
-func getImageLastFM(src string) {
-}
diff --git a/internal/goscrobble/ingress_jellyfin.go b/internal/goscrobble/ingress_jellyfin.go
index 4add3b6b..002c035d 100644
--- a/internal/goscrobble/ingress_jellyfin.go
+++ b/internal/goscrobble/ingress_jellyfin.go
@@ -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")
}
diff --git a/internal/goscrobble/ingress_multiscrobbler.go b/internal/goscrobble/ingress_multiscrobbler.go
new file mode 100644
index 00000000..0c12ac26
--- /dev/null
+++ b/internal/goscrobble/ingress_multiscrobbler.go
@@ -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
+}
diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go
index 25e39c26..791a0439 100644
--- a/internal/goscrobble/server.go
+++ b/internal/goscrobble/server.go
@@ -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)
-}
diff --git a/internal/goscrobble/server_middleware.go b/internal/goscrobble/server_middleware.go
new file mode 100644
index 00000000..f283b66d
--- /dev/null
+++ b/internal/goscrobble/server_middleware.go
@@ -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)
+ })
+}
diff --git a/internal/goscrobble/server_responses.go b/internal/goscrobble/server_responses.go
new file mode 100644
index 00000000..207c120a
--- /dev/null
+++ b/internal/goscrobble/server_responses.go
@@ -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)
+}
diff --git a/internal/goscrobble/server_static.go b/internal/goscrobble/server_static.go
new file mode 100644
index 00000000..7e404244
--- /dev/null
+++ b/internal/goscrobble/server_static.go
@@ -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)
+}
diff --git a/internal/goscrobble/smtp.go b/internal/goscrobble/smtp.go
new file mode 100644
index 00000000..02ed7447
--- /dev/null
+++ b/internal/goscrobble/smtp.go
@@ -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
+}
diff --git a/internal/goscrobble/timers.go b/internal/goscrobble/timers.go
new file mode 100644
index 00000000..59f05e19
--- /dev/null
+++ b/internal/goscrobble/timers.go
@@ -0,0 +1,14 @@
+package goscrobble
+
+import (
+ "fmt"
+ "time"
+)
+
+func ClearTokenTimer() {
+ go func() {
+ for now := range time.Tick(time.Second) {
+ fmt.Println(now)
+ }
+ }()
+}
diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go
index 4c4bb54e..4fa4cf63 100644
--- a/internal/goscrobble/user.go
+++ b/internal/goscrobble/user.go
@@ -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
+}
diff --git a/migrations/7_resettoken.down.sql b/migrations/7_resettoken.down.sql
new file mode 100644
index 00000000..645d7447
--- /dev/null
+++ b/migrations/7_resettoken.down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS `resettoken`;
diff --git a/migrations/7_resettoken.up.sql b/migrations/7_resettoken.up.sql
new file mode 100644
index 00000000..b233f80d
--- /dev/null
+++ b/migrations/7_resettoken.up.sql
@@ -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;
diff --git a/web/src/Api/index.js b/web/src/Api/index.js
index 382b055f..a80f8e0c 100644
--- a/web/src/Api/index.js
+++ b/web/src/Api/index.js
@@ -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 {};
+ });
+};
+
diff --git a/web/src/App.js b/web/src/App.js
index e0e292f4..4cf1fb2c 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -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 = () => {