diff --git a/docs/changelog.md b/docs/changelog.md index 02e77cdd..17f6527c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,9 @@ +# 0.0.12 +- Add client TZ support + Selectable on user page +- Move token auth to GET ?key=XYZ for wider webhook support +- Add Multiscrobbler support +- Add /api/v1/serverinfo for version information + # 0.0.11 - Fix redirects to /login for auth required pages - Add handling for 401/429 + No connection responses in API calls @@ -22,7 +28,7 @@ # 0.0.8 - Added Admin/Site config page in frontend for admin users -- Added API POST/GET /config endpoint +- Added API POST/GET /config endpointnpm install react-select-timezone # 0.0.7 - Switch redux -> Context diff --git a/internal/goscrobble/config.go b/internal/goscrobble/config.go index a62f0068..9100a40e 100644 --- a/internal/goscrobble/config.go +++ b/internal/goscrobble/config.go @@ -11,6 +11,10 @@ type Config struct { Setting map[string]string `json:"configs"` } +type ServerInfo struct { + Version string `json:"version"` +} + func getAllConfigs() (Config, error) { config := Config{} configs := make(map[string]string) diff --git a/internal/goscrobble/ingress_multiscrobbler.go b/internal/goscrobble/ingress_multiscrobbler.go index 0c12ac26..4e103725 100644 --- a/internal/goscrobble/ingress_multiscrobbler.go +++ b/internal/goscrobble/ingress_multiscrobbler.go @@ -4,63 +4,70 @@ import ( "database/sql" "fmt" "net" + "time" ) +type MultiScrobblerInput struct { + Artists []string `json:"artists"` + Album string `json:"album"` + Track string `json:"track"` + PlayedAt time.Time `json:"playDate"` + Duration string `json:"duration"` +} + // 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 { + // if data["artists"] == nil { // return errors.New("Missing artist data") // } - // if data["Album"] == nil { + // if data["album"] == nil { // return errors.New("Missing album data") // } - // if data["Name"] == nil { + // if data["track"] == 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 track artists + // for _, artist := range data["artists"] { + // artist, err := insertArtist(artist.Name, "", artist.ID.String(), tx) + // if err != nil { + // log.Printf("%+v", err) + // return errors.New("Failed to map artist: " + artist.Name) + // } + // artists = append(artists, artist.Uuid) + // } // // 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) + // 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) + // // Insert track if not exist + // length := timestampToSeconds(fmt.Sprintf("%s", data["RunTime"])) + // track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), length, 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) + // // Insert scrobble if not exist + // timestamp := time.Now() + // fmt.Println(timestamp) + // err = insertScrobble(userUUID, track.Uuid, "jellyfin", timestamp, 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/ingress_spotify.go b/internal/goscrobble/ingress_spotify.go index 4a219d89..0c262599 100644 --- a/internal/goscrobble/ingress_spotify.go +++ b/internal/goscrobble/ingress_spotify.go @@ -124,7 +124,6 @@ func (user *User) updateSpotifyPlaydata() { break } tx.Commit() - fmt.Printf("Updated spotify track: %+v", v.Track.Name) } } diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index b6bd2e92..decc0347 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -38,7 +38,7 @@ func HandleRequests(port string) { // JWT Auth - Own profile only (Uses uuid in JWT) v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(fetchUser), lightLimiter)).Methods("GET") - // v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH") + v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(patchUser), lightLimiter)).Methods("PATCH") v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(getSpotifyClientID), lightLimiter)).Methods("GET") v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(deleteSpotifyLink), lightLimiter)).Methods("DELETE") v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET") @@ -55,6 +55,7 @@ func HandleRequests(port string) { v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST") v1.HandleFunc("/sendreset", limitMiddleware(handleSendReset, heavyLimiter)).Methods("POST") v1.HandleFunc("/resetpassword", limitMiddleware(handleResetPassword, heavyLimiter)).Methods("POST") + v1.HandleFunc("/serverinfo", fetchServerInfo).Methods("GET") // Redirect from Spotify Oauth v1.HandleFunc("/link/spotify", limitMiddleware(postSpotifyReponse, lightLimiter)) @@ -67,11 +68,12 @@ func HandleRequests(port string) { r.PathPrefix("/").Handler(spa) c := cors.New(cors.Options{ - // Grrrr CORS. To clean up at a later date AllowedOrigins: []string{"*"}, AllowCredentials: true, + AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE"}, AllowedHeaders: []string{"*"}, }) + handler := c.Handler(r) // Serve it up! @@ -219,6 +221,7 @@ func handleResetPassword(w http.ResponseWriter, r *http.Request) { // serveEndpoint - API stuffs func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) { bodyJson, err := decodeJson(r.Body) + fmt.Println(err) if err != nil { throwInvalidJson(w) return @@ -291,6 +294,29 @@ func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser s w.Write(json) } +// patchUser - Update specific values +func patchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) { + userFull, err := getUser(jwtUser) + if err != nil { + throwOkError(w, "Failed to fetch user information") + return + } + + bodyJson, _ := decodeJson(r.Body) + + ip := getUserIp(r) + for k, v := range bodyJson { + val := fmt.Sprintf("%s", v) + if k == "timezone" { + if isValidTimezone(val) { + userFull.updateUser("timezone", val, ip) + } + } + } + + throwOkMessage(w, "User updated successfully") +} + // fetchScrobbles - Return an array of scrobbles func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) { resp, err := fetchScrobblesForUser(reqUser, 100, 1) @@ -408,3 +434,13 @@ func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, u string, v strin throwOkMessage(w, "Spotify account successfully unlinked") } + +func fetchServerInfo(w http.ResponseWriter, r *http.Request) { + info := ServerInfo{ + Version: "0.0.11", + } + + js, _ := json.Marshal(&info) + w.WriteHeader(http.StatusOK) + w.Write(js) +} diff --git a/internal/goscrobble/server_middleware.go b/internal/goscrobble/server_middleware.go index f283b66d..df4f5800 100644 --- a/internal/goscrobble/server_middleware.go +++ b/internal/goscrobble/server_middleware.go @@ -20,14 +20,21 @@ 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 == "" { + key := "" + urlParams := r.URL.Query() + if val, ok := urlParams["key"]; ok { + key = val[0] + } else { + throwUnauthorized(w, "No key parameter provided") + return + } + + if key == "" { throwUnauthorized(w, "A token is required") return } - userUuid, err := getUserUuidForToken(authToken) + userUuid, err := getUserUuidForToken(key) if err != nil { throwUnauthorized(w, err.Error()) return diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index 43781ca0..3d9722e1 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -156,14 +156,14 @@ func insertUser(username string, email string, password []byte, ip net.IP) error return err } -func updateUser(uuid string, field string, value string, ip net.IP) error { - _, err := db.Exec("UPDATE users SET `"+field+"` = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = ?", value, uuid, ip) +func (user *User) updateUser(field string, value string, ip net.IP) error { + _, err := db.Exec("UPDATE users SET `"+field+"` = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = UUID_TO_BIN(?, true)", value, ip, user.UUID) return err } -func updateUserDirect(uuid string, field string, value string) error { - _, err := db.Exec("UPDATE users SET `"+field+"` = ? WHERE uuid = ?", value, uuid) +func (user *User) updateUserDirect(field string, value string) error { + _, err := db.Exec("UPDATE users SET `"+field+"` = ? WHERE uuid = UUID_TO_BIN(?, true)", value, user.UUID) return err } diff --git a/internal/goscrobble/utils.go b/internal/goscrobble/utils.go index b379e0c5..666d5b91 100644 --- a/internal/goscrobble/utils.go +++ b/internal/goscrobble/utils.go @@ -10,6 +10,7 @@ import ( "net/http" "regexp" "strings" + "time" ) var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") @@ -79,6 +80,10 @@ func getUserIp(r *http.Request) net.IP { } } + if host == "" { + host = "0.0.0.0" + } + ip = net.ParseIP(host) return ip } @@ -144,3 +149,8 @@ func filterSlice(s []string) []string { fmt.Printf("RESTULS: %+v", result) return result } + +func isValidTimezone(tz string) bool { + _, err := time.LoadLocation(tz) + return err == nil +} diff --git a/migrations/2_users.up.sql b/migrations/2_users.up.sql index 2747fd30..44bea470 100644 --- a/migrations/2_users.up.sql +++ b/migrations/2_users.up.sql @@ -1,8 +1,8 @@ CREATE TABLE IF NOT EXISTS `users` ( `uuid` BINARY(16) PRIMARY KEY, - `created_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT NOW(), `created_ip` VARBINARY(16) NULL DEFAULT NULL, - `modified_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL DEFAULT NOW(), `modified_ip` VARBINARY(16) NULL DEFAULT NULL, `username` VARCHAR(64) NOT NULL, `password` VARCHAR(60) NOT NULL, diff --git a/web/package-lock.json b/web/package-lock.json index 26b71e09..a9801c9f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,10 +22,10 @@ "react-bootstrap": "^1.5.2", "react-cookie": "^4.0.3", "react-dom": "^17.0.2", - "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-spinners": "^0.10.6", + "react-timezone-select": "^0.10.7", "react-toastify": "^7.0.3", "reactstrap": "^8.9.0" }, @@ -1518,6 +1518,66 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" }, + "node_modules/@emotion/react": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.1.5.tgz", + "integrity": "sha512-xfnZ9NJEv9SU9K2sxXM06lzjK245xSeHRpUh67eARBm3PBHjjKIZlfWZ7UQvD0Obvw6ZKjlC79uHrlzFYpOB/Q==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@emotion/cache": "^11.1.3", + "@emotion/serialize": "^1.0.0", + "@emotion/sheet": "^1.0.1", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/react/node_modules/@emotion/cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz", + "integrity": "sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA==", + "dependencies": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.0.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.3" + } + }, + "node_modules/@emotion/react/node_modules/@emotion/serialize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.1.tgz", + "integrity": "sha512-TXlKs5sgUKhFlszp/rg4lIAZd7UUSmJpwaf9/lAEFcUh2vPi32i7x4wk7O8TN8L8v2Ol8k0CxnhRBY0zQalTxA==", + "dependencies": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/react/node_modules/@emotion/sheet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz", + "integrity": "sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g==" + }, + "node_modules/@emotion/react/node_modules/@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, "node_modules/@emotion/serialize": { "version": "0.11.16", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", @@ -3390,17 +3450,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -13463,6 +13512,11 @@ "node": ">= 0.6" } }, + "node_modules/memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -16713,6 +16767,17 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "node_modules/react-input-autosize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", + "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16759,36 +16824,6 @@ "react": "0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/react-redux": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz", - "integrity": "sha512-ZhAmQ1lrK+Pyi0ZXNMUZuYxYAZd59wFuVDGUt536kSGdD0ya9Q7BfsE95E3TsFLE3kOSFp5m6G5qbatE+Ic1+w==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/react-redux": "^7.1.16", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.1" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17", - "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -16973,6 +17008,46 @@ "semver": "bin/semver" } }, + "node_modules/react-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.0.tgz", + "integrity": "sha512-SBPD1a3TJqE9zoI/jfOLCAoLr/neluaeokjOixr3zZ1vHezkom8K0A9J4QG9IWDqIDE9K/Mv+0y1GjidC2PDtQ==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.0.0", + "@emotion/react": "^11.1.1", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-input-autosize": "^3.0.0", + "react-transition-group": "^4.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/react-select/node_modules/@emotion/cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz", + "integrity": "sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA==", + "dependencies": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.0.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.3" + } + }, + "node_modules/react-select/node_modules/@emotion/sheet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz", + "integrity": "sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g==" + }, + "node_modules/react-select/node_modules/@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, "node_modules/react-spinners": { "version": "0.10.6", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.10.6.tgz", @@ -16985,6 +17060,20 @@ "react-dom": "^16.0.0 || ^17.0.0" } }, + "node_modules/react-timezone-select": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/react-timezone-select/-/react-timezone-select-0.10.7.tgz", + "integrity": "sha512-JPnrYcXf3NqqNl4HYDsDaS6OiUEPW6i7xeGE1xOA7SPuh7oR5vzKj0g0kbg5IH8LkiWTZNWk5NbBSJkRHp/dlA==", + "dependencies": { + "react-select": "^4.0.2", + "spacetime": "^6.12.3", + "spacetime-informal": "^0.5.0" + }, + "peerDependencies": { + "react": "^17.0.1", + "react-dom": "^17.0.1" + } + }, "node_modules/react-toastify": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.3.tgz", @@ -18851,6 +18940,16 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "node_modules/spacetime": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.14.0.tgz", + "integrity": "sha512-pz/nMIRGNSJeFfDFvhPjMHXhFU1NcrYnpydMuSS2Zsk0NEoHJc2rRKXugkmlqUv/l/fPxWVJVnj8isVS0//vbQ==" + }, + "node_modules/spacetime-informal": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/spacetime-informal/-/spacetime-informal-0.5.0.tgz", + "integrity": "sha512-cdSsniJJfJJTBdeVvXtooxyXzrRfoBVjAl3usQl9DgGExB3XN3deA3MwjInnD/26C/lANf3dU54bT2YweAGrOw==" + }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -19408,6 +19507,11 @@ "node": ">=8" } }, + "node_modules/stylis": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.9.tgz", + "integrity": "sha512-ci7pEFNVW3YJiWEzqPOMsAjY6kgraZ3ZgBfQ5HYbNtLJEsQ0G46ejWZpfSSCp/FaSiCSGGhzL9O2lN+2cB6ong==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -23661,6 +23765,56 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" }, + "@emotion/react": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.1.5.tgz", + "integrity": "sha512-xfnZ9NJEv9SU9K2sxXM06lzjK245xSeHRpUh67eARBm3PBHjjKIZlfWZ7UQvD0Obvw6ZKjlC79uHrlzFYpOB/Q==", + "requires": { + "@babel/runtime": "^7.7.2", + "@emotion/cache": "^11.1.3", + "@emotion/serialize": "^1.0.0", + "@emotion/sheet": "^1.0.1", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + }, + "dependencies": { + "@emotion/cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz", + "integrity": "sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.0.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.3" + } + }, + "@emotion/serialize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.1.tgz", + "integrity": "sha512-TXlKs5sgUKhFlszp/rg4lIAZd7UUSmJpwaf9/lAEFcUh2vPi32i7x4wk7O8TN8L8v2Ol8k0CxnhRBY0zQalTxA==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz", + "integrity": "sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g==" + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + } + } + }, "@emotion/serialize": { "version": "0.11.16", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", @@ -25055,17 +25209,6 @@ "csstype": "^3.0.2" } }, - "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -32905,6 +33048,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -35510,6 +35658,14 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "react-input-autosize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", + "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -35549,26 +35705,6 @@ "warning": "^4.0.2" } }, - "react-redux": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz", - "integrity": "sha512-ZhAmQ1lrK+Pyi0ZXNMUZuYxYAZd59wFuVDGUt536kSGdD0ya9Q7BfsE95E3TsFLE3kOSFp5m6G5qbatE+Ic1+w==", - "requires": { - "@babel/runtime": "^7.12.1", - "@types/react-redux": "^7.1.16", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.1" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } - } - }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -35723,6 +35859,44 @@ } } }, + "react-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.0.tgz", + "integrity": "sha512-SBPD1a3TJqE9zoI/jfOLCAoLr/neluaeokjOixr3zZ1vHezkom8K0A9J4QG9IWDqIDE9K/Mv+0y1GjidC2PDtQ==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.0.0", + "@emotion/react": "^11.1.1", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-input-autosize": "^3.0.0", + "react-transition-group": "^4.3.0" + }, + "dependencies": { + "@emotion/cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz", + "integrity": "sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.0.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.3" + } + }, + "@emotion/sheet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz", + "integrity": "sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g==" + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + } + } + }, "react-spinners": { "version": "0.10.6", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.10.6.tgz", @@ -35731,6 +35905,16 @@ "@emotion/core": "^10.0.35" } }, + "react-timezone-select": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/react-timezone-select/-/react-timezone-select-0.10.7.tgz", + "integrity": "sha512-JPnrYcXf3NqqNl4HYDsDaS6OiUEPW6i7xeGE1xOA7SPuh7oR5vzKj0g0kbg5IH8LkiWTZNWk5NbBSJkRHp/dlA==", + "requires": { + "react-select": "^4.0.2", + "spacetime": "^6.12.3", + "spacetime-informal": "^0.5.0" + } + }, "react-toastify": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.3.tgz", @@ -37221,6 +37405,16 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "spacetime": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.14.0.tgz", + "integrity": "sha512-pz/nMIRGNSJeFfDFvhPjMHXhFU1NcrYnpydMuSS2Zsk0NEoHJc2rRKXugkmlqUv/l/fPxWVJVnj8isVS0//vbQ==" + }, + "spacetime-informal": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/spacetime-informal/-/spacetime-informal-0.5.0.tgz", + "integrity": "sha512-cdSsniJJfJJTBdeVvXtooxyXzrRfoBVjAl3usQl9DgGExB3XN3deA3MwjInnD/26C/lANf3dU54bT2YweAGrOw==" + }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -37668,6 +37862,11 @@ } } }, + "stylis": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.9.tgz", + "integrity": "sha512-ci7pEFNVW3YJiWEzqPOMsAjY6kgraZ3ZgBfQ5HYbNtLJEsQ0G46ejWZpfSSCp/FaSiCSGGhzL9O2lN+2cB6ong==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/web/package.json b/web/package.json index 7c74978e..d781bbc9 100644 --- a/web/package.json +++ b/web/package.json @@ -17,10 +17,10 @@ "react-bootstrap": "^1.5.2", "react-cookie": "^4.0.3", "react-dom": "^17.0.2", - "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-spinners": "^0.10.6", + "react-timezone-select": "^0.10.7", "react-toastify": "^7.0.3", "reactstrap": "^8.9.0" }, diff --git a/web/src/Api/index.js b/web/src/Api/index.js index d61b0d15..1b64cc95 100644 --- a/web/src/Api/index.js +++ b/web/src/Api/index.js @@ -3,18 +3,24 @@ import jwt from 'jwt-decode' import { toast } from 'react-toastify'; function getHeaders() { - // Todo: move this to use Context values instead. + // TODO: move this to use Context values instead. const user = JSON.parse(localStorage.getItem('user')); if (user && user.jwt) { - return { Authorization: 'Bearer ' + user.jwt, changeOrigin: true }; + var unixtime = Math.round((new Date()).getTime() / 1000); + if (user.exp < unixtime) { + // TODO: Handle expiry nicer + toast.warning("Session expired. Please log in again") + } + + return { Authorization: 'Bearer ' + user.jwt }; } else { return {}; } } function getUserUuid() { - // Todo: move this to use Context values instead. + // TODO: move this to use Context values instead. const user = JSON.parse(localStorage.getItem('user')); if (user && user.uuid) { @@ -179,6 +185,15 @@ export const getUser = () => { }); }; +export const patchUser = (values) => { + return axios.patch(process.env.REACT_APP_API_URL + "user", values, { headers: getHeaders() }) + .then((data) => { + return data.data; + }).catch((error) => { + return handleErrorResp(error) + }); +}; + export const validateResetPassword = (tokenStr) => { return axios.get(process.env.REACT_APP_API_URL + "user/", { headers: getHeaders() }) .then((data) => { diff --git a/web/src/Pages/User.css b/web/src/Pages/User.css index 3ed5eaa4..7d8a1e75 100644 --- a/web/src/Pages/User.css +++ b/web/src/Pages/User.css @@ -1,4 +1,9 @@ .userBody { padding: 20px 5px 5px 5px; font-size: 16pt; +} + +.userDropdown { + color: #282C34; + font-size: 12pt; } \ No newline at end of file diff --git a/web/src/Pages/User.js b/web/src/Pages/User.js index 16465512..a5077fd8 100644 --- a/web/src/Pages/User.js +++ b/web/src/Pages/User.js @@ -4,9 +4,11 @@ import './User.css'; import { useHistory } from "react-router"; import AuthContext from '../Contexts/AuthContext'; import ScaleLoader from 'react-spinners/ScaleLoader'; -import { getUser } from '../Api/index' +import { getUser, patchUser } from '../Api/index' import { Button } from 'reactstrap'; + import { spotifyConnectionRequest, spotifyDisonnectionRequest } from '../Api/index' +import TimezoneSelect from 'react-timezone-select' const User = () => { const history = useHistory(); @@ -14,6 +16,11 @@ const User = () => { const [loading, setLoading] = useState(true); const [userdata, setUserdata] = useState({}); + const updateTimezone = (vals) => { + console.log(vals) + setUserdata({...userdata, timezone: vals}); + patchUser({timezone: vals.value}) + } useEffect(() => { if (!user) { @@ -45,6 +52,12 @@ const User = () => { Welcome {userdata.username}

+ Timezone
+
Created At: {userdata.created_at}
Email: {userdata.email}
Verified: {userdata.verified ? '✓' : '✖'}