diff --git a/.gitignore b/.gitignore index f4d432a8..fc1cf7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ *.dll *.so *.dylib - +.env # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index 789ea060..ef38f214 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Currently building on Node V15.X & Go V1.16.X With a prebuilt binary - you will still need the migrations folder + web/build folder on prod. +Copy .env.example to .env and set variables. You can use https://www.grc.com/passwords.htm to generate a JWT_SECRET. + ## Setup MySQL create user 'goscrobble'@'%' identified by 'supersecurepass'; @@ -13,10 +15,11 @@ With a prebuilt binary - you will still need the migrations folder + web/build f grant all privileges on goscrobble.* to 'goscrobble'@'%'; ## Local build/run + cp .env.example .env # Fill in the blanks cd web && npm install && npm start # In another terminal go mod tidy - CGO_ENABLED=0 MYSQL_HOST=127.0.0.1 MYSQL_USER=goscrobble MYSQL_PASS=supersecurepass MYSQL_DB=goscrobble go run cmd/go-scrobble/*.go + CGO_ENABLED=0 go run cmd/go-scrobble/*.go Access dev frontend @ http://127.0.0.1:3000 + API @ http://127.0.0.1:42069/api/v1 @@ -25,7 +28,7 @@ Access dev frontend @ http://127.0.0.1:3000 + API @ http://127.0.0.1:42069/api/v ## Prod deployment We need to build NPM package, and then ship web/build with the binary. - + cp .env.example .env # Fill in the blanks cd web npm install --production && npm run build go build -o goscrobble cmd/go-scrobble/*.go - MYSQL_HOST=127.0.0.1 MYSQL_USER=goscrobble MYSQL_PASS=supersecurepass MYSQL_DB=goscrobble ./goscrobble \ No newline at end of file + ./goscrobble \ No newline at end of file diff --git a/cmd/go-scrobble/main.go b/cmd/go-scrobble/main.go index b0ffb496..906fcaba 100644 --- a/cmd/go-scrobble/main.go +++ b/cmd/go-scrobble/main.go @@ -1,10 +1,22 @@ package main import ( + "log" + "os" + "git.m2.nz/go-scrobble/internal/goscrobble" + "github.com/joho/godotenv" ) func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + // Store JWT secret + goscrobble.JwtToken = []byte(os.Getenv("JWT_SECRET")) + // // Boot up DB connection for life of application goscrobble.InitDb() defer goscrobble.CloseDbConn() diff --git a/go.mod b/go.mod index 3adfcab2..92df90fe 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect github.com/containerd/containerd v1.4.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -14,6 +15,7 @@ require ( github.com/golang-migrate/migrate v3.5.4+incompatible github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/mux v1.8.0 + github.com/joho/godotenv v1.3.0 github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 9164e7a2..01c75fab 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= @@ -47,6 +49,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/init/goscrobble.service b/init/goscrobble.service index 63f84f77..b6ba98ad 100644 --- a/init/goscrobble.service +++ b/init/goscrobble.service @@ -5,10 +5,6 @@ After=network.target [Service] Type=simple User=www-data -Environment="MYSQL_HOST=127.0.0.1" -Environment="MYSQL_USER=goscrobble" -Environment="MYSQL_PASS=supersecurepass" -Environment="MYSQL_DB=goscrobble" ExecStart=/opt/goscrobble/goscrobble Restart=on-failure diff --git a/internal/goscrobble/jwt.go b/internal/goscrobble/jwt.go new file mode 100644 index 00000000..1f58158f --- /dev/null +++ b/internal/goscrobble/jwt.go @@ -0,0 +1,44 @@ +package goscrobble + +import ( + "log" + "net/http" + + "github.com/dgrijalva/jwt-go" +) + +// JwtToken - Store token from .env +var JwtToken []byte + +// Store custom claims here +type Claims struct { + UUID string `json:"uuid"` + jwt.StandardClaims +} + +// verifyToken - Verifies the JWT is valid +func verifyToken(token string, w http.ResponseWriter) bool { + // Initialize a new instance of `Claims` + claims := &Claims{} + + tkn, err := jwt.ParseWithClaims(token, claims, func(JwtToken *jwt.Token) (interface{}, error) { + return JwtToken, nil + }) + + if err != nil { + log.Printf("%v", err) + if err == jwt.ErrSignatureInvalid { + w.WriteHeader(http.StatusUnauthorized) + return false + } + + w.WriteHeader(http.StatusBadRequest) + return false + } + if !tkn.Valid { + w.WriteHeader(http.StatusUnauthorized) + return false + } + + return true +} diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index fdd06c7a..ec7d5e57 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -2,6 +2,7 @@ package goscrobble import ( "encoding/json" + "errors" "fmt" "log" "net/http" @@ -17,24 +18,29 @@ type spaHandler struct { indexPath string } +type jsonResponse struct { + Err string `json:"error"` +} + // HandleRequests - Boot HTTP! func HandleRequests() { // Create a new router r := mux.NewRouter().StrictSlash(true) v1 := r.PathPrefix("/api/v1").Subrouter() - // STATIC TOKEN AUTH - // httpRouter.HandleFunc("/api/v1/ingress/jellyfin", serveEndpoint) - // JWT SESSION AUTH? - // httpRouter.HandleFunc("/api/v1/profile/{id}", serveEndpoint) + // Static Token for /ingress + v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(serveEndpoint)) - // NO AUTH + // JWT Auth + v1.HandleFunc("/profile/{id}", jwtMiddleware(serveEndpoint)) + + // No Auth v1.HandleFunc("/register", serveEndpoint).Methods("POST") v1.HandleFunc("/login", serveEndpoint).Methods("POST") v1.HandleFunc("/logout", serveEndpoint).Methods("POST") - // This just prevents it serving frontend over /api + // This just prevents it serving frontend stuff over /api r.PathPrefix("/api") // SERVE FRONTEND - NO AUTH @@ -45,6 +51,39 @@ func HandleRequests() { log.Fatal(http.ListenAndServe(":42069", r)) } +// MIDDLEWARE + +// tokenMiddleware - Validates token to a user +func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + jr := jsonResponse{ + Err: "Invalid API Token", + } + js, _ := json.Marshal(&jr) + err := errors.New(string(js)) + http.Error(res, err.Error(), http.StatusUnauthorized) + return + // next(res, req) + } +} + +// jwtMiddleware - Validates middleware to a user +func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + jr := jsonResponse{ + Err: "Invalid JWT Token", + } + js, _ := json.Marshal(&jr) + err := errors.New(string(js)) + http.Error(res, err.Error(), http.StatusUnauthorized) + return + // next(res, req) + } +} + +// ENDPOINT HANDLING + +// serveEndpoint - API stuffs func serveEndpoint(w http.ResponseWriter, r *http.Request) { var jsonInput map[string]interface{} decoder := json.NewDecoder(r.Body) @@ -59,6 +98,8 @@ func serveEndpoint(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "{}") } +// FRONTEND HANDLING + // ServerHTTP - Frontend server func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get the absolute path to prevent directory traversal diff --git a/web/public/manifest.json b/web/public/manifest.json index 080d6c77..b58b9428 100644 --- a/web/public/manifest.json +++ b/web/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "GoScrobble", + "name": "GoScrobble - Go based scrobbler", "icons": [ { "src": "favicon.ico",