mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-03-20 14:31:09 +02:00
added ServeEvent.InstallerFunc field
This commit is contained in:
parent
0155e9333f
commit
26cb1cef37
@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
- Added extra validators for the collection field `int64` options (e.g. `FileField.MaxSize`) restricting them to the max safe JSON number (2^53-1).
|
- Added extra validators for the collection field `int64` options (e.g. `FileField.MaxSize`) restricting them to the max safe JSON number (2^53-1).
|
||||||
|
|
||||||
|
- Added option to unset/overwrite the default PocketBase superuser installer using `ServeEvent.InstallerFunc`.
|
||||||
|
|
||||||
- Invalidate all record tokens when the auth record email is changed programmatically or by a superuser ([#5964](https://github.com/pocketbase/pocketbase/issues/5964)).
|
- Invalidate all record tokens when the auth record email is changed programmatically or by a superuser ([#5964](https://github.com/pocketbase/pocketbase/issues/5964)).
|
||||||
|
|
||||||
- Eagerly interrupt waiting for the email alert send in case it takes longer than 15s.
|
- Eagerly interrupt waiting for the email alert send in case it takes longer than 15s.
|
||||||
|
@ -5,37 +5,31 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @todo consider combining with the installer specific hooks after refactoring cmd
|
// DefaultInstallerFunc is the default PocketBase installer function.
|
||||||
func loadInstaller(app core.App, dashboardURL string) error {
|
//
|
||||||
if !needInstallerSuperuser(app) {
|
// It will attempt to open a link in the browser (with a short-lived auth
|
||||||
return nil
|
// token for the systemSuperuser) to the installer UI so that users can
|
||||||
}
|
// create their own custom superuser record.
|
||||||
|
//
|
||||||
installerRecord, err := findOrCreateInstallerSuperuser(app)
|
// See https://github.com/pocketbase/pocketbase/discussions/5814.
|
||||||
if err != nil {
|
func DefaultInstallerFunc(app core.App, systemSuperuser *core.Record, baseURL string) error {
|
||||||
return err
|
token, err := systemSuperuser.NewStaticAuthToken(30 * time.Minute)
|
||||||
}
|
|
||||||
|
|
||||||
token, err := installerRecord.NewStaticAuthToken(30 * time.Minute)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// launch url (ignore errors and always print a help text as fallback)
|
// launch url (ignore errors and always print a help text as fallback)
|
||||||
url := fmt.Sprintf("%s/#/pbinstal/%s", strings.TrimRight(dashboardURL, "/"), token)
|
url := fmt.Sprintf("%s/_/#/pbinstal/%s", strings.TrimRight(baseURL, "/"), token)
|
||||||
_ = launchURL(url)
|
_ = osutils.LaunchURL(url)
|
||||||
color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
|
color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
|
||||||
color.New(color.Bold).Add(color.FgCyan).Println(url)
|
color.New(color.Bold).Add(color.FgCyan).Println(url)
|
||||||
color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", os.Args[0])
|
color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", os.Args[0])
|
||||||
@ -43,6 +37,23 @@ func loadInstaller(app core.App, dashboardURL string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadInstaller(
|
||||||
|
app core.App,
|
||||||
|
baseURL string,
|
||||||
|
installerFunc func(app core.App, systemSuperuser *core.Record, baseURL string) error,
|
||||||
|
) error {
|
||||||
|
if installerFunc == nil || !needInstallerSuperuser(app) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
superuser, err := findOrCreateInstallerSuperuser(app)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return installerFunc(app, superuser, baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
func needInstallerSuperuser(app core.App) bool {
|
func needInstallerSuperuser(app core.App) bool {
|
||||||
total, err := app.CountRecords(core.CollectionNameSuperusers, dbx.Not(dbx.HashExp{
|
total, err := app.CountRecords(core.CollectionNameSuperusers, dbx.Not(dbx.HashExp{
|
||||||
"email": core.DefaultInstallerEmail,
|
"email": core.DefaultInstallerEmail,
|
||||||
@ -65,7 +76,7 @@ func findOrCreateInstallerSuperuser(app core.App) (*core.Record, error) {
|
|||||||
|
|
||||||
record = core.NewRecord(col)
|
record = core.NewRecord(col)
|
||||||
record.SetEmail(core.DefaultInstallerEmail)
|
record.SetEmail(core.DefaultInstallerEmail)
|
||||||
record.SetPassword(security.RandomString(30))
|
record.SetRandomPassword()
|
||||||
|
|
||||||
err = app.Save(record)
|
err = app.Save(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,20 +86,3 @@ func findOrCreateInstallerSuperuser(app core.App) (*core.Record, error) {
|
|||||||
|
|
||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func launchURL(url string) error {
|
|
||||||
if err := is.URL.Validate(url); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
return exec.Command("open", url).Start()
|
|
||||||
case "windows":
|
|
||||||
// not sure if this is the best command but seems to be the most reliable based on the comments in
|
|
||||||
// https://stackoverflow.com/questions/3739327/launching-a-website-via-the-windows-commandline#answer-49115945
|
|
||||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
|
||||||
default: // linux, freebsd, etc.
|
|
||||||
return exec.Command("xdg-open", url).Start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -17,6 +16,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/tools/hook"
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
"github.com/pocketbase/pocketbase/tools/list"
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/routine"
|
||||||
"github.com/pocketbase/pocketbase/ui"
|
"github.com/pocketbase/pocketbase/ui"
|
||||||
"golang.org/x/crypto/acme"
|
"golang.org/x/crypto/acme"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
@ -158,6 +158,7 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
serveEvent.Router = pbRouter
|
serveEvent.Router = pbRouter
|
||||||
serveEvent.Server = server
|
serveEvent.Server = server
|
||||||
serveEvent.CertManager = certManager
|
serveEvent.CertManager = certManager
|
||||||
|
serveEvent.InstallerFunc = DefaultInstallerFunc
|
||||||
|
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
|
|
||||||
@ -204,6 +205,8 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
}()
|
}()
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
var baseURL string
|
||||||
|
|
||||||
// trigger the OnServe hook and start the tcp listener
|
// trigger the OnServe hook and start the tcp listener
|
||||||
serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
|
serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
|
||||||
handler, err := e.Router.BuildMux()
|
handler, err := e.Router.BuildMux()
|
||||||
@ -213,10 +216,20 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
|
|
||||||
e.Server.Handler = handler
|
e.Server.Handler = handler
|
||||||
|
|
||||||
addr := e.Server.Addr
|
if config.HttpsAddr == "" {
|
||||||
|
baseURL = "http://" + serverAddrToHost(serveEvent.Server.Addr)
|
||||||
|
} else {
|
||||||
|
baseURL = "https://"
|
||||||
|
if len(config.CertificateDomains) > 0 {
|
||||||
|
baseURL += config.CertificateDomains[0]
|
||||||
|
} else {
|
||||||
|
baseURL += serverAddrToHost(serveEvent.Server.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fallback similar to the std Server.ListenAndServe/ListenAndServeTLS
|
addr := e.Server.Addr
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
|
// fallback similar to the std Server.ListenAndServe/ListenAndServeTLS
|
||||||
if config.HttpsAddr != "" {
|
if config.HttpsAddr != "" {
|
||||||
addr = ":https"
|
addr = ":https"
|
||||||
} else {
|
} else {
|
||||||
@ -224,11 +237,22 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lnErr error
|
listener, err = net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
listener, lnErr = net.Listen("tcp", addr)
|
if e.InstallerFunc != nil {
|
||||||
|
app := e.App
|
||||||
|
installerFunc := e.InstallerFunc
|
||||||
|
routine.FireAndForget(func() {
|
||||||
|
if err := loadInstaller(app, baseURL, installerFunc); err != nil {
|
||||||
|
app.Logger().Warn("Failed to initialize installer", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return lnErr
|
return nil
|
||||||
})
|
})
|
||||||
if serveHookErr != nil {
|
if serveHookErr != nil {
|
||||||
return serveHookErr
|
return serveHookErr
|
||||||
@ -238,17 +262,6 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
|
return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
|
||||||
}
|
}
|
||||||
|
|
||||||
schema := "http"
|
|
||||||
addr := server.Addr
|
|
||||||
if config.HttpsAddr != "" {
|
|
||||||
schema = "https"
|
|
||||||
if len(config.CertificateDomains) > 0 {
|
|
||||||
addr = config.CertificateDomains[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
baseURL := fmt.Sprintf("%s://%s", schema, addr)
|
|
||||||
dashboardURL := fmt.Sprintf("%s/_", baseURL)
|
|
||||||
|
|
||||||
if config.ShowStartBanner {
|
if config.ShowStartBanner {
|
||||||
date := new(strings.Builder)
|
date := new(strings.Builder)
|
||||||
log.New(date, "", log.LstdFlags).Print()
|
log.New(date, "", log.LstdFlags).Print()
|
||||||
@ -262,16 +275,9 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
|
|
||||||
regular := color.New()
|
regular := color.New()
|
||||||
regular.Printf("├─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
|
regular.Printf("├─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
|
||||||
regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/", dashboardURL))
|
regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/_/", baseURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
installerErr := loadInstaller(app, dashboardURL)
|
|
||||||
if installerErr != nil {
|
|
||||||
app.Logger().Warn("Failed to initialize installer", "error", installerErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var serveErr error
|
var serveErr error
|
||||||
if config.HttpsAddr != "" {
|
if config.HttpsAddr != "" {
|
||||||
if config.HttpAddr != "" {
|
if config.HttpAddr != "" {
|
||||||
@ -280,10 +286,10 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start HTTPS server
|
// start HTTPS server
|
||||||
serveErr = server.ServeTLS(listener, "", "")
|
serveErr = serveEvent.Server.ServeTLS(listener, "", "")
|
||||||
} else {
|
} else {
|
||||||
// OR start HTTP server
|
// OR start HTTP server
|
||||||
serveErr = server.Serve(listener)
|
serveErr = serveEvent.Server.Serve(listener)
|
||||||
}
|
}
|
||||||
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
||||||
return serveErr
|
return serveErr
|
||||||
@ -292,6 +298,14 @@ func Serve(app core.App, config ServeConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serverAddrToHost loosely converts http.Server.Addr string into a host to print.
|
||||||
|
func serverAddrToHost(addr string) string {
|
||||||
|
if addr == "" || strings.HasSuffix(addr, ":http") || strings.HasSuffix(addr, ":https") {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
type serverErrorLogWriter struct {
|
type serverErrorLogWriter struct {
|
||||||
app core.App
|
app core.App
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,22 @@ type ServeEvent struct {
|
|||||||
Router *router.Router[*RequestEvent]
|
Router *router.Router[*RequestEvent]
|
||||||
Server *http.Server
|
Server *http.Server
|
||||||
CertManager *autocert.Manager
|
CertManager *autocert.Manager
|
||||||
|
|
||||||
|
// InstallerFunc is the "installer" function that is called after
|
||||||
|
// successfull server tcp bind but only if there is no explicit
|
||||||
|
// superuser record created yet.
|
||||||
|
//
|
||||||
|
// It runs in a separate goroutine and its default value is [apis.DefaultInstallerFunc].
|
||||||
|
//
|
||||||
|
// It receives a system superuser record as argument that you can use to generate
|
||||||
|
// a short-lived auth token (e.g. systemSuperuser.NewStaticAuthToken(30 * time.Minute))
|
||||||
|
// and concatenate it as query param for your installer page
|
||||||
|
// (if you are using the client-side SDKs, you can then load the
|
||||||
|
// token with pb.authStore.save(token) and perform any Web API request
|
||||||
|
// e.g. creating a new superuser).
|
||||||
|
//
|
||||||
|
// Set it to nil if you want to skip the installer.
|
||||||
|
InstallerFunc func(app App, systemSuperuser *Record, baseURL string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
31
tools/osutils/cmd.go
Normal file
31
tools/osutils/cmd.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package osutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LaunchURL attempts to open the provided url in the user's default browser.
|
||||||
|
//
|
||||||
|
// It is platform dependent and it uses:
|
||||||
|
// - "open" on macOS
|
||||||
|
// - "rundll32" on Windows
|
||||||
|
// - "xdg-open" on everything else (Linux, FreeBSD, etc.)
|
||||||
|
func LaunchURL(url string) error {
|
||||||
|
if err := is.URL.Validate(url); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
return exec.Command("open", url).Start()
|
||||||
|
case "windows":
|
||||||
|
// not sure if this is the best command but seems to be the most reliable based on the comments in
|
||||||
|
// https://stackoverflow.com/questions/3739327/launching-a-website-via-the-windows-commandline#answer-49115945
|
||||||
|
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
default:
|
||||||
|
return exec.Command("xdg-open", url).Start()
|
||||||
|
}
|
||||||
|
}
|
@ -116,4 +116,3 @@ function refreshProtectedFilesCollectionsCache() {
|
|||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user