github.com/dhax/go-base@v0.0.0-20231004214136-8be7e5c1972b/api/api.go (about)

     1  // Package api configures an http server for administration and application resources.
     2  package api
     3  
     4  import (
     5  	"net/http"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/dhax/go-base/api/admin"
    12  	"github.com/dhax/go-base/api/app"
    13  	"github.com/dhax/go-base/auth/jwt"
    14  	"github.com/dhax/go-base/auth/pwdless"
    15  	"github.com/dhax/go-base/database"
    16  	"github.com/dhax/go-base/email"
    17  	"github.com/dhax/go-base/logging"
    18  	"github.com/go-chi/chi/v5"
    19  	"github.com/go-chi/chi/v5/middleware"
    20  	"github.com/go-chi/cors"
    21  	"github.com/go-chi/render"
    22  )
    23  
    24  // New configures application resources and routes.
    25  func New(enableCORS bool) (*chi.Mux, error) {
    26  	logger := logging.NewLogger()
    27  
    28  	db, err := database.DBConn()
    29  	if err != nil {
    30  		logger.WithField("module", "database").Error(err)
    31  		return nil, err
    32  	}
    33  
    34  	mailer, err := email.NewMailer()
    35  	if err != nil {
    36  		logger.WithField("module", "email").Error(err)
    37  		return nil, err
    38  	}
    39  
    40  	authStore := database.NewAuthStore(db)
    41  	authResource, err := pwdless.NewResource(authStore, mailer)
    42  	if err != nil {
    43  		logger.WithField("module", "auth").Error(err)
    44  		return nil, err
    45  	}
    46  
    47  	adminAPI, err := admin.NewAPI(db)
    48  	if err != nil {
    49  		logger.WithField("module", "admin").Error(err)
    50  		return nil, err
    51  	}
    52  
    53  	appAPI, err := app.NewAPI(db)
    54  	if err != nil {
    55  		logger.WithField("module", "app").Error(err)
    56  		return nil, err
    57  	}
    58  
    59  	r := chi.NewRouter()
    60  	r.Use(middleware.Recoverer)
    61  	r.Use(middleware.RequestID)
    62  	// r.Use(middleware.RealIP)
    63  	r.Use(middleware.Timeout(15 * time.Second))
    64  
    65  	r.Use(logging.NewStructuredLogger(logger))
    66  	r.Use(render.SetContentType(render.ContentTypeJSON))
    67  
    68  	// use CORS middleware if client is not served by this api, e.g. from other domain or CDN
    69  	if enableCORS {
    70  		r.Use(corsConfig().Handler)
    71  	}
    72  
    73  	r.Mount("/auth", authResource.Router())
    74  	r.Group(func(r chi.Router) {
    75  		r.Use(authResource.TokenAuth.Verifier())
    76  		r.Use(jwt.Authenticator)
    77  		r.Mount("/admin", adminAPI.Router())
    78  		r.Mount("/api", appAPI.Router())
    79  	})
    80  
    81  	r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
    82  		w.Write([]byte("pong"))
    83  	})
    84  
    85  	client := "./public"
    86  	r.Get("/*", SPAHandler(client))
    87  
    88  	return r, nil
    89  }
    90  
    91  func corsConfig() *cors.Cors {
    92  	// Basic CORS
    93  	// for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
    94  	return cors.New(cors.Options{
    95  		// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
    96  		AllowedOrigins: []string{"*"},
    97  		// AllowOriginFunc:  func(r *http.Request, origin string) bool { return true },
    98  		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    99  		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
   100  		ExposedHeaders:   []string{"Link"},
   101  		AllowCredentials: true,
   102  		MaxAge:           86400, // Maximum value not ignored by any of major browsers
   103  	})
   104  }
   105  
   106  // SPAHandler serves the public Single Page Application.
   107  func SPAHandler(publicDir string) http.HandlerFunc {
   108  	handler := http.FileServer(http.Dir(publicDir))
   109  	return func(w http.ResponseWriter, r *http.Request) {
   110  		indexPage := path.Join(publicDir, "index.html")
   111  		serviceWorker := path.Join(publicDir, "service-worker.js")
   112  
   113  		requestedAsset := path.Join(publicDir, r.URL.Path)
   114  		if strings.Contains(requestedAsset, "service-worker.js") {
   115  			requestedAsset = serviceWorker
   116  		}
   117  		if _, err := os.Stat(requestedAsset); err != nil {
   118  			http.ServeFile(w, r, indexPage)
   119  			return
   120  		}
   121  		handler.ServeHTTP(w, r)
   122  	}
   123  }