Basic API structure

This commit is contained in:
Daniel Mason 2021-03-25 18:15:01 +13:00
parent 28d8d3491a
commit 529ac7ab84
9 changed files with 118 additions and 16 deletions

2
.gitignore vendored
View File

@ -5,7 +5,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
.env
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View File

@ -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. 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 ## Setup MySQL
create user 'goscrobble'@'%' identified by 'supersecurepass'; 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'@'%'; grant all privileges on goscrobble.* to 'goscrobble'@'%';
## Local build/run ## Local build/run
cp .env.example .env # Fill in the blanks
cd web && npm install && npm start cd web && npm install && npm start
# In another terminal # In another terminal
go mod tidy 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 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 ## Prod deployment
We need to build NPM package, and then ship web/build with the binary. 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 cd web npm install --production && npm run build
go build -o goscrobble cmd/go-scrobble/*.go go build -o goscrobble cmd/go-scrobble/*.go
MYSQL_HOST=127.0.0.1 MYSQL_USER=goscrobble MYSQL_PASS=supersecurepass MYSQL_DB=goscrobble ./goscrobble ./goscrobble

View File

@ -1,10 +1,22 @@
package main package main
import ( import (
"log"
"os"
"git.m2.nz/go-scrobble/internal/goscrobble" "git.m2.nz/go-scrobble/internal/goscrobble"
"github.com/joho/godotenv"
) )
func main() { 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 // // Boot up DB connection for life of application
goscrobble.InitDb() goscrobble.InitDb()
defer goscrobble.CloseDbConn() defer goscrobble.CloseDbConn()

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.16
require ( require (
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect
github.com/containerd/containerd v1.4.1 // 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/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+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 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-migrate/migrate v3.5.4+incompatible
github.com/golang/protobuf v1.4.3 // indirect github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/mux v1.8.0 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/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect

4
go.sum
View File

@ -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 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 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/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 h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 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= 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/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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/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/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= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=

View File

@ -5,10 +5,6 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=www-data 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 ExecStart=/opt/goscrobble/goscrobble
Restart=on-failure Restart=on-failure

View File

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

View File

@ -2,6 +2,7 @@ package goscrobble
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -17,24 +18,29 @@ type spaHandler struct {
indexPath string indexPath string
} }
type jsonResponse struct {
Err string `json:"error"`
}
// HandleRequests - Boot HTTP! // HandleRequests - Boot HTTP!
func HandleRequests() { func HandleRequests() {
// Create a new router // Create a new router
r := mux.NewRouter().StrictSlash(true) r := mux.NewRouter().StrictSlash(true)
v1 := r.PathPrefix("/api/v1").Subrouter() v1 := r.PathPrefix("/api/v1").Subrouter()
// STATIC TOKEN AUTH
// httpRouter.HandleFunc("/api/v1/ingress/jellyfin", serveEndpoint)
// JWT SESSION AUTH? // Static Token for /ingress
// httpRouter.HandleFunc("/api/v1/profile/{id}", serveEndpoint) 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("/register", serveEndpoint).Methods("POST")
v1.HandleFunc("/login", serveEndpoint).Methods("POST") v1.HandleFunc("/login", serveEndpoint).Methods("POST")
v1.HandleFunc("/logout", 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") r.PathPrefix("/api")
// SERVE FRONTEND - NO AUTH // SERVE FRONTEND - NO AUTH
@ -45,6 +51,39 @@ func HandleRequests() {
log.Fatal(http.ListenAndServe(":42069", r)) 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) { func serveEndpoint(w http.ResponseWriter, r *http.Request) {
var jsonInput map[string]interface{} var jsonInput map[string]interface{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
@ -59,6 +98,8 @@ func serveEndpoint(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{}") fmt.Fprintf(w, "{}")
} }
// FRONTEND HANDLING
// ServerHTTP - Frontend server // ServerHTTP - Frontend server
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the absolute path to prevent directory traversal // Get the absolute path to prevent directory traversal

View File

@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "GoScrobble",
"name": "Create React App Sample", "name": "GoScrobble - Go based scrobbler",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",