- Added Admin/Site config page in frontend for admin users
- Added API POST/GET /config endpoint
This commit is contained in:
Daniel Mason 2021-03-31 21:40:20 +13:00
parent 8be4a190a6
commit af02bd99cc
Signed by: idanoo
GPG key ID: 387387CDBC02F132
17 changed files with 826 additions and 24 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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 == "" {

View file

@ -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
}