diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10d39ba9..886c4170 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,12 +3,13 @@ stages: - bundle variables: - VERSION: 0.0.7 + VERSION: 0.0.8 build-go: image: golang:1.16.2 stage: build - only: master + only: + - master script: - go build -o goscrobble cmd/go-scrobble/*.go artifacts: @@ -22,7 +23,8 @@ build-go: build-react: image: node:15.12.0 stage: build - only: master + only: + - master script: - cd web - npm install @@ -35,7 +37,8 @@ build-react: bundle: image: bash:latest stage: bundle - only: master + only: + - master variables: GIT_STRATEGY: none before_script: diff --git a/docs/changelog.md b/docs/changelog.md index d92978d8..dbb4cd2e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,11 @@ +# 0.0.8 +- Added Admin/Site config page in frontend for admin users +- Added API POST/GET /config endpoint + # 0.0.7 - Switch redux -> Context - Remove excess packages - # 0.0.6 - Fix hitting dashboard when logged out - Clean up app.js diff --git a/internal/goscrobble/config.go b/internal/goscrobble/config.go new file mode 100644 index 00000000..951f01a7 --- /dev/null +++ b/internal/goscrobble/config.go @@ -0,0 +1,57 @@ +package goscrobble + +import ( + "errors" + "fmt" + "log" +) + +type Config struct { + Setting map[string]string `json:"configs"` +} + +func getAllConfigs() (Config, error) { + config := Config{} + configs := make(map[string]string) + + rows, err := db.Query("SELECT `key`, `value` FROM `config`") + if err != nil { + log.Printf("Failed to fetch config: %+v", err) + return config, errors.New("Failed to fetch configs") + } + defer rows.Close() + + for rows.Next() { + var key string + var value string + err := rows.Scan(&key, &value) + if err != nil { + log.Printf("Failed to fetch config: %+v", err) + return config, errors.New("Failed to fetch configs") + } + + // Append + configs[key] = value + } + + // Assign the data to the parent + config.Setting = configs + + err = rows.Err() + if err != nil { + log.Printf("Failed to fetch config: %+v", err) + return config, errors.New("Failed to fetch configs") + } + + return config, nil +} + +func updateConfigValue(key string, value string) error { + _, err := db.Exec("UPDATE `config` SET `value` = ? WHERE `key` = ?", value, key) + if err != nil { + fmt.Printf("Failed to update config: %+v", err) + return errors.New("Failed to update config value.") + } + + return nil +} diff --git a/internal/goscrobble/jwt.go b/internal/goscrobble/jwt.go index 34796dcf..57d85e79 100644 --- a/internal/goscrobble/jwt.go +++ b/internal/goscrobble/jwt.go @@ -15,6 +15,7 @@ var JwtExpiry time.Duration type CustomClaims struct { Username string `json:"username"` Email string `json:"email"` + Admin bool `json:"admin"` jwt.StandardClaims } @@ -23,6 +24,7 @@ func generateJWTToken(user User) (string, error) { atClaims["sub"] = user.UUID atClaims["username"] = user.Username atClaims["email"] = user.Email + atClaims["admin"] = user.Admin atClaims["iat"] = time.Now().Unix() atClaims["exp"] = time.Now().Add(JwtExpiry).Unix() at := jwt.NewWithClaims(jwt.SigningMethodHS512, atClaims) diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index c08efb99..d242682c 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -51,6 +51,10 @@ func HandleRequests(port string) { // JWT Auth v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET") + // Config auth + v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET") + v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST") + // No Auth v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST") v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST") @@ -117,6 +121,16 @@ func throwOkMessage(w http.ResponseWriter, m string) { 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{ @@ -142,9 +156,10 @@ func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http authToken := strings.Replace(fullToken, "Bearer ", "", 1) if authToken == "" { throwUnauthorized(w, "A token is required") + return } - userUuid, err := getUserForToken(authToken) + userUuid, err := getUserUuidForToken(authToken) if err != nil { throwUnauthorized(w, err.Error()) return @@ -180,6 +195,32 @@ func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string) } } +// 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) { @@ -255,8 +296,7 @@ func handleStats(w http.ResponseWriter, r *http.Request) { func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) { bodyJson, err := decodeJson(r.Body) if err != nil { - // If we can't decode. Lets tell them nicely. - http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest) + throwInvalidJson(w) return } @@ -302,6 +342,38 @@ func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser strin w.Write(json) } +// fetchScrobbles - Return an array of scrobbles +func fetchConfig(w http.ResponseWriter, r *http.Request, jwtUser string) { + config, err := getAllConfigs() + if err != nil { + throwOkError(w, "Failed to fetch scrobbles") + return + } + + json, _ := json.Marshal(&config) + w.WriteHeader(http.StatusOK) + w.Write(json) +} + +// fetchScrobbles - Return an array of scrobbles +func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) { + bodyJson, err := decodeJson(r.Body) + if err != nil { + throwInvalidJson(w) + return + } + + for k, v := range bodyJson { + err = updateConfigValue(k, fmt.Sprintf("%s", v)) + if err != nil { + throwOkError(w, err.Error()) + return + } + } + + throwOkMessage(w, "Config updated successfully") +} + // FRONTEND HANDLING // ServerHTTP - Frontend server diff --git a/internal/goscrobble/tokens.go b/internal/goscrobble/tokens.go index 5c6f1364..e45cccba 100644 --- a/internal/goscrobble/tokens.go +++ b/internal/goscrobble/tokens.go @@ -15,7 +15,7 @@ func generateToken(n int) string { return string(b) } -func getUserForToken(token string) (string, error) { +func getUserUuidForToken(token string) (string, error) { var uuid string cachedKey := getRedisVal("user_token:" + token) if cachedKey == "" { diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index c1626325..f889bf69 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -98,16 +98,16 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) { } if strings.Contains(logReq.Username, "@") { - err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password` FROM `users` WHERE `email` = ? AND `active` = 1", - logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) + err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password`, `admin` FROM `users` WHERE `email` = ? AND `active` = 1", + logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password, &user.Admin) if err != nil { if err == sql.ErrNoRows { return resp, errors.New("Invalid Username or Password") } } } else { - err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password` FROM `users` WHERE `username` = ? AND `active` = 1", - logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password) + err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `username`, `email`, `password`, `admin` FROM `users` WHERE `username` = ? AND `active` = 1", + logReq.Username).Scan(&user.UUID, &user.Username, &user.Email, &user.Password, &user.Admin) if err == sql.ErrNoRows { return resp, errors.New("Invalid Username or Password") } @@ -193,3 +193,15 @@ func userAlreadyExists(req *RegisterRequest) bool { return count > 0 } + +func getUser(uuid 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 `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) + + if err == sql.ErrNoRows { + return user, errors.New("Invalid JWT Token") + } + + return user, nil +} diff --git a/migrations/6_configs.down.sql b/migrations/6_configs.down.sql new file mode 100644 index 00000000..b85acff1 --- /dev/null +++ b/migrations/6_configs.down.sql @@ -0,0 +1 @@ +TRUNCATE `config`; \ No newline at end of file diff --git a/migrations/6_configs.up.sql b/migrations/6_configs.up.sql new file mode 100644 index 00000000..b5f693c2 --- /dev/null +++ b/migrations/6_configs.up.sql @@ -0,0 +1,7 @@ +INSERT INTO + `config`(`key`, `value`) +VALUES + ('SPOTIFY_API_ID', ''), + ('SPOTIFY_API_SECRET', ''), + ('LASTFM_API_KEY', ''), + ('REGISTRATION_ENABLED', '1'); \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index de4226a5..26b71e09 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "goscrobble", "version": "0.1.0", "dependencies": { + "@material-ui/core": "^4.11.3", "@reduxjs/toolkit": "^1.5.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", @@ -15,6 +16,7 @@ "axios": "^0.21.1", "bootstrap": "^4.6.0", "formik": "^2.2.6", + "formik-material-ui": "^3.0.1", "jwt-decode": "^3.1.2", "react": "^17.0.2", "react-bootstrap": "^1.5.2", @@ -2370,6 +2372,155 @@ "node": ">=8" } }, + "node_modules/@material-ui/core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", + "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.3", + "@material-ui/system": "^4.11.3", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/core/node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, + "node_modules/@material-ui/styles": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz", + "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/styles/node_modules/csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + }, + "node_modules/@material-ui/system": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", + "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/system/node_modules/csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", @@ -6186,6 +6337,15 @@ "node": ">=0.10.0" } }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "node_modules/css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -9009,6 +9169,17 @@ "react": ">=16.8.0" } }, + "node_modules/formik-material-ui": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/formik-material-ui/-/formik-material-ui-3.0.1.tgz", + "integrity": "sha512-N8oxZIdhY70npRv86IfF6Zaaps9RL3a37XRdq02WDroB3XZC1mXs6lA/zQ09ZYFWYJp/UjI80SKVpVa/xJOJJA==", + "peerDependencies": { + "@material-ui/core": ">=4.0.0", + "formik": ">=2.0.0", + "react": ">=16.8.0", + "tiny-warning": ">=1.0.2" + } + }, "node_modules/formik/node_modules/deepmerge": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", @@ -9954,6 +10125,11 @@ "node": ">=8.12.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10115,6 +10291,14 @@ "node": ">=0.8.19" } }, + "node_modules/indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "dependencies": { + "symbol-observable": "1.2.0" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -10432,6 +10616,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -12845,6 +13034,89 @@ "verror": "1.10.0" } }, + "node_modules/jss": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.6.0.tgz", + "integrity": "sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz", + "integrity": "sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.6.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz", + "integrity": "sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz", + "integrity": "sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz", + "integrity": "sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz", + "integrity": "sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz", + "integrity": "sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz", + "integrity": "sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.6.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", @@ -24050,6 +24322,96 @@ } } }, + "@material-ui/core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", + "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.3", + "@material-ui/system": "^4.11.3", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "dependencies": { + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + } + } + }, + "@material-ui/styles": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz", + "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + } + } + }, + "@material-ui/system": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", + "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + } + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "requires": {} + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", @@ -27073,6 +27435,15 @@ } } }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -29281,6 +29652,12 @@ } } }, + "formik-material-ui": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/formik-material-ui/-/formik-material-ui-3.0.1.tgz", + "integrity": "sha512-N8oxZIdhY70npRv86IfF6Zaaps9RL3a37XRdq02WDroB3XZC1mXs6lA/zQ09ZYFWYJp/UjI80SKVpVa/xJOJJA==", + "requires": {} + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -30045,6 +30422,11 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -30152,6 +30534,14 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "requires": { + "symbol-observable": "1.2.0" + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -30379,6 +30769,11 @@ "is-extglob": "^2.1.1" } }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -32151,6 +32546,85 @@ "verror": "1.10.0" } }, + "jss": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.6.0.tgz", + "integrity": "sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw==", + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-camel-case": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz", + "integrity": "sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A==", + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.6.0" + } + }, + "jss-plugin-default-unit": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz", + "integrity": "sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "jss-plugin-global": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz", + "integrity": "sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "jss-plugin-nested": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz", + "integrity": "sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz", + "integrity": "sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz", + "integrity": "sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.6.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz", + "integrity": "sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.6.0" + } + }, "jsx-ast-utils": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", diff --git a/web/package.json b/web/package.json index 44605bb0..7c74978e 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@material-ui/core": "^4.11.3", "@reduxjs/toolkit": "^1.5.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", @@ -10,6 +11,7 @@ "axios": "^0.21.1", "bootstrap": "^4.6.0", "formik": "^2.2.6", + "formik-material-ui": "^3.0.1", "jwt-decode": "^3.1.2", "react": "^17.0.2", "react-bootstrap": "^1.5.2", diff --git a/web/src/Api/index.js b/web/src/Api/index.js index ba263584..b4bab48c 100644 --- a/web/src/Api/index.js +++ b/web/src/Api/index.js @@ -25,6 +25,7 @@ export const PostLogin = (formValues) => { uuid: expandedUser.sub, exp: expandedUser.exp, username: expandedUser.username, + admin: expandedUser.admin } toast.success('Successfully logged in.'); @@ -72,3 +73,31 @@ export const getRecentScrobbles = (id) => { return data.data; }); }; + +export const getConfigs = () => { + return axios.get(process.env.REACT_APP_API_URL + "config", { headers: getHeaders() }) + .then((data) => { + return data.data; + }); +}; + +export const postConfigs = (values, toggle) => { + if (toggle) { + values.REGISTRATION_ENABLED = "1" + } else { + values.REGISTRATION_ENABLED = "0" + } + + return axios.post(process.env.REACT_APP_API_URL + "config", values, { headers: getHeaders() }) + .then((data) => { + if (data.data && data.data.message) { + toast.success(data.data.message); + } else if (data.data && data.data.error) { + toast.error(data.data.error); + } + }) + .catch(() => { + toast.error('Error updating values'); + }); +}; + diff --git a/web/src/App.js b/web/src/App.js index 7c440fec..d4266fcd 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,16 +1,17 @@ -import './App.css'; +import { Route, Switch, withRouter } from 'react-router-dom'; + import Home from './Pages/Home'; import About from './Pages/About'; - import Dashboard from './Pages/Dashboard'; import Profile from './Pages/Profile'; +import Admin from './Pages/Admin'; import Login from './Pages/Login'; -import Settings from './Pages/Settings'; import Register from './Pages/Register'; + import Navigation from './Components/Navigation'; -import { Route, Switch, withRouter } from 'react-router-dom'; import 'bootstrap/dist/css/bootstrap.min.css'; +import './App.css'; const App = () => { let boolTrue = true @@ -24,7 +25,8 @@ const App = () => { - + + diff --git a/web/src/Components/Navigation.js b/web/src/Components/Navigation.js index 5c3ea871..abc6dd4b 100644 --- a/web/src/Components/Navigation.js +++ b/web/src/Components/Navigation.js @@ -33,10 +33,14 @@ const Navigation = () => { let { user, Logout } = useContext(AuthContext); let [collapsed, setCollapsed] = useState(true); + const toggleCollapsed = () => { + setCollapsed(!collapsed) + } + const renderMobileNav = () => { return logo GoScrobble - + {user ? :