3
0

upd(system):

- update apis for login and logout,
- make OIDC routes and setup optional,
- refactor entrypoint to open up http testing
This commit is contained in:
Tit Petric
2018-11-26 17:01:39 +01:00
parent 1c9b1d2d7f
commit 6ab0c9bf09
16 changed files with 318 additions and 270 deletions
+24 -31
View File
@@ -13,11 +13,33 @@
"path": "/check",
"parameters": {}
},
{
"name": "login",
"method": "POST",
"title": "Login user",
"path": "/login",
"parameters": {
"post": [
{
"name": "username",
"type": "string",
"required": true,
"title": "Username"
},
{
"name": "password",
"type": "string",
"required": true,
"title": "Password"
}
]
}
},
{
"name": "logout",
"method": "DELETE",
"method": "GET",
"title": "Delete JWT token (Sign Out)",
"path": "/check",
"path": "/logout",
"parameters": {}
}
]
@@ -381,35 +403,6 @@
}
],
"apis": [
{
"name": "login",
"method": "POST",
"title": "Login user",
"path": "/login",
"parameters": {
"post": [
{
"name": "username",
"type": "string",
"required": true,
"title": "Username"
},
{
"name": "password",
"type": "string",
"required": true,
"title": "Password"
}
]
}
},
{
"name": "logout",
"method": "GET",
"title": "Delete JWT token (Sign Out)",
"path": "/logout",
"parameters": {}
},
{
"name": "list",
"method": "GET",
+24 -2
View File
@@ -15,11 +15,33 @@
"Path": "/check",
"Parameters": {}
},
{
"Name": "login",
"Method": "POST",
"Title": "Login user",
"Path": "/login",
"Parameters": {
"post": [
{
"name": "username",
"required": true,
"title": "Username",
"type": "string"
},
{
"name": "password",
"required": true,
"title": "Password",
"type": "string"
}
]
}
},
{
"Name": "logout",
"Method": "DELETE",
"Method": "GET",
"Title": "Delete JWT token (Sign Out)",
"Path": "/check",
"Path": "/logout",
"Parameters": {}
}
]
-29
View File
@@ -17,35 +17,6 @@
],
"Path": "/users",
"APIs": [
{
"Name": "login",
"Method": "POST",
"Title": "Login user",
"Path": "/login",
"Parameters": {
"post": [
{
"name": "username",
"required": true,
"title": "Username",
"type": "string"
},
{
"name": "password",
"required": true,
"title": "Password",
"type": "string"
}
]
}
},
{
"Name": "logout",
"Method": "GET",
"Title": "Delete JWT token (Sign Out)",
"Path": "/logout",
"Parameters": {}
},
{
"Name": "list",
"Method": "GET",
+16 -29
View File
@@ -13,13 +13,28 @@
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## Login user
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/auth/login` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| username | string | POST | Username | N/A | YES |
| password | string | POST | Password | N/A | YES |
## Delete JWT token (Sign Out)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/auth/check` | HTTP/S | DELETE | |
| `/auth/logout` | HTTP/S | GET | |
#### Request parameters
@@ -277,34 +292,6 @@ An organisation may have many teams. Teams may have many channels available. Acc
# Users
## Login user
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/users/login` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| username | string | POST | Username | N/A | YES |
| password | string | POST | Password | N/A | YES |
## Delete JWT token (Sign Out)
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/users/logout` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## Search users (Directory)
#### Method
+36 -5
View File
@@ -19,7 +19,7 @@ var _ = errors.Wrap
type (
Auth struct {
jwt jwtEncodeCookieSetter
jwt auth.TokenEncoder
}
checkResponse struct {
@@ -36,6 +36,10 @@ func (ctrl *Auth) Check(ctx context.Context, r *request.AuthCheck) (interface{},
return nil, errors.New("Not implemented: Auth.check")
}
func (ctrl *Auth) Login(ctx context.Context, r *request.AuthLogin) (interface{}, error) {
return nil, errors.New("Not implemented: Auth.login")
}
func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{}, error) {
return nil, errors.New("Not implemented: Auth.logout")
}
@@ -43,7 +47,7 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{
// Handlers() func ignores "std" crust controllers
//
// Crush handlers are too abstract for our auth needs so we need (direct access to htt.ResponseWriter)
func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth {
h := handlers.NewAuth(ctrl)
// Check JWT if valid
h.Check = func(w http.ResponseWriter, r *http.Request) {
@@ -52,7 +56,7 @@ func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
if identity := auth.GetIdentityFromContext(ctx); identity != nil && identity.Valid() {
if user, err := service.DefaultUser.With(ctx).FindByID(identity.Identity()); err == nil {
jwtAuth.SetCookie(w, r, user)
jwtEncoder.SetCookie(w, r, user)
resputil.JSON(w, checkResponse{
JWT: c.Value,
@@ -64,14 +68,41 @@ func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
}
// Did not send response, assuming invalid cookie
jwtAuth.SetCookie(w, r, nil)
jwtEncoder.SetCookie(w, r, nil)
} else {
resputil.JSON(w, err)
}
}
h.Logout = func(w http.ResponseWriter, r *http.Request) {
jwtAuth.SetCookie(w, r, nil)
jwtEncoder.SetCookie(w, r, nil)
}
h.Login = func(w http.ResponseWriter, r *http.Request) {
params := request.NewAuthLogin()
ctx := r.Context()
userSvc := service.User().With(ctx)
// check email and username for login
user, err := userSvc.FindByEmail(params.Username)
if err != nil {
user, err = userSvc.FindByUsername(params.Username)
}
// can't find user
if err != nil {
resputil.JSON(w, err)
return
}
// validate password
if user.ValidatePassword(params.Password) {
jwtEncoder.SetCookie(w, r, user)
resputil.JSON(w, user, err)
return
}
resputil.JSON(w, errors.New("Password doesn't match"))
}
return h
}
+11 -1
View File
@@ -28,12 +28,14 @@ import (
// Internal API interface
type AuthAPI interface {
Check(context.Context, *request.AuthCheck) (interface{}, error)
Login(context.Context, *request.AuthLogin) (interface{}, error)
Logout(context.Context, *request.AuthLogout) (interface{}, error)
}
// HTTP API interface
type Auth struct {
Check func(http.ResponseWriter, *http.Request)
Login func(http.ResponseWriter, *http.Request)
Logout func(http.ResponseWriter, *http.Request)
}
@@ -46,6 +48,13 @@ func NewAuth(ah AuthAPI) *Auth {
return ah.Check(r.Context(), params)
})
},
Login: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAuthLogin()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Login(r.Context(), params)
})
},
Logout: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAuthLogout()
@@ -61,7 +70,8 @@ func (ah *Auth) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
r.Use(middlewares...)
r.Route("/auth", func(r chi.Router) {
r.Get("/check", ah.Check)
r.Delete("/check", ah.Logout)
r.Post("/login", ah.Login)
r.Get("/logout", ah.Logout)
})
})
}
-20
View File
@@ -27,8 +27,6 @@ import (
// Internal API interface
type UserAPI interface {
Login(context.Context, *request.UserLogin) (interface{}, error)
Logout(context.Context, *request.UserLogout) (interface{}, error)
List(context.Context, *request.UserList) (interface{}, error)
Create(context.Context, *request.UserCreate) (interface{}, error)
Edit(context.Context, *request.UserEdit) (interface{}, error)
@@ -40,8 +38,6 @@ type UserAPI interface {
// HTTP API interface
type User struct {
Login func(http.ResponseWriter, *http.Request)
Logout func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
@@ -53,20 +49,6 @@ type User struct {
func NewUser(uh UserAPI) *User {
return &User{
Login: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewUserLogin()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return uh.Login(r.Context(), params)
})
},
Logout: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewUserLogout()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return uh.Logout(r.Context(), params)
})
},
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewUserList()
@@ -123,8 +105,6 @@ func (uh *User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/users", func(r chi.Router) {
r.Post("/login", uh.Login)
r.Get("/logout", uh.Logout)
r.Get("/", uh.List)
r.Post("/", uh.Create)
r.Put("/{userID}", uh.Edit)
+2 -7
View File
@@ -32,7 +32,7 @@ type (
userService service.UserService
jwt jwtEncodeCookieSetter
jwt auth.TokenEncoder
}
oidcProfile struct {
@@ -40,11 +40,6 @@ type (
Name string `json:"name"`
Sub string `json:"sub"`
}
jwtEncodeCookieSetter interface {
auth.TokenEncoder
SetCookie(w http.ResponseWriter, r *http.Request, identity auth.Identifiable)
}
)
const openIdConnectStateCookie = "oidc-state"
@@ -52,7 +47,7 @@ const openIdConnectStateCookie = "oidc-state"
// Sets-up OIDC connection (issuer discovery, client registration)
//
// Client registration is done when no cfg.ClientID is provided.
func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt jwtEncodeCookieSetter, settings repository.Settings) (c *openIdConnect, err error) {
func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt auth.TokenEncoder, settings repository.Settings) (c *openIdConnect, err error) {
c = &openIdConnect{
appURL: cfg.AppURL,
stateCookieExpiry: cfg.StateCookieExpiry,
+51
View File
@@ -70,6 +70,57 @@ func (a *AuthCheck) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewAuthCheck()
// Auth login request parameters
type AuthLogin struct {
Username string
Password string
}
func NewAuthLogin() *AuthLogin {
return &AuthLogin{}
}
func (a *AuthLogin) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(a)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["username"]; ok {
a.Username = val
}
if val, ok := post["password"]; ok {
a.Password = val
}
return err
}
var _ RequestFiller = NewAuthLogin()
// Auth logout request parameters
type AuthLogout struct {
}
-91
View File
@@ -30,97 +30,6 @@ var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// User login request parameters
type UserLogin struct {
Username string
Password string
}
func NewUserLogin() *UserLogin {
return &UserLogin{}
}
func (u *UserLogin) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(u)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["username"]; ok {
u.Username = val
}
if val, ok := post["password"]; ok {
u.Password = val
}
return err
}
var _ RequestFiller = NewUserLogin()
// User logout request parameters
type UserLogout struct {
}
func NewUserLogout() *UserLogout {
return &UserLogout{}
}
func (u *UserLogout) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(u)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
return err
}
var _ RequestFiller = NewUserLogout()
// User list request parameters
type UserList struct {
Query string
+12 -6
View File
@@ -13,18 +13,24 @@ import (
"github.com/crusttech/crust/system/service"
)
func MountRoutes(oidcConfig *config.OIDC, jwtAuth jwtEncodeCookieSetter) func(chi.Router) {
func MountRoutes(oidcConfig *config.OIDC, jwtEncoder auth.TokenEncoder) func(chi.Router) {
var err error
var userSvc = service.User()
var ctx = context.Background()
var oidc *openIdConnect
oidc, err := OpenIdConnect(ctx, oidcConfig, userSvc, jwtAuth, repository.NewSettings(ctx, repository.DB(ctx)))
if err != nil {
log.Print("Could not initialize OIDC:", err.Error())
if (oidcConfig.Enabled) {
oidc, err = OpenIdConnect(ctx, oidcConfig, userSvc, jwtEncoder, repository.NewSettings(ctx, repository.DB(ctx)))
if err != nil {
log.Println("Could not initialize OIDC:", err.Error())
}
} else {
log.Println("OIDC is disabled")
}
// Initialize handers & controllers.
return func(r chi.Router) {
if oidc != nil {
if oidcConfig.Enabled && oidc != nil {
r.Route("/oidc", func(r chi.Router) {
r.Get("/", oidc.HandleRedirect)
r.Get("/callback", oidc.HandleOAuth2Callback)
@@ -32,7 +38,7 @@ func MountRoutes(oidcConfig *config.OIDC, jwtAuth jwtEncodeCookieSetter) func(ch
}
// Provide raw `/auth` handlers
Auth{}.New().Handlers(jwtAuth).MountRoutes(r)
Auth{}.New().Handlers(jwtEncoder).MountRoutes(r)
// Protect all _private_ routes
r.Group(func(r chi.Router) {
-8
View File
@@ -29,14 +29,6 @@ func (ctrl *User) List(ctx context.Context, r *request.UserList) (interface{}, e
return ctrl.user.With(ctx).Find(&types.UserFilter{Query: r.Query})
}
func (ctrl *User) Login(ctx context.Context, r *request.UserLogin) (interface{}, error) {
return nil, errors.New("Not implemented: User.Login")
}
func (ctrl *User) Logout(ctx context.Context, r *request.UserLogout) (interface{}, error) {
return nil, errors.New("Not implemented: User.Logout")
}
func (ctrl *User) Create(ctx context.Context, r *request.UserCreate) (interface{}, error) {
user := &types.User{
Email: r.Email,
+30
View File
@@ -5,15 +5,34 @@ import (
"reflect"
"runtime"
"net/http"
"github.com/99designs/basicauth-go"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/metrics"
"github.com/crusttech/crust/internal/version"
"github.com/crusttech/crust/system/rest"
)
func Routes() *chi.Mux {
r := chi.NewRouter()
r.Use(handleCORS)
// Only protect application routes with JWT
r.Group(func(r chi.Router) {
r.Use(jwtVerifier, jwtAuthenticator)
mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, jwtEncoder))
})
printRoutes(r, flags.http)
mountSystemRoutes(r, flags.http)
return r
}
func mountRoutes(r chi.Router, opts *config.HTTP, mounts ...func(r chi.Router)) {
if opts.Logging {
r.Use(middleware.Logger)
@@ -57,3 +76,14 @@ func printRoutes(r chi.Router, opts *config.HTTP) {
}
printRoutes(r, "", "")
}
// Sets up default CORS rules to use as a middleware
func handleCORS(next http.Handler) http.Handler {
return cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}).Handler(next)
}
+82
View File
@@ -0,0 +1,82 @@
package service
import (
"fmt"
"log"
"os"
"strings"
"testing"
"net/http"
"net/http/httptest"
"net/http/httputil"
"github.com/joho/godotenv"
"github.com/namsral/flag"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/rbac"
)
func TestUsers(t *testing.T) {
godotenv.Load("../.env")
mountFlags("system", Flags, auth.Flags, rbac.Flags)
// log to stdout not stderr
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile)
err := Init()
assert(t, err == nil, "Error initializing: %+v", err)
req, err := http.NewRequest("GET", "http://localhost/auth/check", nil)
assert(t, err == nil, "Error creating request: %+v", err)
recorder := httptest.NewRecorder()
Routes().ServeHTTP(recorder, req)
resp := recorder.Result()
fmt.Println(">>> (request)")
fmt.Println(request(req))
fmt.Println("----")
fmt.Println("<<< (response)")
fmt.Println(response(resp))
}
func request(req *http.Request) string {
b, err := httputil.DumpRequestOut(req, true)
if err != nil {
return ">>> Error: " + err.Error()
}
if b != nil {
return strings.TrimSpace(string(b))
}
return ""
}
func response(resp *http.Response) string {
b, err := httputil.DumpResponse(resp, true)
if err != nil {
return "<<< Error: " + err.Error()
}
if b != nil {
return strings.TrimSpace(string(b))
}
return ""
}
func assert(t *testing.T, ok bool, format string, args ...interface{}) bool {
if !ok {
t.Fatalf(format, args...)
}
return ok
}
func mountFlags(prefix string, mountFlags ...func(...string)) {
for _, mount := range mountFlags {
mount(prefix)
}
flag.Parse()
}
+12 -1
View File
@@ -28,9 +28,13 @@ type (
UserService interface {
With(ctx context.Context) UserService
FindByUsername(username string) (*types.User, error)
FindByEmail(email string) (*types.User, error)
FindByID(id uint64) (*types.User, error)
Find(filter *types.UserFilter) (types.UserSet, error)
FindOrCreate(*types.User) (*types.User, error)
Create(input *types.User) (*types.User, error)
Update(mod *types.User) (*types.User, error)
@@ -38,7 +42,6 @@ type (
Suspend(id uint64) error
Unsuspend(id uint64) error
FindOrCreate(*types.User) (*types.User, error)
ValidateCredentials(username, password string) (*types.User, error)
}
)
@@ -90,6 +93,14 @@ func (svc *user) FindByID(id uint64) (*types.User, error) {
return svc.user.FindByID(id)
}
func (svc *user) FindByEmail(email string) (*types.User, error) {
return svc.user.FindByEmail(email)
}
func (svc *user) FindByUsername(username string) (*types.User, error) {
return svc.user.FindByUsername(username)
}
func (svc *user) Find(filter *types.UserFilter) (types.UserSet, error) {
return svc.user.Find(filter)
}
+18 -40
View File
@@ -7,14 +7,11 @@ import (
"net/http"
"github.com/SentimensRG/ctx/sigctx"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/titpetric/factory/resputil"
migrate "github.com/crusttech/crust/system/db"
"github.com/crusttech/crust/system/rest"
"github.com/crusttech/crust/system/service"
"github.com/crusttech/crust/internal/auth"
@@ -22,25 +19,36 @@ import (
"github.com/crusttech/crust/internal/version"
)
var (
jwtVerifier (func(http.Handler) http.Handler)
jwtAuthenticator (func(http.Handler) http.Handler)
jwtEncoder auth.TokenEncoder
)
func Init() error {
// validate configuration
if err := flags.Validate(); err != nil {
return err
}
// JWT Auth
jwtEncoder, err := auth.JWT()
if err != nil {
return errors.Wrap(err, "Error creating JWT Auth object")
}
jwtVerifier = jwtEncoder.Verifier()
jwtAuthenticator = jwtEncoder.Authenticator()
// start/configure database connection
factory.Database.Add("default", flags.db.DSN)
db, err := factory.Database.Get()
if err != nil {
return err
}
db := factory.Database.MustGet()
// @todo: profiling as an external service?
switch flags.db.Profiler {
case "stdout":
db.Profiler = &factory.Database.ProfilerStdout
default:
fmt.Println("No database query profiler selected")
log.Println("No database query profiler selected")
}
// migrate database schema
@@ -63,8 +71,6 @@ func Init() error {
}
func Start() error {
var deadline = sigctx.New()
log.Printf("Starting auth, version: %v, built on: %v", version.Version, version.BuildTime)
log.Println("Starting http server on address " + flags.http.Addr)
listener, err := net.Listen("tcp", flags.http.Addr)
@@ -72,41 +78,13 @@ func Start() error {
return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", flags.http.Addr))
}
// JWT Auth
jwtAuth, err := auth.JWT()
if err != nil {
return errors.Wrap(err, "Error creating JWT Auth object")
}
r := chi.NewRouter()
r.Use(handleCORS)
// Only protect application routes with JWT
r.Group(func(r chi.Router) {
r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator())
mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, jwtAuth))
})
printRoutes(r, flags.http)
mountSystemRoutes(r, flags.http)
if flags.monitor.Interval > 0 {
go metrics.NewMonitor(flags.monitor.Interval)
}
go http.Serve(listener, Routes())
go http.Serve(listener, r)
var deadline = sigctx.New()
<-deadline.Done()
return nil
}
// Sets up default CORS rules to use as a middleware
func handleCORS(next http.Handler) http.Handler {
return cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}).Handler(next)
}