- Add registration_enabled to /api/v1/serverinfo
- Add config table caching on save
- Fix redis TTL not being parsed correctly
- Move registration enabled to backend toggle
- Fixed navbar when loading /u/profile URL
- Token now shows on user page + can reset
- Added basic popup validation to disconnect/reset buttons
This commit is contained in:
Daniel Mason 2021-04-03 13:54:07 +13:00
parent f8bd321fbc
commit 9cbb94fc56
Signed by: idanoo
GPG Key ID: 387387CDBC02F132
21 changed files with 246 additions and 59 deletions

View File

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

View File

@ -1,3 +1,12 @@
# 0.0.16
- Add registration_enabled to /api/v1/serverinfo
- Add config table caching on save
- Fix redis TTL not being parsed correctly
- Move registration enabled to backend toggle
- Fixed navbar when loading /u/profile URL
- Token now shows on user page + can reset
- Added basic popup validation to disconnect/reset buttons
# 0.0.15 # 0.0.15
- Fix spotify track duration - Fix spotify track duration

View File

@ -4,7 +4,6 @@ GoScrobble runs as UTC and connects to MySQL as UTC. All timezone handling is do
## FRONTEND VARS ## FRONTEND VARS
These are stored in `web/.env.production` and `web/.env.development` These are stored in `web/.env.production` and `web/.env.development`
REACT_APP_REGISTRATION_DISABLED=true // Disables registration
REACT_APP_API_URL=https://goscrobble.com // Sets API URL REACT_APP_API_URL=https://goscrobble.com // Sets API URL

View File

@ -13,6 +13,7 @@ type Config struct {
type ServerInfo struct { type ServerInfo struct {
Version string `json:"version"` Version string `json:"version"`
RegistrationEnabled string `json:"registration_enabled"`
} }
func getAllConfigs() (Config, error) { func getAllConfigs() (Config, error) {

View File

@ -2,7 +2,6 @@ package goscrobble
import ( import (
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -21,10 +20,9 @@ type MultiScrobblerRequest struct {
// ParseMultiScrobblerInput - Transform API data // ParseMultiScrobblerInput - Transform API data
func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip net.IP, tx *sql.Tx) error { func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip net.IP, tx *sql.Tx) error {
// Cache key // Cache key
json, _ := json.Marshal(data) json := fmt.Sprintf("%s:%s:%s:%s", data.PlayedAt, data.Track, data.Album, userUUID)
redisKey := getMd5(string(json) + userUUID) redisKey := getMd5(json)
if getRedisKeyExists(redisKey) { if getRedisKeyExists(redisKey) {
fmt.Printf("Prevented duplicate entry!")
return nil return nil
} }
@ -63,8 +61,7 @@ func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip ne
return errors.New("Failed to map track") return errors.New("Failed to map track")
} }
// Add cache key for the duration of the song *2 since we're caching the start time too ttl := time.Duration(30) * time.Minute
ttl := time.Duration(data.Duration*2) * time.Second
setRedisValTtl(redisKey, "1", ttl) setRedisValTtl(redisKey, "1", ttl)
return nil return nil

View File

@ -58,7 +58,7 @@ func setRedisVal(key string, val string) error {
// setRedisTtl - Allows custom TTL // setRedisTtl - Allows custom TTL
func setRedisValTtl(key string, val string, ttl time.Duration) error { func setRedisValTtl(key string, val string, ttl time.Duration) error {
return redisDb.Set(ctx, redisPrefix+key, val, 0).Err() return redisDb.Set(ctx, redisPrefix+key, val, ttl).Err()
} }
// getRedisVal - Returns value if exists // getRedisVal - Returns value if exists

View File

@ -54,16 +54,7 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRespon
// Yeah this isn't great. But for now.. it works! Cache later // Yeah this isn't great. But for now.. it works! Cache later
total, err := getDbCount( total, err := getDbCount(
"SELECT COUNT(*) FROM `scrobbles` "+ "SELECT COUNT(*) FROM `scrobbles` WHERE `user` = UUID_TO_BIN(?, true) ", userUuid)
"JOIN tracks ON scrobbles.track = tracks.uuid "+
"JOIN track_artist ON track_artist.track = tracks.uuid "+
"JOIN track_album ON track_album.track = tracks.uuid "+
"JOIN artists ON track_artist.artist = artists.uuid "+
"JOIN albums ON track_album.album = albums.uuid "+
"JOIN users ON scrobbles.user = users.uuid "+
"WHERE user = UUID_TO_BIN(?, true) "+
"GROUP BY scrobbles.uuid",
userUuid)
if err != nil { if err != nil {
log.Printf("Failed to fetch scrobble count: %+v", err) log.Printf("Failed to fetch scrobble count: %+v", err)
@ -71,7 +62,7 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRespon
} }
rows, err := db.Query( rows, err := db.Query(
"SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, `artists`.`name`, `albums`.`name`,`tracks`.`name`, `scrobbles`.`source` FROM `scrobbles` "+ "SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, GROUP_CONCAT(`artists`.`name` separator ','), `albums`.`name`, `tracks`.`name`, `scrobbles`.`source` FROM `scrobbles` "+
"JOIN tracks ON scrobbles.track = tracks.uuid "+ "JOIN tracks ON scrobbles.track = tracks.uuid "+
"JOIN track_artist ON track_artist.track = tracks.uuid "+ "JOIN track_artist ON track_artist.track = tracks.uuid "+
"JOIN track_album ON track_album.track = tracks.uuid "+ "JOIN track_album ON track_album.track = tracks.uuid "+
@ -79,8 +70,10 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRespon
"JOIN albums ON track_album.album = albums.uuid "+ "JOIN albums ON track_album.album = albums.uuid "+
"JOIN users ON scrobbles.user = users.uuid "+ "JOIN users ON scrobbles.user = users.uuid "+
"WHERE user = UUID_TO_BIN(?, true) "+ "WHERE user = UUID_TO_BIN(?, true) "+
"GROUP BY scrobbles.uuid, albums.uuid "+
"ORDER BY scrobbles.created_at DESC LIMIT ?", "ORDER BY scrobbles.created_at DESC LIMIT ?",
userUuid, limit) userUuid, limit)
if err != nil { if err != nil {
log.Printf("Failed to fetch scrobbles: %+v", err) log.Printf("Failed to fetch scrobbles: %+v", err)
return scrobbleReq, errors.New("Failed to fetch scrobbles") return scrobbleReq, errors.New("Failed to fetch scrobbles")

View File

@ -321,6 +321,9 @@ func patchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser s
if isValidTimezone(val) { if isValidTimezone(val) {
userFull.updateUser("timezone", val, ip) userFull.updateUser("timezone", val, ip)
} }
} else if k == "token" {
token := generateToken(32)
userFull.updateUser("token", token, ip)
} }
} }
@ -363,11 +366,13 @@ func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) {
} }
for k, v := range bodyJson { for k, v := range bodyJson {
err = updateConfigValue(k, fmt.Sprintf("%s", v)) val := fmt.Sprintf("%s", v)
err = updateConfigValue(k, val)
if err != nil { if err != nil {
throwOkError(w, err.Error()) throwOkError(w, err.Error())
return return
} }
setRedisVal(k, val)
} }
throwOkMessage(w, "Config updated successfully") throwOkMessage(w, "Config updated successfully")
@ -446,8 +451,19 @@ func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, u string, v strin
} }
func fetchServerInfo(w http.ResponseWriter, r *http.Request) { func fetchServerInfo(w http.ResponseWriter, r *http.Request) {
cachedRegistrationEnabled := getRedisVal("REGISTRATION_ENABLED")
if cachedRegistrationEnabled == "" {
registrationEnabled, err := getConfigValue("REGISTRATION_ENABLED")
if err != nil {
throwOkError(w, "Error fetching serverinfo")
}
setRedisVal("REGISTRATION_ENABLED", registrationEnabled)
cachedRegistrationEnabled = registrationEnabled
}
info := ServerInfo{ info := ServerInfo{
Version: "0.0.15", Version: "0.0.16",
RegistrationEnabled: cachedRegistrationEnabled,
} }
js, _ := json.Marshal(&info) js, _ := json.Marshal(&info)

View File

@ -29,6 +29,7 @@ type User struct {
Active bool `json:"active"` Active bool `json:"active"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
Token string `json:"token"`
} }
type UserResponse struct { type UserResponse struct {
@ -42,6 +43,7 @@ type UserResponse struct {
Verified bool `json:"verified"` Verified bool `json:"verified"`
SpotifyUsername string `json:"spotify_username"` SpotifyUsername string `json:"spotify_username"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
Token string `json:"token"`
} }
// RegisterRequest - Incoming JSON // RegisterRequest - Incoming JSON
@ -211,8 +213,8 @@ func userAlreadyExists(req *RegisterRequest) bool {
func getUser(uuid string) (User, error) { func getUser(uuid string) (User, error) {
var user User 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`, `timezone` FROM `users` WHERE `uuid` = UUID_TO_BIN(?, true) AND `active` = 1", err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone`, `token` FROM `users` WHERE `uuid` = UUID_TO_BIN(?, true) AND `active` = 1",
uuid).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone) uuid).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone, &user.Token)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, errors.New("Invalid JWT Token") return user, errors.New("Invalid JWT Token")
@ -223,8 +225,8 @@ func getUser(uuid string) (User, error) {
func getUserByUsername(username string) (User, error) { func getUserByUsername(username string) (User, error) {
var user User 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`, `timezone` FROM `users` WHERE `username` = ? AND `active` = 1", err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone`, `token` FROM `users` WHERE `username` = ? AND `active` = 1",
username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone) username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone, &user.Token)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, errors.New("Invalid Username") return user, errors.New("Invalid Username")
@ -235,8 +237,8 @@ func getUserByUsername(username string) (User, error) {
func getUserByEmail(email string) (User, error) { func getUserByEmail(email string) (User, error) {
var user User 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`, `timezone` FROM `users` WHERE `email` = ? AND `active` = 1", err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone`, `token` 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, &user.Timezone) email).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone, &user.Token)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, errors.New("Invalid Email") return user, errors.New("Invalid Email")
@ -247,9 +249,9 @@ func getUserByEmail(email string) (User, error) {
func getUserByResetToken(token string) (User, error) { func getUserByResetToken(token string) (User, error) {
var user User 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`, timezone FROM `users` "+ err := db.QueryRow("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone`, `token` FROM `users` "+
"JOIN `resettoken` ON `resettoken`.`user` = `users`.`uuid` WHERE `resettoken`.`token` = ? AND `active` = 1", "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, &user.Timezone) token).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone, &user.Token)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, errors.New("Invalid Token") return user, errors.New("Invalid Token")

View File

@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`active` TINYINT(1) NOT NULL DEFAULT 1, `active` TINYINT(1) NOT NULL DEFAULT 1,
`admin` TINYINT(1) NOT NULL DEFAULT 0, `admin` TINYINT(1) NOT NULL DEFAULT 0,
`private` TINYINT(1) NOT NULL DEFAULT 0, `private` TINYINT(1) NOT NULL DEFAULT 0,
`timezone` VARCHAR(100) NOT NULL DEFAULT 'Etc/UTC', `timezone` VARCHAR(100) NOT NULL DEFAULT 'Pacific/Auckland',
KEY `usernameLookup` (`username`, `active`), KEY `usernameLookup` (`username`, `active`),
KEY `emailLookup` (`email`, `active`) KEY `emailLookup` (`email`, `active`)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

View File

@ -1,2 +1 @@
REACT_APP_API_URL=http://127.0.0.1:42069/api/v1/ REACT_APP_API_URL=http://127.0.0.1:42069/api/v1/
REACT_APP_REGISTRATION_DISABLED=false

35
web/package-lock.json generated
View File

@ -20,6 +20,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-confirm-alert": "^2.7.0",
"react-cookie": "^4.0.3", "react-cookie": "^4.0.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
@ -27,6 +28,7 @@
"react-spinners": "^0.10.6", "react-spinners": "^0.10.6",
"react-timezone-select": "^0.10.7", "react-timezone-select": "^0.10.7",
"react-toastify": "^7.0.3", "react-toastify": "^7.0.3",
"reactjs-popup": "^2.0.4",
"reactstrap": "^8.9.0" "reactstrap": "^8.9.0"
}, },
"devDependencies": { "devDependencies": {
@ -16633,6 +16635,15 @@
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
}, },
"node_modules/react-confirm-alert": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-confirm-alert/-/react-confirm-alert-2.7.0.tgz",
"integrity": "sha512-21NWtGK/e85+ZX3TLRpMc3IsU5Kj6Z9ElCOrkTIlwMzV9EancyXNlkqHGbtKP63a2iS6g5hOxROokmJOqKQiXA==",
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/react-cookie": { "node_modules/react-cookie": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz",
@ -17101,6 +17112,18 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/reactjs-popup": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.4.tgz",
"integrity": "sha512-G5jTXL2JkClKAYAdqedf+K9QvbNsFWvdbrXW1/vWiyanuCU/d7DtQzQux+uKOz2HeNVRsFQHvs7abs0Z7VLAhg==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/reactstrap": { "node_modules/reactstrap": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.9.0.tgz", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.9.0.tgz",
@ -35553,6 +35576,12 @@
} }
} }
}, },
"react-confirm-alert": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-confirm-alert/-/react-confirm-alert-2.7.0.tgz",
"integrity": "sha512-21NWtGK/e85+ZX3TLRpMc3IsU5Kj6Z9ElCOrkTIlwMzV9EancyXNlkqHGbtKP63a2iS6g5hOxROokmJOqKQiXA==",
"requires": {}
},
"react-cookie": { "react-cookie": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz",
@ -35934,6 +35963,12 @@
"prop-types": "^15.6.2" "prop-types": "^15.6.2"
} }
}, },
"reactjs-popup": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.4.tgz",
"integrity": "sha512-G5jTXL2JkClKAYAdqedf+K9QvbNsFWvdbrXW1/vWiyanuCU/d7DtQzQux+uKOz2HeNVRsFQHvs7abs0Z7VLAhg==",
"requires": {}
},
"reactstrap": { "reactstrap": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.9.0.tgz", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.9.0.tgz",

View File

@ -15,6 +15,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-confirm-alert": "^2.7.0",
"react-cookie": "^4.0.3", "react-cookie": "^4.0.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
@ -22,6 +23,7 @@
"react-spinners": "^0.10.6", "react-spinners": "^0.10.6",
"react-timezone-select": "^0.10.7", "react-timezone-select": "^0.10.7",
"react-toastify": "^7.0.3", "react-toastify": "^7.0.3",
"reactjs-popup": "^2.0.4",
"reactstrap": "^8.9.0" "reactstrap": "^8.9.0"
}, },
"scripts": { "scripts": {

View File

@ -243,3 +243,21 @@ export const spotifyDisonnectionRequest = () => {
}); });
} }
export const getServerInfo = () => {
return axios.get(process.env.REACT_APP_API_URL + "serverinfo")
.then((data) => {
return data.data
}).catch((error) => {
return handleErrorResp(error)
});
}
export const resetScrobbleToken = () => {
return axios.patch(process.env.REACT_APP_API_URL + "user", { token: "" }, { headers: getHeaders() })
.then((data) => {
return data.data
}).catch((error) => {
return handleErrorResp(error)
});
}

View File

@ -24,7 +24,7 @@ const Navigation = () => {
const location = useLocation(); const location = useLocation();
// Lovely hack to highlight the current page (: // Lovely hack to highlight the current page (:
let active = "Home" let active = "home"
if (location && location.pathname && location.pathname.length > 1) { if (location && location.pathname && location.pathname.length > 1) {
active = location.pathname.replace(/\//, ""); active = location.pathname.replace(/\//, "");
} }
@ -49,8 +49,8 @@ const Navigation = () => {
<Link <Link
key={menuItem} key={menuItem}
className="navLinkMobile" className="navLinkMobile"
style={active === menuItem ? activeStyle : {}} style={active === menuItem.toLowerCase() ? activeStyle : {}}
to={menuItem} to={menuItem.toLowerCase()}
onClick={toggleCollapsed} onClick={toggleCollapsed}
>{menuItem}</Link> >{menuItem}</Link>
</NavItem> </NavItem>
@ -76,8 +76,8 @@ const Navigation = () => {
<Link <Link
key={menuItem} key={menuItem}
className="navLinkMobile" className="navLinkMobile"
style={active === menuItem ? activeStyle : {}} style={active === "home" && menuItem.toLowerCase() === "home" ? activeStyle : (active === menuItem.toLowerCase() ? activeStyle : {})}
to={menuItem === "Home" ? "/" : menuItem} to={menuItem.toLowerCase() === "home" ? "/" : "/" + menuItem.toLowerCase()}
onClick={toggleCollapsed} onClick={toggleCollapsed}
>{menuItem} >{menuItem}
</Link> </Link>
@ -85,17 +85,17 @@ const Navigation = () => {
)} )}
<NavItem> <NavItem>
<Link <Link
to="/Login" to="/login"
style={active === "Login" ? activeStyle : {}} style={active === "login" ? activeStyle : {}}
className="navLinkMobile" className="navLinkMobile"
onClick={toggleCollapsed} onClick={toggleCollapsed}
>Login</Link> >Login</Link>
</NavItem> </NavItem>
<NavItem> <NavItem>
<Link <Link
to="/Register" to="/register"
className="navLinkMobile" className="navLinkMobile"
style={active === "Register" ? activeStyle : {}} style={active === "register" ? activeStyle : {}}
onClick={toggleCollapsed} onClick={toggleCollapsed}
>Register</Link> >Register</Link>
</NavItem> </NavItem>
@ -114,8 +114,8 @@ const Navigation = () => {
<Link <Link
key={menuItem} key={menuItem}
className="navLink" className="navLink"
style={active === menuItem ? activeStyle : {}} style={active === menuItem.toLowerCase() ? activeStyle : {}}
to={menuItem} to={"/" + menuItem.toLowerCase()}
> >
{menuItem} {menuItem}
</Link> </Link>
@ -126,8 +126,8 @@ const Navigation = () => {
<Link <Link
key={menuItem} key={menuItem}
className="navLink" className="navLink"
style={active === menuItem ? activeStyle : {}} style={active === "home" && menuItem.toLowerCase() === "home" ? activeStyle : (active === menuItem.toLowerCase() ? activeStyle : {})}
to={menuItem === "Home" ? "/" : menuItem} to={menuItem.toLowerCase() === "home" ? "/" : "/" + menuItem.toLowerCase()}
> >
{menuItem} {menuItem}
</Link> </Link>

View File

@ -17,8 +17,9 @@ const ScrobbleTable = (props) => {
{ {
props.data && props.data &&
props.data.map(function (element) { props.data.map(function (element) {
let localTime = new Date(element.time);
return <tr key={element.uuid}> return <tr key={element.uuid}>
<td>{element.time}</td> <td>{localTime.toLocaleString()}</td>
<td>{element.track}</td> <td>{element.track}</td>
<td>{element.artist}</td> <td>{element.artist}</td>
<td>{element.album}</td> <td>{element.album}</td>

View File

@ -24,6 +24,7 @@ const Dashboard = () => {
}) })
}, [user]) }, [user])
if (!user) { if (!user) {
history.push("/login") history.push("/login")
} }

View File

@ -11,7 +11,9 @@ const Profile = (route) => {
let username = false; let username = false;
if (route && route.match && route.match.params && route.match.params.uuid) { if (route && route.match && route.match.params && route.match.params.uuid) {
username = route.match.params.uuid username = route.match.params.uuid;
} else {
username = false;
} }
useEffect(() => { useEffect(() => {

View File

@ -1,16 +1,37 @@
import React, { useContext } from 'react'; import React, { useContext, useState, useEffect } from 'react';
import '../App.css'; import '../App.css';
import './Register.css'; import './Register.css';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import ScaleLoader from "react-spinners/ScaleLoader"; import ScaleLoader from "react-spinners/ScaleLoader";
import AuthContext from '../Contexts/AuthContext'; import AuthContext from '../Contexts/AuthContext';
import { Formik, Field, Form } from 'formik'; import { Formik, Field, Form } from 'formik';
import { useHistory } from "react-router"; import { useHistory } from 'react-router';
import { getServerInfo } from '../Api/index';
const Register = () => { const Register = () => {
const history = useHistory(); const history = useHistory();
let boolTrue = true; let boolTrue = true;
let { Register, user, loading } = useContext(AuthContext); let { Register, user, loading } = useContext(AuthContext);
let [serverInfo, setServerInfo] = useState({ registration_enabled: true });
let [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (user) {
return
}
getServerInfo()
.then(data => {
setServerInfo(data);
setIsLoading(false);
})
}, [user])
if (isLoading) {
return (
<div className="pageWrapper">
<ScaleLoader color="#6AD7E5" />
</div>
)
}
if (user) { if (user) {
history.push("/dashboard"); history.push("/dashboard");
@ -19,8 +40,7 @@ const Register = () => {
return ( return (
<div className="pageWrapper"> <div className="pageWrapper">
{ {
// TODO: Move to DB:config REGISTRATION_DISABLED=1|0 :upsidedownsmile: serverInfo.registration_enabled !== "1" ?
process.env.REACT_APP_REGISTRATION_DISABLED === "true" ?
<p>Registration is temporarily disabled. Please try again soon!</p> <p>Registration is temporarily disabled. Please try again soon!</p>
: :
<div> <div>

View File

@ -7,3 +7,43 @@
color: #282C34; color: #282C34;
font-size: 12pt; font-size: 12pt;
} }
.userButton {
height: 50px;
width: 100%;
margin-top:-5px;
}
.modal {
font-size: 12px;
}
.modal > .header {
width: 100%;
border-bottom: 1px solid gray;
font-size: 18px;
text-align: center;
padding: 5px;
}
.modal > .content {
width: 100%;
padding: 10px 5px;
}
.modal > .actions {
width: 100%;
padding: 10px 5px;
margin: auto;
text-align: center;
}
.modal > .close {
cursor: pointer;
position: absolute;
display: block;
padding: 2px 5px;
line-height: 20px;
right: -10px;
top: -10px;
font-size: 24px;
background: #ffffff;
border-radius: 18px;
border: 1px solid #cfcece;
}

View File

@ -6,8 +6,9 @@ import AuthContext from '../Contexts/AuthContext';
import ScaleLoader from 'react-spinners/ScaleLoader'; import ScaleLoader from 'react-spinners/ScaleLoader';
import { getUser, patchUser } from '../Api/index' import { getUser, patchUser } from '../Api/index'
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { confirmAlert } from 'react-confirm-alert';
import { spotifyConnectionRequest, spotifyDisonnectionRequest } from '../Api/index' import 'react-confirm-alert/src/react-confirm-alert.css';
import { spotifyConnectionRequest, spotifyDisonnectionRequest, resetScrobbleToken } from '../Api/index'
import TimezoneSelect from 'react-timezone-select' import TimezoneSelect from 'react-timezone-select'
const User = () => { const User = () => {
@ -17,11 +18,54 @@ const User = () => {
const [userdata, setUserdata] = useState({}); const [userdata, setUserdata] = useState({});
const updateTimezone = (vals) => { const updateTimezone = (vals) => {
console.log(vals)
setUserdata({...userdata, timezone: vals}); setUserdata({...userdata, timezone: vals});
patchUser({timezone: vals.value}) patchUser({timezone: vals.value})
} }
const resetTokenPopup = () => {
confirmAlert({
title: 'Reset token',
message: 'Resetting your token will require you to update your sources with the new token. Continue?',
buttons: [
{
label: 'Reset',
onClick: () => resetToken()
},
{
label: 'No',
}
]
});
};
const disconnectSpotifyPopup = () => {
confirmAlert({
title: 'Disconnect Spotify',
message: 'Are you sure you want to disconnect your spotify account?',
buttons: [
{
label: 'Disconnect',
onClick: () => spotifyDisonnectionRequest()
},
{
label: 'No',
}
]
});
};
const resetToken = () => {
setLoading(true);
resetScrobbleToken(user.uuid)
.then(() => {
getUser()
.then(data => {
setUserdata(data);
setLoading(false);
})
})
}
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
return return
@ -58,23 +102,31 @@ const User = () => {
value={userdata.timezone} value={userdata.timezone}
onChange={updateTimezone} onChange={updateTimezone}
/><br/> /><br/>
Token: {userdata.token}<br/>
<Button
color="primary"
type="button"
className="userButton"
onClick={resetTokenPopup}
>Reset Token</Button><br/><br/>
Created At: {userdata.created_at}<br/> Created At: {userdata.created_at}<br/>
Email: {userdata.email}<br/> Email: {userdata.email}<br/>
Verified: {userdata.verified ? '✓' : '✖'}<br/> Verified: {userdata.verified ? '✓' : '✖'}<br/>
{userdata.spotify_username {userdata.spotify_username
? <div>Spotify Account: {userdata.spotify_username}<br/><br/> ? <div>Spotify Account: {userdata.spotify_username}<br/><br/>
<Button <Button
color="secondary" color="secondary"
type="button" type="button"
className="loginButton" className="userButton"
onClick={spotifyDisonnectionRequest} onClick={disconnectSpotifyPopup}
>Disconnect Spotify</Button></div> >Disconnect Spotify</Button></div>
: <div> : <div>
<br/> <br/>
<Button <Button
color="primary" color="primary"
type="button" type="button"
className="loginButton" className="userButton"
onClick={spotifyConnectionRequest} onClick={spotifyConnectionRequest}
>Connect To Spotify</Button> >Connect To Spotify</Button>
</div> </div>