1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-24 17:07:00 +02:00
pocketbase/apis/base.go

265 lines
7.0 KiB
Go
Raw Normal View History

2022-07-06 23:19:05 +02:00
// Package apis implements the default PocketBase api services and middlewares.
package apis
import (
2023-07-18 11:33:18 +02:00
"database/sql"
2022-10-30 10:28:14 +02:00
"errors"
2022-07-06 23:19:05 +02:00
"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"
2022-07-06 23:19:05 +02:00
"github.com/pocketbase/pocketbase/ui"
"github.com/spf13/cast"
2022-07-06 23:19:05 +02:00
)
const trailedAdminPath = "/_/"
2022-07-06 23:19:05 +02:00
// 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()
e.JSONSerializer = &rest.Serializer{
FieldsParam: "fields",
}
2022-07-06 23:19:05 +02:00
// configure a custom router
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
return echo.NewRouter(echo.RouterConfig{
UnescapePathParamValues: true,
AllowOverwritingRoute: true,
})
})
2022-07-06 23:19:05 +02:00
// default middlewares
e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
Skipper: func(c echo.Context) bool {
// enable by default only for the API routes
return !strings.HasPrefix(c.Request().URL.Path, "/api/")
},
}))
2022-07-06 23:19:05 +02:00
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 {
if app.IsDebug() {
log.Println("HTTPErrorHandler response was already committed:", err)
}
2022-07-06 23:19:05 +02:00
return
}
2022-10-30 10:28:14 +02:00
var apiErr *ApiError
2022-07-06 23:19:05 +02:00
switch v := err.(type) {
case *echo.HTTPError:
2022-07-06 23:19:05 +02:00
if v.Internal != nil && app.IsDebug() {
log.Println(v.Internal)
}
msg := fmt.Sprintf("%v", v.Message)
2022-10-30 10:28:14 +02:00
apiErr = NewApiError(v.Code, msg, v)
case *ApiError:
2022-07-06 23:19:05 +02:00
if app.IsDebug() && v.RawData() != nil {
log.Println(v.RawData())
}
apiErr = v
default:
if err != nil && app.IsDebug() {
log.Println(err)
}
2023-07-18 11:33:18 +02:00
if err != nil && errors.Is(err, sql.ErrNoRows) {
apiErr = NewNotFoundError("", err)
} else {
apiErr = NewBadRequestError("", err)
}
2022-07-06 23:19:05 +02:00
}
event := new(core.ApiErrorEvent)
event.HttpContext = c
event.Error = apiErr
2022-07-06 23:19:05 +02:00
// send error response
2022-12-02 16:36:15 +02:00
hookErr := app.OnBeforeApiError().Trigger(event, func(e *core.ApiErrorEvent) error {
if c.Response().Committed {
return nil
}
// @see https://github.com/labstack/echo/issues/608
2022-12-02 16:36:15 +02:00
if e.HttpContext.Request().Method == http.MethodHead {
return e.HttpContext.NoContent(apiErr.Code)
}
return e.HttpContext.JSON(apiErr.Code, apiErr)
})
if hookErr == nil {
if err := app.OnAfterApiError().Trigger(event); err != nil && app.IsDebug() {
log.Println(hookErr)
}
} else if app.IsDebug() {
// truly rare case; eg. client already disconnected
2022-12-02 16:36:15 +02:00
log.Println(hookErr)
2022-07-06 23:19:05 +02:00
}
}
// admin ui routes
bindStaticAdminUI(app, e)
2022-07-06 23:19:05 +02:00
// default routes
api := e.Group("/api", eagerRequestInfoCache(app))
2022-10-30 10:28:14 +02:00
bindSettingsApi(app, api)
bindAdminApi(app, api)
bindCollectionApi(app, api)
bindRecordCrudApi(app, api)
bindRecordAuthApi(app, api)
bindFileApi(app, api)
bindRealtimeApi(app, api)
bindLogsApi(app, api)
2022-12-11 17:32:43 +02:00
bindHealthApi(app, api)
2023-05-13 21:10:14 +02:00
bindBackupApi(app, api)
2022-07-06 23:19:05 +02:00
// 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.
//
2022-10-30 10:28:14 +02:00
// If a file resource is missing and indexFallback is set, the request
// will be forwarded to the base index.html (useful also for SPA).
//
2022-07-06 23:19:05 +02:00
// @see https://github.com/labstack/echo/issues/2211
2022-10-30 10:28:14 +02:00
func StaticDirectoryHandler(fileSystem fs.FS, indexFallback bool) echo.HandlerFunc {
2022-07-06 23:19:05 +02:00
return func(c echo.Context) error {
p := c.PathParam("*")
2022-10-30 10:28:14 +02:00
// escape url path
tmpPath, err := url.PathUnescape(p)
if err != nil {
return fmt.Errorf("failed to unescape path variable: %w", err)
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
p = tmpPath
2022-07-06 23:19:05 +02:00
// 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, "/")))
2022-10-30 10:28:14 +02:00
fileErr := c.FileFS(name, fileSystem)
if fileErr != nil && indexFallback && errors.Is(fileErr, echo.ErrNotFound) {
return c.FileFS("index.html", fileSystem)
}
return fileErr
2022-07-06 23:19:05 +02:00
}
}
// bindStaticAdminUI registers the endpoints that serves the static admin UI.
func bindStaticAdminUI(app core.App, e *echo.Echo) error {
// redirect to trailing slash to ensure that relative urls will still work properly
e.GET(
strings.TrimRight(trailedAdminPath, "/"),
func(c echo.Context) error {
return c.Redirect(http.StatusTemporaryRedirect, strings.TrimLeft(trailedAdminPath, "/"))
},
)
// serves static files from the /ui/dist directory
// (similar to echo.StaticFS but with gzip middleware enabled)
e.GET(
trailedAdminPath+"*",
echo.StaticDirectoryHandler(ui.DistDirFS, false),
installerRedirect(app),
uiCacheControl(),
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
}
func uiCacheControl() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// add default Cache-Control header for all Admin UI resources
// (ignoring the root admin path)
if c.Request().URL.Path != trailedAdminPath {
c.Response().Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
}
return next(c)
}
}
}
// 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 {
// skip redirect checks for non-root level index.html requests
path := c.Request().URL.Path
if path != trailedAdminPath && path != trailedAdminPath+"index.html" {
return next(c)
}
// 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 {
// clear the installer param
return c.Redirect(http.StatusTemporaryRedirect, "?")
}
return next(c)
}
}
}