diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6dff17..64f3b590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## (WIP) v0.15.1 +## (WIP) + +- Added `apis.Serve(app, options)` helper to allow starting the API server programmatically. + + +## v0.15.1 - Fixed `Ctrl + S` in the `editor` field not propagating the quick save shortcut to the parent form. diff --git a/apis/serve.go b/apis/serve.go new file mode 100644 index 00000000..5d0e8d33 --- /dev/null +++ b/apis/serve.go @@ -0,0 +1,165 @@ +package apis + +import ( + "crypto/tls" + "log" + "net" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/fatih/color" + "github.com/labstack/echo/v5/middleware" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/migrations/logs" + "github.com/pocketbase/pocketbase/tools/migrate" + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" +) + +// ServeOptions defines an optional struct for apis.Serve(). +type ServeOptions struct { + ShowStartBanner bool + HttpAddr string + HttpsAddr string + AllowedOrigins []string // optional list of CORS origins (default to "*") + BeforeServeFunc func(server *http.Server) error +} + +// Serve starts a new app web server. +func Serve(app core.App, options *ServeOptions) error { + if options == nil { + options = &ServeOptions{} + } + + if len(options.AllowedOrigins) == 0 { + options.AllowedOrigins = []string{"*"} + } + + // ensure that the latest migrations are applied before starting the server + if err := runMigrations(app); err != nil { + return err + } + + // reload app settings in case a new default value was set with a migration + // (or if this is the first time the init migration was executed) + if err := app.RefreshSettings(); err != nil { + color.Yellow("=====================================") + color.Yellow("WARNING: Settings load error! \n%v", err) + color.Yellow("Fallback to the application defaults.") + color.Yellow("=====================================") + } + + router, err := InitApi(app) + if err != nil { + return err + } + + // configure cors + router.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + Skipper: middleware.DefaultSkipper, + AllowOrigins: options.AllowedOrigins, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, + })) + + // start http server + // --- + mainAddr := options.HttpAddr + if options.HttpsAddr != "" { + mainAddr = options.HttpsAddr + } + + mainHost, _, _ := net.SplitHostPort(mainAddr) + + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")), + HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost), + } + + serverConfig := &http.Server{ + TLSConfig: &tls.Config{ + GetCertificate: certManager.GetCertificate, + NextProtos: []string{acme.ALPNProto}, + }, + ReadTimeout: 5 * time.Minute, + ReadHeaderTimeout: 30 * time.Second, + // WriteTimeout: 60 * time.Second, // breaks sse! + Handler: router, + Addr: mainAddr, + } + + if options.BeforeServeFunc != nil { + if err := options.BeforeServeFunc(serverConfig); err != nil { + return err + } + } + + if options.ShowStartBanner { + schema := "http" + if options.HttpsAddr != "" { + schema = "https" + } + + date := new(strings.Builder) + log.New(date, "", log.LstdFlags).Print() + + bold := color.New(color.Bold).Add(color.FgGreen) + bold.Printf( + "%s Server started at %s\n", + strings.TrimSpace(date.String()), + color.CyanString("%s://%s", schema, serverConfig.Addr), + ) + + regular := color.New() + regular.Printf(" ➜ REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr)) + regular.Printf(" ➜ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr)) + } + + // start HTTPS server + if options.HttpsAddr != "" { + // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version + if options.HttpAddr != "" { + go http.ListenAndServe(options.HttpAddr, certManager.HTTPHandler(nil)) + } + + return serverConfig.ListenAndServeTLS("", "") + } + + // OR start HTTP server + return serverConfig.ListenAndServe() +} + +type migrationsConnection struct { + DB *dbx.DB + MigrationsList migrate.MigrationsList +} + +func runMigrations(app core.App) error { + connections := []migrationsConnection{ + { + DB: app.DB(), + MigrationsList: migrations.AppMigrations, + }, + { + DB: app.LogsDB(), + MigrationsList: logs.LogsMigrations, + }, + } + + for _, c := range connections { + runner, err := migrate.NewRunner(c.DB, c.MigrationsList) + if err != nil { + return err + } + + if _, err := runner.Up(); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/serve.go b/cmd/serve.go index 6f7267a8..996f9b8c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,25 +1,12 @@ package cmd import ( - "crypto/tls" "log" - "net" "net/http" - "path/filepath" - "strings" - "time" - "github.com/fatih/color" - "github.com/labstack/echo/v5/middleware" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/migrations/logs" - "github.com/pocketbase/pocketbase/tools/migrate" "github.com/spf13/cobra" - "golang.org/x/crypto/acme" - "golang.org/x/crypto/acme/autocert" ) // NewServeCommand creates and returns new command responsible for @@ -33,96 +20,15 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { Use: "serve", Short: "Starts the web server (default to 127.0.0.1:8090)", Run: func(command *cobra.Command, args []string) { - // ensure that the latest migrations are applied before starting the server - if err := runMigrations(app); err != nil { - panic(err) - } + err := apis.Serve(app, &apis.ServeOptions{ + HttpAddr: httpAddr, + HttpsAddr: httpsAddr, + ShowStartBanner: showStartBanner, + AllowedOrigins: allowedOrigins, + }) - // reload app settings in case a new default value was set with a migration - // (or if this is the first time the init migration was executed) - if err := app.RefreshSettings(); err != nil { - color.Yellow("=====================================") - color.Yellow("WARNING: Settings load error! \n%v", err) - color.Yellow("Fallback to the application defaults.") - color.Yellow("=====================================") - } - - router, err := apis.InitApi(app) - if err != nil { - panic(err) - } - - // configure cors - router.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - Skipper: middleware.DefaultSkipper, - AllowOrigins: allowedOrigins, - AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, - })) - - // start http server - // --- - mainAddr := httpAddr - if httpsAddr != "" { - mainAddr = httpsAddr - } - - mainHost, _, _ := net.SplitHostPort(mainAddr) - - certManager := autocert.Manager{ - Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")), - HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost), - } - - serverConfig := &http.Server{ - TLSConfig: &tls.Config{ - GetCertificate: certManager.GetCertificate, - NextProtos: []string{acme.ALPNProto}, - }, - ReadTimeout: 5 * time.Minute, - ReadHeaderTimeout: 30 * time.Second, - // WriteTimeout: 60 * time.Second, // breaks sse! - Handler: router, - Addr: mainAddr, - } - - if showStartBanner { - schema := "http" - if httpsAddr != "" { - schema = "https" - } - - date := new(strings.Builder) - log.New(date, "", log.LstdFlags).Print() - - bold := color.New(color.Bold).Add(color.FgGreen) - bold.Printf( - "%s Server started at %s\n", - strings.TrimSpace(date.String()), - color.CyanString("%s://%s", schema, serverConfig.Addr), - ) - - regular := color.New() - regular.Printf(" ➜ REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr)) - regular.Printf(" ➜ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr)) - } - - var serveErr error - if httpsAddr != "" { - // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version - if httpAddr != "" { - go http.ListenAndServe(httpAddr, certManager.HTTPHandler(nil)) - } - - // start HTTPS server - serveErr = serverConfig.ListenAndServeTLS("", "") - } else { - // start HTTP server - serveErr = serverConfig.ListenAndServe() - } - - if serveErr != http.ErrServerClosed { - log.Fatalln(serveErr) + if err != http.ErrServerClosed { + log.Fatalln(err) } }, } @@ -150,34 +56,3 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { return command } - -type migrationsConnection struct { - DB *dbx.DB - MigrationsList migrate.MigrationsList -} - -func runMigrations(app core.App) error { - connections := []migrationsConnection{ - { - DB: app.DB(), - MigrationsList: migrations.AppMigrations, - }, - { - DB: app.LogsDB(), - MigrationsList: logs.LogsMigrations, - }, - } - - for _, c := range connections { - runner, err := migrate.NewRunner(c.DB, c.MigrationsList) - if err != nil { - return err - } - - if _, err := runner.Up(); err != nil { - return err - } - } - - return nil -}