mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-22 00:21:55 +00:00
0.0.26
- Make email required - Add basic navidrome/subsonic connection - Tidy up request/response structure in backend - Tidy Settings page
This commit is contained in:
parent
8294791abe
commit
48a99b31fd
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
60
internal/goscrobble/ingress_navidrome.go
Normal file
60
internal/goscrobble/ingress_navidrome.go
Normal 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")
|
||||||
|
}
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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(®Req)
|
err := decoder.Decode(®Req)
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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")
|
||||||
|
@ -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>;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user