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 }