// Package apis implements the default PocketBase api services and middlewares. package apis import ( "fmt" "io/fs" "log" "net/http" "net/url" "path/filepath" "strings" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/rest" "github.com/pocketbase/pocketbase/ui" "github.com/spf13/cast" ) // InitApi creates a configured echo instance with registered // system and app specific routes and middlewares. func InitApi(app core.App) (*echo.Echo, error) { e := echo.New() e.Debug = app.IsDebug() // default middlewares e.Pre(middleware.RemoveTrailingSlash()) e.Use(middleware.Recover()) e.Use(middleware.Secure()) e.Use(LoadAuthContext(app)) // custom error handler e.HTTPErrorHandler = func(c echo.Context, err error) { if c.Response().Committed { return } var apiErr *rest.ApiError switch v := err.(type) { case *echo.HTTPError: if v.Internal != nil && app.IsDebug() { log.Println(v.Internal) } msg := fmt.Sprintf("%v", v.Message) apiErr = rest.NewApiError(v.Code, msg, v) case *rest.ApiError: if app.IsDebug() && v.RawData() != nil { log.Println(v.RawData()) } apiErr = v default: if err != nil && app.IsDebug() { log.Println(err) } apiErr = rest.NewBadRequestError("", err) } // Send response var cErr error if c.Request().Method == http.MethodHead { // @see https://github.com/labstack/echo/issues/608 cErr = c.NoContent(apiErr.Code) } else { cErr = c.JSON(apiErr.Code, apiErr) } // truly rare case; eg. client already disconnected if cErr != nil && app.IsDebug() { log.Println(cErr) } } // admin ui routes bindStaticAdminUI(app, e) // default routes api := e.Group("/api") BindSettingsApi(app, api) BindAdminApi(app, api) BindUserApi(app, api) BindCollectionApi(app, api) BindRecordApi(app, api) BindFileApi(app, api) BindRealtimeApi(app, api) BindLogsApi(app, api) // trigger the custom BeforeServe hook for the created api router // allowing users to further adjust its options or register new routes serveEvent := &core.ServeEvent{ App: app, Router: e, } if err := app.OnBeforeServe().Trigger(serveEvent); err != nil { return nil, err } // catch all any route api.Any("/*", func(c echo.Context) error { return echo.ErrNotFound }, ActivityLogger(app)) return e, nil } // StaticDirectoryHandler is similar to `echo.StaticDirectoryHandler` // but without the directory redirect which conflicts with RemoveTrailingSlash middleware. // // @see https://github.com/labstack/echo/issues/2211 func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) echo.HandlerFunc { return func(c echo.Context) error { p := c.PathParam("*") if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice tmpPath, err := url.PathUnescape(p) if err != nil { return fmt.Errorf("failed to unescape path variable: %w", err) } p = tmpPath } // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/"))) return c.FileFS(name, fileSystem) } } // bindStaticAdminUI registers the endpoints that serves the static admin UI. func bindStaticAdminUI(app core.App, e *echo.Echo) error { // serves /ui/dist/index.html file // (explicit route is used to avoid conflicts with `RemoveTrailingSlash` middleware) e.FileFS( "/_", "index.html", ui.DistIndexHTML, middleware.Gzip(), installerRedirect(app), ) // serves static files from the /ui/dist directory // (similar to echo.StaticFS but with gzip middleware enabled) e.GET( "/_/*", StaticDirectoryHandler(ui.DistDirFS, false), middleware.Gzip(), ) return nil } const totalAdminsCacheKey = "totalAdmins" func updateTotalAdminsCache(app core.App) error { total, err := app.Dao().TotalAdmins() if err != nil { return err } app.Cache().Set(totalAdminsCacheKey, total) return nil } // installerRedirect redirects the user to the installer admin UI page // when the application needs some preliminary configurations to be done. func installerRedirect(app core.App) echo.MiddlewareFunc { // keep totalAdminsCacheKey value up-to-date app.OnAdminAfterCreateRequest().Add(func(data *core.AdminCreateEvent) error { return updateTotalAdminsCache(app) }) app.OnAdminAfterDeleteRequest().Add(func(data *core.AdminDeleteEvent) error { return updateTotalAdminsCache(app) }) return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // load into cache (if not already) if !app.Cache().Has(totalAdminsCacheKey) { if err := updateTotalAdminsCache(app); err != nil { return err } } totalAdmins := cast.ToInt(app.Cache().Get(totalAdminsCacheKey)) _, hasInstallerParam := c.Request().URL.Query()["installer"] if totalAdmins == 0 && !hasInstallerParam { // redirect to the installer page return c.Redirect(http.StatusTemporaryRedirect, "/_/?installer#") } if totalAdmins != 0 && hasInstallerParam { // redirect to the home page return c.Redirect(http.StatusTemporaryRedirect, "/_/#/") } return next(c) } } }