- Fix mobile menu auto collapse on select
- Add /u/ route for public user profiles (Added private flag to db - to implement later)
- Add /user route for your own profile / edit profile
- Added handling for if API is offline/incorrect
- Add index.html loading spinner while react bundle downloads
- Change HashRouter to BrowserRouter
- Added sources column to scrobbles
This commit is contained in:
Daniel Mason 2021-04-01 23:17:46 +13:00
parent af02bd99cc
commit e570314ac2
Signed by: idanoo
GPG key ID: 387387CDBC02F132
27 changed files with 435 additions and 89 deletions

View file

@ -0,0 +1,4 @@
package goscrobble
func getImageLastFM(src string) {
}

View file

@ -50,7 +50,7 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
}
// Insert album if not exist
err = insertScrobble(userUUID, track.Uuid, ip, tx)
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
if err != nil {
log.Printf("%+v", err)
return errors.New("Failed to map track")

View file

@ -0,0 +1,21 @@
package goscrobble
type ProfileResponse struct {
UUID string `json:"uuid"`
Username string `json:"username"`
Scrobbles []ScrobbleRequestItem `json:"scrobbles"`
}
func getProfile(user User) (ProfileResponse, error) {
resp := ProfileResponse{
UUID: user.UUID,
Username: user.Username,
}
scrobbleReq, err := fetchScrobblesForUser(user.UUID, 10, 1)
if err != nil {
return resp, err
}
resp.Scrobbles = scrobbleReq.Items
return resp, nil
}

View file

@ -36,8 +36,8 @@ type ScrobbleRequestItem struct {
}
// insertScrobble - This will return if it exists or create it based on MBID > Name
func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
err := insertNewScrobble(user, track, ip, tx)
func insertScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
err := insertNewScrobble(user, track, source, ip, tx)
if err != nil {
log.Printf("Error inserting scrobble %s %+v", user, err)
return errors.New("Failed to insert scrobble!")
@ -46,7 +46,7 @@ func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
return nil
}
func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
scrobbleReq := ScrobbleRequest{}
var count int
@ -76,8 +76,8 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
"JOIN albums ON track_album.album = albums.uuid "+
"JOIN users ON scrobbles.user = users.uuid "+
"WHERE user = UUID_TO_BIN(?, true) "+
"ORDER BY scrobbles.created_at DESC LIMIT 500",
userUuid)
"ORDER BY scrobbles.created_at DESC LIMIT ?",
userUuid, limit)
if err != nil {
log.Printf("Failed to fetch scrobbles: %+v", err)
return scrobbleReq, errors.New("Failed to fetch scrobbles")
@ -108,9 +108,9 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
return scrobbleReq, nil
}
func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`) "+
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true))", ip, user, track)
func insertNewScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`, `source`) "+
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true), ?)", ip, user, track, source)
return err
}

View file

@ -25,11 +25,14 @@ type jsonResponse struct {
Msg string `json:"message,omitempty"`
}
// Limits to 1 req / 10 sec
// Limits to 1 req / 4 sec
var heavyLimiter = NewIPRateLimiter(0.25, 2)
// Limits to 5 req / sec
var standardLimiter = NewIPRateLimiter(1, 5)
var standardLimiter = NewIPRateLimiter(5, 5)
// Limits to 10 req / sec
var lightLimiter = NewIPRateLimiter(10, 10)
// List of Reverse proxies
var ReverseProxies []string
@ -48,17 +51,21 @@ func HandleRequests(port string) {
// Static Token for /ingress
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
// JWT Auth
v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
// JWT Auth - PWN PROFILE ONLY.
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
// v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH")
v1.HandleFunc("/user/{uuid}/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("/stats", handleStats).Methods("GET")
v1.HandleFunc("/profile/{username}", limitMiddleware(fetchProfile, lightLimiter)).Methods("GET")
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
v1.HandleFunc("/stats", handleStats).Methods("GET")
// This just prevents it serving frontend stuff over /api
r.PathPrefix("/api")
@ -80,7 +87,7 @@ func HandleRequests(port string) {
log.Fatal(http.ListenAndServe(":"+port, handler))
}
// MIDDLEWARE
// MIDDLEWARE RESPONSES
// throwUnauthorized - Throws a 403
func throwUnauthorized(w http.ResponseWriter, m string) {
jr := jsonResponse{
@ -149,6 +156,7 @@ func generateJsonError(m string) []byte {
return js
}
// MIDDLEWARE ACTIONS
// tokenMiddleware - Validates token to a user
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@ -182,15 +190,11 @@ func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)
var reqUuid string
for k, v := range mux.Vars(r) {
if k == "id" {
if k == "uuid" {
reqUuid = v
}
}
if reqUuid == "" {
throwBadReq(w, "Invalid Request")
}
next(w, r, claims.Subject, reqUuid)
}
}
@ -311,13 +315,13 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
if err != nil {
// log.Printf("Error inserting track: %+v", err)
tx.Rollback()
throwBadReq(w, err.Error())
throwOkError(w, err.Error())
return
}
err = tx.Commit()
if err != nil {
throwBadReq(w, err.Error())
throwOkError(w, err.Error())
return
}
@ -328,11 +332,35 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
throwBadReq(w, "Unknown ingress type")
}
// fetchUser - Return personal userprofile
func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
// We don't this var most of the time
userFull, err := getUser(jwtUser)
if err != nil {
throwOkError(w, "Failed to fetch user information")
return
}
jsonFull, _ := json.Marshal(&userFull)
// Lets strip out vars we don't want to send.
user := UserResponse{}
err = json.Unmarshal(jsonFull, &user)
if err != nil {
throwOkError(w, "Failed to fetch user information")
return
}
json, _ := json.Marshal(&user)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// fetchScrobbles - Return an array of scrobbles
func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
resp, err := fetchScrobblesForUser(reqUser, 1)
resp, err := fetchScrobblesForUser(reqUser, 100, 1)
if err != nil {
throwBadReq(w, "Failed to fetch scrobbles")
throwOkError(w, "Failed to fetch scrobbles")
return
}
@ -374,6 +402,37 @@ func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) {
throwOkMessage(w, "Config updated successfully")
}
// fetchProfile - Returns public user profile data
func fetchProfile(w http.ResponseWriter, r *http.Request) {
var username string
for k, v := range mux.Vars(r) {
if k == "username" {
username = v
}
}
if username == "" {
throwOkError(w, "Invalid Username")
return
}
user, err := getUserByUsername(username)
if err != nil {
throwOkError(w, err.Error())
return
}
resp, err := getProfile(user)
if err != nil {
throwOkError(w, err.Error())
return
}
json, _ := json.Marshal(&resp)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// FRONTEND HANDLING
// ServerHTTP - Frontend server

View file

@ -3,11 +3,14 @@ package goscrobble
import (
"errors"
"math/rand"
"time"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// generateToken - Generates a unique token for user input
func generateToken(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]

View file

@ -29,6 +29,17 @@ type User struct {
Admin bool `json:"admin"`
}
type UserResponse struct {
UUID string `json:"uuid"`
CreatedAt time.Time `json:"created_at"`
CreatedIp net.IP `json:"created_ip"`
ModifiedAt time.Time `json:"modified_at"`
ModifiedIP net.IP `jsos:"modified_ip"`
Username string `json:"username"`
Email string `json:"email"`
Verified bool `json:"verified"`
}
// RegisterRequest - Incoming JSON
type RegisterRequest struct {
Username string `json:"username"`
@ -205,3 +216,15 @@ func getUser(uuid string) (User, error) {
return user, nil
}
func getUserByUsername(username 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 `username` = ? AND `active` = 1",
username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
if err == sql.ErrNoRows {
return user, errors.New("Invalid Username")
}
return user, nil
}