- Make email required
- Add basic navidrome/subsonic connection
- Tidy up request/response structure in backend
- Tidy Settings page
This commit is contained in:
Daniel Mason 2021-04-10 09:49:32 +12:00
parent 8294791abe
commit 48a99b31fd
Signed by: idanoo
GPG Key ID: 387387CDBC02F132
15 changed files with 402 additions and 87 deletions

View File

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

View File

@ -1,3 +1,9 @@
# 0.0.26
- Make email required
- Add basic navidrome/subsonic connection
- Tidy up request/response structure in backend
- Tidy Settings page
# 0.0.25 # 0.0.25
- Images now pull from spotify if setup! - Images now pull from spotify if setup!
- Show top artists/album - Show top artists/album

View File

@ -20,8 +20,7 @@ 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 := fmt.Sprintf("%s:%s:%s:%s", data.PlayedAt, data.Track, data.Album, userUUID) json := fmt.Sprintf("%s:%s:%s", data.PlayedAt, data.Track, userUUID)
fmt.Printf(json)
redisKey := getMd5(json) redisKey := getMd5(json)
if getRedisKeyExists(redisKey) { if getRedisKeyExists(redisKey) {
return nil return nil
@ -62,7 +61,7 @@ func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip ne
return errors.New("Failed to map track") return errors.New("Failed to map track")
} }
ttl := time.Duration(30) * time.Minute ttl := time.Duration(24) * time.Hour
setRedisValTtl(redisKey, "1", ttl) setRedisValTtl(redisKey, "1", ttl)
return nil return nil

View File

@ -0,0 +1,60 @@
package goscrobble
import (
"encoding/json"
"errors"
"fmt"
"net/http"
)
type NavidromeResponse struct {
Response struct {
Status string `json:"status"`
Version string `json:"version"`
Type string `json:"type"`
Serverversion string `json:"serverVersion"`
} `json:"subsonic-response"`
}
// updateSpotifyData - Pull data for all users
func updateNavidromeData() {
// Get all active users with a spotify token
users, err := getAllNavidromeUsers()
if err != nil {
fmt.Printf("Failed to fetch navidrome users")
return
}
for _, user := range users {
user.updateNavidromePlaydata()
}
}
func (user *User) updateNavidromePlaydata() {
_, err := user.getNavidromeTokens()
if err != nil {
fmt.Printf("No Navidrome token for user: %+v %+v", user.Username, err)
return
}
}
func validateNavidromeConnection(url string, username string, hash string, salt string) error {
fmt.Printf("url:%s, username:%s, hash:%s, salt:%s", url, username, hash, salt)
resp, err := http.Get(url + "/rest/ping.view?u=" + username + "&t=" + hash + "&s=" + salt + "&c=GoScrobble&v=1.16.1&f=json")
if err != nil {
return err
}
response := NavidromeResponse{}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&response)
if err != nil {
return err
}
if response.Response.Status == "ok" {
return nil
}
return errors.New("Failed to validate")
}

View File

@ -71,7 +71,7 @@ func connectSpotifyResponse(r *http.Request) error {
// Lets pull in last 30 minutes // Lets pull in last 30 minutes
time := time.Now().UTC().Add(-(time.Duration(30) * time.Minute)) time := time.Now().UTC().Add(-(time.Duration(30) * time.Minute))
err = insertOauthToken(userUuid, "spotify", token.AccessToken, token.RefreshToken, token.Expiry, spotifyUser.DisplayName, time) err = insertOauthToken(userUuid, "spotify", token.AccessToken, token.RefreshToken, token.Expiry, spotifyUser.DisplayName, time, "")
if err != nil { if err != nil {
return err return err
} }
@ -127,7 +127,7 @@ func (user *User) updateSpotifyPlaydata() {
// Check if token has changed.. if so, save it to db // Check if token has changed.. if so, save it to db
currToken, err := client.Token() currToken, err := client.Token()
err = insertOauthToken(user.UUID, "spotify", currToken.AccessToken, currToken.RefreshToken, currToken.Expiry, dbToken.Username, currTime) err = insertOauthToken(user.UUID, "spotify", currToken.AccessToken, currToken.RefreshToken, currToken.Expiry, dbToken.Username, currTime, "")
if err != nil { if err != nil {
fmt.Printf("Failed to update spotify token in database") fmt.Printf("Failed to update spotify token in database")
} }

View File

@ -14,14 +14,15 @@ type OauthToken struct {
Expiry time.Time `json:"expiry"` Expiry time.Time `json:"expiry"`
Username string `json:"username"` Username string `json:"username"`
LastSynced time.Time `json:"last_synced"` LastSynced time.Time `json:"last_synced"`
URL string `json:"url"`
} }
func getOauthToken(userUuid string, service string) (OauthToken, error) { func getOauthToken(userUuid string, service string) (OauthToken, error) {
var oauth OauthToken var oauth OauthToken
err := db.QueryRow("SELECT BIN_TO_UUID(`user`, true), `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced` FROM `oauth_tokens` "+ err := db.QueryRow("SELECT BIN_TO_UUID(`user`, true), `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced`, `url` FROM `oauth_tokens` "+
"WHERE `user` = UUID_TO_BIN(?, true) AND `service` = ?", "WHERE `user` = UUID_TO_BIN(?, true) AND `service` = ?",
userUuid, service).Scan(&oauth.UserUUID, &oauth.Service, &oauth.AccessToken, &oauth.RefreshToken, &oauth.Expiry, &oauth.Username, &oauth.LastSynced) userUuid, service).Scan(&oauth.UserUUID, &oauth.Service, &oauth.AccessToken, &oauth.RefreshToken, &oauth.Expiry, &oauth.Username, &oauth.LastSynced, &oauth.URL)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return oauth, errors.New("No token for user") return oauth, errors.New("No token for user")
@ -30,9 +31,9 @@ func getOauthToken(userUuid string, service string) (OauthToken, error) {
return oauth, nil return oauth, nil
} }
func insertOauthToken(userUuid string, service string, token string, refresh string, expiry time.Time, username string, lastSynced time.Time) error { func insertOauthToken(userUuid string, service string, token string, refresh string, expiry time.Time, username string, lastSynced time.Time, url string) error {
_, err := db.Exec("REPLACE INTO `oauth_tokens` (`user`, `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced`) "+ _, err := db.Exec("REPLACE INTO `oauth_tokens` (`user`, `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced`, `url`) "+
"VALUES (UUID_TO_BIN(?, true),?,?,?,?,?,?)", userUuid, service, token, refresh, expiry, username, lastSynced) "VALUES (UUID_TO_BIN(?, true),?,?,?,?,?,?,?)", userUuid, service, token, refresh, expiry, username, lastSynced, url)
return err return err
} }

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/rs/cors" "github.com/rs/cors"
@ -21,6 +22,21 @@ type jsonResponse struct {
// List of Reverse proxies // List of Reverse proxies
var ReverseProxies []string var ReverseProxies []string
// RequestRequest - Incoming JSON!
type RequestRequest struct {
URL string `json:"url"`
Token string `json:"token"`
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
}
type RequestResponse struct {
Token string `json:"token,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
func enableCors(w *http.ResponseWriter) { func enableCors(w *http.ResponseWriter) {
(*w).Header().Set("Access-Control-Allow-Origin", "*") (*w).Header().Set("Access-Control-Allow-Origin", "*")
} }
@ -39,8 +55,10 @@ func HandleRequests(port string) {
// JWT Auth - Own profile only (Uses uuid in JWT) // JWT Auth - Own profile only (Uses uuid in JWT)
v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(getUser), lightLimiter)).Methods("GET") v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(getUser), lightLimiter)).Methods("GET")
v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(patchUser), lightLimiter)).Methods("PATCH") v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(patchUser), lightLimiter)).Methods("PATCH")
v1.HandleFunc("/user/navidrome", limitMiddleware(jwtMiddleware(postNavidrome), lightLimiter)).Methods("POST")
v1.HandleFunc("/user/navidrome", limitMiddleware(jwtMiddleware(deleteNavidrome), lightLimiter)).Methods("DELETE")
v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(getSpotifyClientID), lightLimiter)).Methods("GET") v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(getSpotifyClientID), lightLimiter)).Methods("GET")
v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(deleteSpotifyLink), lightLimiter)).Methods("DELETE") v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(deleteSpotify), lightLimiter)).Methods("DELETE")
v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(getScrobbles)).Methods("GET") v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(getScrobbles)).Methods("GET")
// Config auth // Config auth
@ -108,7 +126,7 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
return return
} }
regReq := RegisterRequest{} regReq := RequestRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&regReq) err := decoder.Decode(&regReq)
if err != nil { if err != nil {
@ -128,7 +146,7 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
// handleLogin - Does as it says! // handleLogin - Does as it says!
func handleLogin(w http.ResponseWriter, r *http.Request) { func handleLogin(w http.ResponseWriter, r *http.Request) {
logReq := LoginRequest{} logReq := RequestRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&logReq) err := decoder.Decode(&logReq)
if err != nil { if err != nil {
@ -149,7 +167,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
// handleTokenRefresh - Refresh access token based on refresh token // handleTokenRefresh - Refresh access token based on refresh token
func handleTokenRefresh(w http.ResponseWriter, r *http.Request) { func handleTokenRefresh(w http.ResponseWriter, r *http.Request) {
logReq := LoginResponse{} logReq := RequestRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&logReq) err := decoder.Decode(&logReq)
user, err := isValidRefreshToken(logReq.Token) user, err := isValidRefreshToken(logReq.Token)
@ -165,7 +183,7 @@ func handleTokenRefresh(w http.ResponseWriter, r *http.Request) {
return return
} }
loginResp := LoginResponse{ loginResp := RequestResponse{
Token: token, Token: token,
} }
@ -189,7 +207,7 @@ func handleStats(w http.ResponseWriter, r *http.Request) {
// handleSendReset - Does as it says! // handleSendReset - Does as it says!
func handleSendReset(w http.ResponseWriter, r *http.Request) { func handleSendReset(w http.ResponseWriter, r *http.Request) {
req := RegisterRequest{} req := RequestRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&req) err := decoder.Decode(&req)
if err != nil { if err != nil {
@ -342,10 +360,14 @@ func getUser(w http.ResponseWriter, r *http.Request, claims CustomClaims, reqUse
return return
} }
// oauthNavi, err := getOauthToken(user.UUID, "navidrome")
oauth, err := getOauthToken(user.UUID, "spotify")
if err == nil { if err == nil {
user.SpotifyUsername = oauth.Username user.NavidromeURL = oauthNavi.URL
}
oauthSpotify, err := getOauthToken(user.UUID, "spotify")
if err == nil {
user.SpotifyUsername = oauthSpotify.Username
} }
json, _ := json.Marshal(&user) json, _ := json.Marshal(&user)
@ -631,7 +653,7 @@ func getSpotifyClientID(w http.ResponseWriter, r *http.Request, claims CustomCla
return return
} }
response := LoginResponse{ response := RequestResponse{
Token: key, Token: key,
} }
@ -640,8 +662,8 @@ func getSpotifyClientID(w http.ResponseWriter, r *http.Request, claims CustomCla
w.Write(resp) w.Write(resp)
} }
// deleteSpotifyLink - Unlinks spotify account // deleteSpotify - Unlinks spotify account
func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, claims CustomClaims, v string) { func deleteSpotify(w http.ResponseWriter, r *http.Request, claims CustomClaims, v string) {
jwtUser := claims.Subject jwtUser := claims.Subject
err := removeOauthToken(jwtUser, "spotify") err := removeOauthToken(jwtUser, "spotify")
if err != nil { if err != nil {
@ -653,6 +675,52 @@ func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, claims CustomClai
throwOkMessage(w, "Spotify account successfully unlinked") throwOkMessage(w, "Spotify account successfully unlinked")
} }
// postNavidrome - Submits data for navidrome URL/User/Password
func postNavidrome(w http.ResponseWriter, r *http.Request, claims CustomClaims, v string) {
jwtUser := claims.Subject
request := RequestRequest{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&request)
if err != nil {
throwOkError(w, "Invalid JSON")
return
}
// hash password with salt
salt := generateToken(32)
hash := getMd5(request.Password + salt)
err = validateNavidromeConnection(request.URL, request.Username, hash, salt)
if err != nil {
throwOkError(w, "Failed to validate credentials")
return
}
// Lets set this back 30min
time := time.Now().UTC().Add(-(time.Duration(30) * time.Minute))
err = insertOauthToken(jwtUser, "navidrome", hash, salt, time, request.Username, time, request.URL)
if err != nil {
throwOkError(w, "Failed to save Navidome token")
return
}
throwOkMessage(w, "Successfully saved!")
}
// deleteNavidrome - Unlinks Navidrome account
func deleteNavidrome(w http.ResponseWriter, r *http.Request, claims CustomClaims, v string) {
jwtUser := claims.Subject
err := removeOauthToken(jwtUser, "navidrome")
if err != nil {
fmt.Println(err)
throwOkError(w, "Failed to unlink navidrome account")
return
}
throwOkMessage(w, "Navidrome account successfully unlinked")
}
func getServerInfo(w http.ResponseWriter, r *http.Request) { func getServerInfo(w http.ResponseWriter, r *http.Request) {
cachedRegistrationEnabled := getRedisVal("REGISTRATION_ENABLED") cachedRegistrationEnabled := getRedisVal("REGISTRATION_ENABLED")
if cachedRegistrationEnabled == "" { if cachedRegistrationEnabled == "" {
@ -665,7 +733,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
} }
info := ServerInfo{ info := ServerInfo{
Version: "0.0.25", Version: "0.0.26",
RegistrationEnabled: cachedRegistrationEnabled, RegistrationEnabled: cachedRegistrationEnabled,
} }

View File

@ -22,14 +22,17 @@ func StartBackgroundWorkers() {
return return
case <-hourTicker.C: case <-hourTicker.C:
// Clear old password reset tokens // Clear old password reset tokens
clearOldResetTokens() go clearOldResetTokens()
// Attempt to pull missing images from spotify - hackerino version! // Attempt to pull missing images from spotify - hackerino version!
user, _ := getUserByUsername("idanoo") user, _ := getUserByUsername("idanoo")
user.updateImageDataFromSpotify() go user.updateImageDataFromSpotify()
case <-minuteTicker.C: case <-minuteTicker.C:
// Update playdata from spotify // Update playdata from Spotify
updateSpotifyData() go updateSpotifyData()
// Update playdate from Navidrome
go updateNavidromeData()
} }
} }
}() }()

View File

@ -44,28 +44,11 @@ type UserResponse struct {
SpotifyUsername string `json:"spotify_username"` SpotifyUsername string `json:"spotify_username"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
Token string `json:"token"` Token string `json:"token"`
} NavidromeURL string `json:"navidrome_server"`
// RegisterRequest - Incoming JSON
type RegisterRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
// RegisterRequest - Incoming JSON
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// LoginResponse - JWT issued
type LoginResponse struct {
Token string `json:"token"`
} }
// createUser - Called from API // createUser - Called from API
func createUser(req *RegisterRequest, ip net.IP) error { func createUser(req *RequestRequest, ip net.IP) error {
// Check if user already exists.. // Check if user already exists..
if len(req.Password) < 8 { if len(req.Password) < 8 {
return errors.New("Password must be at least 8 characters") return errors.New("Password must be at least 8 characters")
@ -102,7 +85,7 @@ func createUser(req *RegisterRequest, ip net.IP) error {
return insertUser(req.Username, req.Email, hash, ip) return insertUser(req.Username, req.Email, hash, ip)
} }
func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) { func loginUser(logReq *RequestRequest, ip net.IP) ([]byte, error) {
var resp []byte var resp []byte
var user User var user User
@ -141,7 +124,7 @@ func loginUser(logReq *LoginRequest, ip net.IP) ([]byte, error) {
return resp, errors.New("Error logging in") return resp, errors.New("Error logging in")
} }
loginResp := LoginResponse{ loginResp := RequestResponse{
Token: token, Token: token,
} }
@ -187,7 +170,7 @@ func isValidPassword(password string, user User) bool {
// userAlreadyExists - Returns bool indicating if a record exists for either username or email // userAlreadyExists - Returns bool indicating if a record exists for either username or email
// Using two look ups to make use of DB indexes. // Using two look ups to make use of DB indexes.
func userAlreadyExists(req *RegisterRequest) bool { func userAlreadyExists(req *RequestRequest) bool {
count, err := getDbCount("SELECT COUNT(*) FROM users WHERE username = ?", req.Username) count, err := getDbCount("SELECT COUNT(*) FROM users WHERE username = ?", req.Username)
if err != nil { if err != nil {
fmt.Printf("Error querying for duplicate users: %v", err) fmt.Printf("Error querying for duplicate users: %v", err)
@ -327,6 +310,10 @@ func (user *User) getSpotifyTokens() (OauthToken, error) {
return getOauthToken(user.UUID, "spotify") return getOauthToken(user.UUID, "spotify")
} }
func (user *User) getNavidromeTokens() (OauthToken, error) {
return getOauthToken(user.UUID, "navidrome")
}
func getAllSpotifyUsers() ([]User, error) { func getAllSpotifyUsers() ([]User, error) {
users := make([]User, 0) users := make([]User, 0)
rows, err := db.Query("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `users`.`username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` " + rows, err := db.Query("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `users`.`username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` " +
@ -358,3 +345,35 @@ func getAllSpotifyUsers() ([]User, error) {
return users, nil return users, nil
} }
func getAllNavidromeUsers() ([]User, error) {
users := make([]User, 0)
rows, err := db.Query("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `users`.`username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` " +
"JOIN `oauth_tokens` ON `oauth_tokens`.`user` = `users`.`uuid` AND `oauth_tokens`.`service` = 'navidrome' WHERE `users`.`active` = 1")
if err != nil {
log.Printf("Failed to fetch navidrome users: %+v", err)
return users, errors.New("Failed to fetch configs")
}
defer rows.Close()
for rows.Next() {
var user User
err := rows.Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
if err != nil {
log.Printf("Failed to fetch navidrome user: %+v", err)
return users, errors.New("Failed to fetch users")
}
users = append(users, user)
}
err = rows.Err()
if err != nil {
log.Printf("Failed to fetch navidrome users: %+v", err)
return users, errors.New("Failed to fetch users")
}
return users, nil
}

View File

@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS `oauth_tokens` (
`service` VARCHAR(64) NOT NULL, `service` VARCHAR(64) NOT NULL,
`access_token` VARCHAR(255) NULL DEFAULT '', `access_token` VARCHAR(255) NULL DEFAULT '',
`refresh_token` VARCHAR(255) NULL DEFAULT '', `refresh_token` VARCHAR(255) NULL DEFAULT '',
`url` VARCHAR(255) NULL DEFAULT '',
`expiry` DATETIME NOT NULL, `expiry` DATETIME NOT NULL,
`username` VARCHAR(100) NULL DEFAULT '', `username` VARCHAR(100) NULL DEFAULT '',
`last_synced` DATETIME NOT NULL DEFAULT NOW(), `last_synced` DATETIME NOT NULL DEFAULT NOW(),

View File

@ -281,6 +281,26 @@ export const spotifyDisonnectionRequest = () => {
}); });
} }
export const navidromeConnectionRequest = (values) => {
return axios.post(process.env.REACT_APP_API_URL + "user/navidrome", values, { headers: getHeaders() })
.then((data) => {
toast.success(data.data.message);
return true
}).catch((error) => {
return handleErrorResp(error)
});
};
export const navidromeDisonnectionRequest = () => {
return axios.delete(process.env.REACT_APP_API_URL + "user/navidrome", { headers: getHeaders() })
.then((data) => {
toast.success(data.data.message);
return true
}).catch((error) => {
return handleErrorResp(error)
});
}
export const getServerInfo = () => { export const getServerInfo = () => {
return axios.get(process.env.REACT_APP_API_URL + "serverinfo") return axios.get(process.env.REACT_APP_API_URL + "serverinfo")

View File

@ -3,15 +3,14 @@ import { Link } from 'react-router-dom';
const ScrobbleTable = (props) => { const ScrobbleTable = (props) => {
return ( return (
<div> <div style={{width: `100%`}}>
<table width={900} border={1} cellPadding={5}> <table style={{width: `100%`}} border={1} cellPadding={5}>
<thead> <thead>
<tr> <tr>
<td>Timestamp</td> <td>Timestamp</td>
<td>Track</td> <td>Track</td>
<td>Artist</td> <td>Artist</td>
<td>Album</td> <td>Album</td>
<td>Source</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -30,7 +29,6 @@ const ScrobbleTable = (props) => {
</td> </td>
<td>{element.artist}</td> <td>{element.artist}</td>
<td>{element.album}</td> <td>{element.album}</td>
<td>{element.source}</td>
</tr>; </tr>;
}) })
} }

View File

@ -71,7 +71,7 @@ const Profile = (route) => {
<br/> <br/>
<TopTable type="artist" items={topArtists} /> <TopTable type="artist" items={topArtists} />
<br/> <br/>
Last 10 scrobbles...<br/> Last 10 scrobbles<br/>
<ScrobbleTable data={profile.scrobbles}/> <ScrobbleTable data={profile.scrobbles}/>
</div> </div>
</div> </div>

View File

@ -54,7 +54,7 @@ const Register = () => {
> >
<Form> <Form>
<label> <label>
Username*<br/> Username<br/>
<Field <Field
name="username" name="username"
type="text" type="text"
@ -68,12 +68,13 @@ const Register = () => {
<Field <Field
name="email" name="email"
type="email" type="email"
required={boolTrue}
className="registerFields" className="registerFields"
/> />
</label> </label>
<br/> <br/>
<label> <label>
Password*<br/> Password<br/>
<Field <Field
name="password" name="password"
type="password" type="password"
@ -83,7 +84,7 @@ const Register = () => {
</label> </label>
<br/> <br/>
<label> <label>
Confirm Password*<br/> Confirm Password<br/>
<Field <Field
name="passwordconfirm" name="passwordconfirm"
type="password" type="password"

View File

@ -4,11 +4,18 @@ import './User.css';
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import AuthContext from '../Contexts/AuthContext'; import AuthContext from '../Contexts/AuthContext';
import ScaleLoader from 'react-spinners/ScaleLoader'; import ScaleLoader from 'react-spinners/ScaleLoader';
import { getUser, patchUser } from '../Api/index'
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { Formik, Form, Field } from 'formik';
import { confirmAlert } from 'react-confirm-alert'; import { confirmAlert } from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css'; import 'react-confirm-alert/src/react-confirm-alert.css';
import { spotifyConnectionRequest, spotifyDisonnectionRequest } from '../Api/index' import {
getUser,
patchUser,
spotifyConnectionRequest,
spotifyDisonnectionRequest,
navidromeDisonnectionRequest,
navidromeConnectionRequest,
} from '../Api/index'
import TimezoneSelect from 'react-timezone-select' import TimezoneSelect from 'react-timezone-select'
const User = () => { const User = () => {
@ -25,7 +32,7 @@ const User = () => {
const resetTokenPopup = () => { const resetTokenPopup = () => {
confirmAlert({ confirmAlert({
title: 'Reset token', title: 'Reset token',
message: 'Resetting your token will require you to update your sources with the new token. Continue?', message: 'Resetting your token will require you to update your Jellyfin server / custom scroblers with the new token. Continue?',
buttons: [ buttons: [
{ {
label: 'Reset', label: 'Reset',
@ -38,10 +45,73 @@ const User = () => {
}); });
}; };
const connectNavidromePopup = () => {
confirmAlert({
title: 'Connect Navidrome',
buttons: [
{
label: 'Close',
}
],
childrenElement: () => <Formik
initialValues={{ url: '', username: '', password: '' }}
onSubmit={values => navidromeConnectionRequest(values)}
>
<Form>
<label>
Server URL<br/>
<Field
name="url"
type="text"
/>
</label>
<br/>
<label>
Username<br/>
<Field
name="username"
type="text"
/>
</label>
<br/>
<label>
Password<br/>
<Field
name="password"
type="password"
/>
</label>
<br/><br/>
<Button
color="primary"
type="submit"
className="loginButton"
>Connect</Button>
</Form>
</Formik>,
});
};
const disconnectNavidromePopup = () => {
confirmAlert({
title: 'Disconnect Navidrome',
message: 'Are you sure you want to disconnect your Navidrome connection?',
buttons: [
{
label: 'Disconnect',
onClick: () => navidromeDisonnectionRequest()
},
{
label: 'No',
}
]
});
};
const disconnectSpotifyPopup = () => { const disconnectSpotifyPopup = () => {
confirmAlert({ confirmAlert({
title: 'Disconnect Spotify', title: 'Disconnect Spotify',
message: 'Are you sure you want to disconnect your spotify account?', message: 'Are you sure you want to disconnect your Spotify account?',
buttons: [ buttons: [
{ {
label: 'Disconnect', label: 'Disconnect',
@ -54,6 +124,32 @@ const User = () => {
}); });
}; };
const connectJellyfinPopup = () => {
confirmAlert({
title: 'Connect Jellyfin',
message: 'Install the webhook plugin. Add a webhook to {API_URL}/api/v1/ingress/jellyfin?key='+userdata.token
+'\nSet it to only send "Playback Start" and "Songs/Albums"',
buttons: [
{
label: 'Close',
}
]
});
}
const connectOtherPopup = () => {
confirmAlert({
title: 'Connect Jellyfin',
message: 'Endpoint: {API_URL}/api/v1/ingress/multiscrobbler?key='+userdata.token
+'\nNeed to send JSON body with a string array for artists names, album:string, track:string, playDate:timestamp of scrobble, duration:tracklength in seconds',
buttons: [
{
label: 'Close',
}
]
});
}
const resetToken = () => { const resetToken = () => {
setLoading(true); setLoading(true);
patchUser({ token: '' }) patchUser({ token: '' })
@ -95,32 +191,29 @@ const User = () => {
<h1> <h1>
Welcome {userdata.username} Welcome {userdata.username}
</h1> </h1>
<p className="pageBody"> <div style={{display: `flex`, flexWrap: `wrap`, textAlign: `center`}}>
Timezone<br/> <div style={{width: `300px`, padding: `0 10px 10px 10px`, textAlign: `left`}}>
<TimezoneSelect <h3 style={{textAlign: `center`}}>Profile</h3>
className="userDropdown" Timezone<br/>
value={userdata.timezone} <TimezoneSelect
onChange={updateTimezone} className="userDropdown"
/><br/> value={userdata.timezone}
Token: {userdata.token}<br/> onChange={updateTimezone}
<Button /><br/>
color="primary" Created At:<br/>{userdata.created_at}<br/>
type="button" Email:<br/>{userdata.email}<br/>
className="userButton" Verified: {userdata.verified ? '✓' : '✖'}
onClick={resetTokenPopup} </div>
>Reset Token</Button><br/><br/> <div style={{width: `300px`, padding: `0 10px 10px 10px`}}>
Created At: {userdata.created_at}<br/> <h3>Scrobblers</h3>
Email: {userdata.email}<br/> <br/>
Verified: {userdata.verified ? '✓' : '✖'}<br/> {userdata.spotify_username
? <Button
{userdata.spotify_username
? <div>Spotify Account: {userdata.spotify_username}<br/><br/>
<Button
color="secondary" color="secondary"
type="button" type="button"
className="userButton" className="userButton"
onClick={disconnectSpotifyPopup} onClick={disconnectSpotifyPopup}
>Disconnect Spotify</Button></div> >Disconnect Spotify ({userdata.spotify_username})</Button>
: <div> : <div>
<br/> <br/>
<Button <Button
@ -129,9 +222,55 @@ const User = () => {
className="userButton" className="userButton"
onClick={spotifyConnectionRequest} onClick={spotifyConnectionRequest}
>Connect To Spotify</Button> >Connect To Spotify</Button>
</div> </div>
} }
</p> <br/><br/>
{userdata.navidrome_server
? <Button
color="secondary"
type="button"
className="userButton"
onClick={disconnectNavidromePopup}
>Disconnect Navidrome ({userdata.navidrome_server})</Button>
: <Button
color="primary"
type="button"
className="userButton"
onClick={connectNavidromePopup}
>Connect Navidrome</Button>
}
<br/><br/>
<Button
color="primary"
type="button"
className="userButton"
onClick={connectJellyfinPopup}
>Connect Jellyfin</Button>
<br/><br/>
<Button
color="primary"
type="button"
className="userButton"
onClick={connectOtherPopup}
>Other Scrobblers</Button>
</div>
<div style={{width: `300px`, padding: `0 10px 10px 10px`}}>
<h3>Sad Settings</h3>
<br/>
<Button
color="secondary"
type="button"
className="userButton"
>Delete Account</Button>
<br/><br/>
<Button
color="secondary"
type="button"
className="userButton"
onClick={resetTokenPopup}
>Reset Scrobbler Token</Button>
</div>
</div>
</div> </div>
); );
} }