Version 3 starter

This commit is contained in:
Jamie Curnow
2021-06-14 19:29:35 +10:00
parent 60fc57431a
commit 6205434140
642 changed files with 25817 additions and 32319 deletions

View File

@@ -0,0 +1,13 @@
package middleware
import (
"net/http"
)
// AccessControl sets http headers for responses
func AccessControl(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,64 @@
package middleware
import (
"context"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/config"
"npm/internal/entity/user"
njwt "npm/internal/jwt"
"npm/internal/logger"
"github.com/go-chi/jwtauth"
)
// DecodeAuth ...
func DecodeAuth() func(http.Handler) http.Handler {
privateKey, privateKeyParseErr := njwt.GetPrivateKey()
if privateKeyParseErr != nil && privateKey == nil {
logger.Error("PrivateKeyParseError", privateKeyParseErr)
}
publicKey, publicKeyParseErr := njwt.GetPublicKey()
if publicKeyParseErr != nil && publicKey == nil {
logger.Error("PublicKeyParseError", publicKeyParseErr)
}
tokenAuth := jwtauth.New("RS256", privateKey, publicKey)
return jwtauth.Verifier(tokenAuth)
}
// Enforce is a authentication middleware to enforce access from the
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
// response for any unverified tokens and passes the good ones through.
func Enforce() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if config.IsSetup {
token, claims, err := jwtauth.FromContext(ctx)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil)
return
}
userID := int(claims["uid"].(float64))
_, enabled := user.IsEnabled(userID)
if token == nil || !token.Valid || !enabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
return
}
// Add claims to context
ctx = context.WithValue(ctx, c.UserIDCtxKey, userID)
}
// Token is authenticated, continue as normal
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -0,0 +1,26 @@
package middleware
import (
"context"
"io/ioutil"
"net/http"
c "npm/internal/api/context"
)
// BodyContext simply adds the body data to a context item
func BodyContext() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Grab the Body Data
var body []byte
if r.Body != nil {
body, _ = ioutil.ReadAll(r.Body)
}
// Add it to the context
ctx := r.Context()
ctx = context.WithValue(ctx, c.BodyCtxKey, body)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -0,0 +1,88 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi"
)
var methodMap = []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
http.MethodTrace,
}
func getRouteMethods(routes chi.Router, path string) []string {
var methods []string
tctx := chi.NewRouteContext()
for _, method := range methodMap {
if routes.Match(tctx, method, path) {
methods = append(methods, method)
}
}
return methods
}
var headersAllowedByCORS = []string{
"Authorization",
"Host",
"Content-Type",
"Connection",
"User-Agent",
"Cache-Control",
"Accept-Encoding",
"X-Jumbo-AppKey",
"X-Jumbo-SKey",
"X-Jumbo-SV",
"X-Jumbo-Timestamp",
"X-Jumbo-Version",
"X-Jumbo-Customer-Id",
}
// Cors ...
func Cors(routes chi.Router) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
methods := getRouteMethods(routes, r.URL.Path)
if len(methods) == 0 {
// no route no cors
next.ServeHTTP(w, r)
return
}
methods = append(methods, http.MethodOptions)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
w.Header().Set("Access-Control-Allow-Headers",
strings.Join(headersAllowedByCORS, ","),
)
next.ServeHTTP(w, r)
})
}
}
// Options ...
func Options(routes chi.Router) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
methods := getRouteMethods(routes, r.URL.Path)
if len(methods) == 0 {
// no route shouldn't have options
next.ServeHTTP(w, r)
return
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, "{}")
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,28 @@
package middleware
import (
"fmt"
"net/http"
h "npm/internal/api/http"
"npm/internal/config"
)
// EnforceSetup will error if the config setup doesn't match what is required
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.IsSetup != shouldBeSetup {
state := "during"
if config.IsSetup {
state = "after"
}
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
return
}
// All good
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,114 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/model"
"npm/internal/util"
"strings"
"github.com/qri-io/jsonschema"
)
// Filters will accept a pre-defined schemaData to validate against the GET query params
// passed in to this endpoint. This will ensure that the filters are not injecting SQL.
// After we have determined what the Filters are to be, they are saved on the Context
// to be used later in other endpoints.
func Filters(schemaData string) func(http.Handler) http.Handler {
reservedFilterKeys := []string{
"limit",
"offset",
"sort",
"order",
"t", // This is used as a timestamp paramater in some clients and can be ignored
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var filters []model.Filter
for key, val := range r.URL.Query() {
key = strings.ToLower(key)
// Split out the modifier from the field name and set a default modifier
var keyParts []string
keyParts = strings.Split(key, ":")
if len(keyParts) == 1 {
// Default modifier
keyParts = append(keyParts, "equals")
}
// Only use this filter if it's not a reserved get param
if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) {
for _, valItem := range val {
// Check that the val isn't empty
if len(strings.TrimSpace(valItem)) > 0 {
valSlice := []string{valItem}
if keyParts[1] == "in" || keyParts[1] == "notin" {
valSlice = strings.Split(valItem, ",")
}
filters = append(filters, model.Filter{
Field: keyParts[0],
Modifier: keyParts[1],
Value: valSlice,
})
}
}
}
}
// Only validate schema if there are filters to validate
if len(filters) > 0 {
ctx := r.Context()
// Marshal the Filters in to a JSON string so that the Schema Validation works against it
filterData, marshalErr := json.MarshalIndent(filters, "", " ")
if marshalErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil)
return
}
// Create root schema
rs := &jsonschema.Schema{}
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
return
}
// Validate it
errors, jsonError := rs.ValidateBytes(ctx, filterData)
if jsonError != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil)
return
}
if len(errors) > 0 {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors)
return
}
ctx = context.WithValue(ctx, c.FiltersCtxKey, filters)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}
}
// GetFiltersFromContext returns the Filters
func GetFiltersFromContext(r *http.Request) []model.Filter {
filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter)
if !ok {
// the assertion failed
var emptyFilters []model.Filter
return emptyFilters
}
return filters
}

View File

@@ -0,0 +1,23 @@
package middleware
import (
"context"
"net/http"
c "npm/internal/api/context"
)
// PrettyPrint will determine whether the request should be pretty printed in output
// with ?pretty=1 or ?pretty=true
func PrettyPrint(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prettyStr := r.URL.Query().Get("pretty")
if prettyStr == "1" || prettyStr == "true" {
ctx := r.Context()
ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}

View File

@@ -0,0 +1,55 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"github.com/qri-io/jsonschema"
)
// CheckRequestSchema ...
func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) {
// Create root schema
rs := &jsonschema.Schema{}
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
return nil, fmt.Errorf("Schema Fatal: %v", err)
}
// Validate it
schemaErrors, jsonError := rs.ValidateBytes(ctx, payload)
if jsonError != nil {
return nil, jsonError
}
return schemaErrors, nil
}
// EnforceRequestSchema accepts a schema and validates the request body against it
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get content from context
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
return
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return
}
// All good
next.ServeHTTP(w, r)
})
}
}