mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-20 13:30:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4fcba5210 | ||
|
|
d8c8289269 | ||
|
|
dbbfa243bc | ||
|
|
a16b0c9004 | ||
|
|
03a7bafa66 | ||
|
|
d129959098 | ||
|
|
6749559a22 | ||
|
|
28bc2678e9 | ||
|
|
d4202e696b | ||
|
|
9de3cc99a0 | ||
|
|
111bc59472 | ||
|
|
b2647ebca9 | ||
|
|
ef226cf9c1 | ||
|
|
63d5a8d633 | ||
|
|
d71c3cd19c | ||
|
|
240bd6790a | ||
|
|
05a4071eba | ||
|
|
ce857985be | ||
|
|
320d1482a4 | ||
|
|
46399dddac | ||
|
|
52c288d9db | ||
|
|
ed741662b2 | ||
|
|
0c14f32822 | ||
|
|
ec0d3b0d3d | ||
|
|
10d7faea31 | ||
|
|
f62664098e | ||
|
|
1dfc314bc1 | ||
|
|
7b2d88fa30 | ||
|
|
72cb2d3f43 | ||
|
|
0739e90ff2 | ||
|
|
460c684caa | ||
|
|
d64fbf9011 | ||
|
|
dfd9528847 | ||
|
|
d4d425c61d | ||
|
|
0f0c00bd03 | ||
|
|
a26006ad56 | ||
|
|
d761fa12af |
@@ -1,4 +1,5 @@
|
||||
/.vscode/
|
||||
.idea
|
||||
|
||||
.DS_Store
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ before:
|
||||
builds:
|
||||
- main: ./examples/base
|
||||
binary: pocketbase
|
||||
ldflags:
|
||||
- -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
lint:
|
||||
golangci-lint run -c ./golangci.yml ./...
|
||||
|
||||
test:
|
||||
go test -v --cover ./...
|
||||
|
||||
test-report:
|
||||
go test -v --cover -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
**For documentation and examples, please visit https://pocketbase.io/docs.**
|
||||
|
||||
> ⚠️ Although the web API defintions are considered stable,
|
||||
> ⚠️ Although the web API definitions are considered stable,
|
||||
> please keep in mind that PocketBase is still under active development
|
||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||
|
||||
@@ -43,6 +43,7 @@ your own custom app specific business logic and still have a single portable exe
|
||||
# go 1.18+
|
||||
go get github.com/pocketbase/pocketbase
|
||||
```
|
||||
> For Windows, you may have to use go 1.19+ due to an incorrect js mime type in the Windows Registry (see [issue#6](https://github.com/pocketbase/pocketbase/issues/6)).
|
||||
|
||||
### Example
|
||||
|
||||
@@ -90,10 +91,12 @@ Running/building the application is the same as for any other Go program, aka. j
|
||||
|
||||
**PocketBase embeds SQLite, but doesn't require CGO.**
|
||||
|
||||
If CGO is enabled, it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite).
|
||||
If CGO is enabled, it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite). You can enable and disable CGO by setting the `CGO_ENABLED` enviroment variable to `1` or `0` respectively.
|
||||
|
||||
Enable CGO only if you really need to squeeze the read/write query performance at the expense of complicating cross compilation.
|
||||
|
||||
To build the minimal standalone executable, like the prebuilt ones in the releases page, you can simply run `go build` inside the `examples/base` directory.
|
||||
|
||||
### Testing
|
||||
|
||||
PocketBase comes with mixed bag of unit and integration tests.
|
||||
|
||||
+31
-23
@@ -24,7 +24,7 @@ func BindAdminApi(app core.App, rg *echo.Group) {
|
||||
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
|
||||
subGroup.POST("/refresh", api.refresh, RequireAdminAuth())
|
||||
subGroup.GET("", api.list, RequireAdminAuth())
|
||||
subGroup.POST("", api.create, RequireAdminAuth())
|
||||
subGroup.POST("", api.create, RequireAdminAuthOnlyIfAny(app))
|
||||
subGroup.GET("/:id", api.view, RequireAdminAuth())
|
||||
subGroup.PATCH("/:id", api.update, RequireAdminAuth())
|
||||
subGroup.DELETE("/:id", api.delete, RequireAdminAuth())
|
||||
@@ -66,7 +66,7 @@ func (api *adminApi) refresh(c echo.Context) error {
|
||||
func (api *adminApi) emailAuth(c echo.Context) error {
|
||||
form := forms.NewAdminLogin(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
admin, submitErr := form.Submit()
|
||||
@@ -80,11 +80,11 @@ func (api *adminApi) emailAuth(c echo.Context) error {
|
||||
func (api *adminApi) requestPasswordReset(c echo.Context) error {
|
||||
form := forms.NewAdminPasswordResetRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
|
||||
}
|
||||
|
||||
if err := form.Validate(); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while validating the form.", err)
|
||||
return rest.NewBadRequestError("An error occurred while validating the form.", err)
|
||||
}
|
||||
|
||||
// run in background because we don't need to show the result
|
||||
@@ -101,7 +101,7 @@ func (api *adminApi) requestPasswordReset(c echo.Context) error {
|
||||
func (api *adminApi) confirmPasswordReset(c echo.Context) error {
|
||||
form := forms.NewAdminPasswordResetConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
admin, submitErr := form.Submit()
|
||||
@@ -166,7 +166,7 @@ func (api *adminApi) create(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.AdminCreateEvent{
|
||||
@@ -174,20 +174,24 @@ func (api *adminApi) create(c echo.Context) error {
|
||||
Admin: admin,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error {
|
||||
// create the admin
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create admin.", err)
|
||||
}
|
||||
// create the admin
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create admin.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Admin)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Admin)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnAdminAfterCreateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *adminApi) update(c echo.Context) error {
|
||||
@@ -205,7 +209,7 @@ func (api *adminApi) update(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.AdminUpdateEvent{
|
||||
@@ -213,20 +217,24 @@ func (api *adminApi) update(c echo.Context) error {
|
||||
Admin: admin,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error {
|
||||
// update the admin
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update admin.", err)
|
||||
}
|
||||
// update the admin
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update admin.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Admin)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Admin)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnAdminAfterUpdateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *adminApi) delete(c echo.Context) error {
|
||||
|
||||
+26
-10
@@ -456,12 +456,37 @@ func TestAdminDelete(t *testing.T) {
|
||||
func TestAdminCreate(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Name: "unauthorized (while having at least 1 existing admin)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "unauthorized (while having 0 existing admins)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins",
|
||||
Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":3}`),
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// delete all admins
|
||||
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":`,
|
||||
`"email":"testnew@example.com"`,
|
||||
`"avatar":3`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 1,
|
||||
"OnModelAfterCreate": 1,
|
||||
"OnAdminBeforeCreateRequest": 1,
|
||||
"OnAdminAfterCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user",
|
||||
Method: http.MethodPost,
|
||||
@@ -482,9 +507,6 @@ func TestAdminCreate(t *testing.T) {
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnAdminBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + invalid data format",
|
||||
@@ -507,9 +529,6 @@ func TestAdminCreate(t *testing.T) {
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnAdminBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + valid data",
|
||||
@@ -622,9 +641,6 @@ func TestAdminUpdate(t *testing.T) {
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnAdminBeforeUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Method: http.MethodPatch,
|
||||
|
||||
+81
-10
@@ -15,6 +15,7 @@ import (
|
||||
"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
|
||||
@@ -38,13 +39,13 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
var apiErr *rest.ApiError
|
||||
|
||||
switch v := err.(type) {
|
||||
case (*echo.HTTPError):
|
||||
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):
|
||||
case *rest.ApiError:
|
||||
if app.IsDebug() && v.RawData() != nil {
|
||||
log.Println(v.RawData())
|
||||
}
|
||||
@@ -67,17 +68,12 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
|
||||
// truly rare case; eg. client already disconnected
|
||||
if cErr != nil && app.IsDebug() {
|
||||
log.Println(err)
|
||||
log.Println(cErr)
|
||||
}
|
||||
}
|
||||
|
||||
// serves /ui/dist/index.html file
|
||||
// (explicit route is used to avoid conflicts with `RemoveTrailingSlash` middleware)
|
||||
e.FileFS("/_", "index.html", ui.DistIndexHTML, middleware.Gzip())
|
||||
|
||||
// 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())
|
||||
// admin ui routes
|
||||
bindStaticAdminUI(app, e)
|
||||
|
||||
// default routes
|
||||
api := e.Group("/api")
|
||||
@@ -129,3 +125,78 @@ func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) echo.H
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-20
@@ -76,9 +76,9 @@ func (api *collectionApi) create(c echo.Context) error {
|
||||
|
||||
form := forms.NewCollectionUpsert(api.app, collection)
|
||||
|
||||
// read
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.CollectionCreateEvent{
|
||||
@@ -86,20 +86,24 @@ func (api *collectionApi) create(c echo.Context) error {
|
||||
Collection: collection,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error {
|
||||
// submit
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create the collection.", err)
|
||||
}
|
||||
// create the collection
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create the collection.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Collection)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Collection)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnCollectionAfterCreateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *collectionApi) update(c echo.Context) error {
|
||||
@@ -110,9 +114,9 @@ func (api *collectionApi) update(c echo.Context) error {
|
||||
|
||||
form := forms.NewCollectionUpsert(api.app, collection)
|
||||
|
||||
// read
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.CollectionUpdateEvent{
|
||||
@@ -120,20 +124,24 @@ func (api *collectionApi) update(c echo.Context) error {
|
||||
Collection: collection,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error {
|
||||
// submit
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update the collection.", err)
|
||||
}
|
||||
// update the collection
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update the collection.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Collection)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Collection)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnCollectionAfterUpdateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *collectionApi) delete(c echo.Context) error {
|
||||
|
||||
@@ -297,9 +297,6 @@ func TestCollectionCreate(t *testing.T) {
|
||||
`"name":{"code":"validation_required"`,
|
||||
`"schema":{"code":"validation_required"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + invalid data (eg. existing name)",
|
||||
@@ -315,9 +312,6 @@ func TestCollectionCreate(t *testing.T) {
|
||||
`"name":{"code":"validation_collection_name_exists"`,
|
||||
`"schema":{"0":{"name":{"code":"validation_required"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + valid data",
|
||||
@@ -399,9 +393,6 @@ func TestCollectionUpdate(t *testing.T) {
|
||||
`"data":{`,
|
||||
`"name":{"code":"validation_collection_name_exists"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionBeforeUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + valid data",
|
||||
|
||||
+2
-1
@@ -1,13 +1,14 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestFileDownload(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ package apis
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
|
||||
+25
-3
@@ -77,6 +77,28 @@ func RequireAdminAuth() echo.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdminAuthIfAny middleware requires a request to have
|
||||
// a valid admin Authorization header set (aka. `Authorization: Admin ...`)
|
||||
// ONLY if the application has at least 1 existing Admin model.
|
||||
func RequireAdminAuthOnlyIfAny(app core.App) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
totalAdmins, err := app.Dao().TotalAdmins()
|
||||
if err != nil {
|
||||
return rest.NewBadRequestError("Failed to fetch admins info.", err)
|
||||
}
|
||||
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
|
||||
if admin != nil || totalAdmins == 0 {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
return rest.NewUnauthorizedError("The request requires admin authorization token to be set.", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdminOrUserAuth middleware requires a request to have
|
||||
// a valid admin or user Authorization header set
|
||||
// (aka. `Authorization: Admin ...` or `Authorization: User ...`).
|
||||
@@ -204,11 +226,11 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
|
||||
|
||||
if err != nil {
|
||||
switch v := err.(type) {
|
||||
case (*echo.HTTPError):
|
||||
case *echo.HTTPError:
|
||||
status = v.Code
|
||||
meta["errorMessage"] = v.Message
|
||||
meta["errorDetails"] = fmt.Sprint(v.Internal)
|
||||
case (*rest.ApiError):
|
||||
case *rest.ApiError:
|
||||
status = v.Code
|
||||
meta["errorMessage"] = v.Message
|
||||
meta["errorDetails"] = fmt.Sprint(v.RawData())
|
||||
@@ -259,7 +281,7 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
|
||||
// ---
|
||||
now := time.Now()
|
||||
lastLogsDeletedAt := cast.ToTime(app.Cache().Get("lastLogsDeletedAt"))
|
||||
daysDiff := (now.Sub(lastLogsDeletedAt).Hours() * 24)
|
||||
daysDiff := now.Sub(lastLogsDeletedAt).Hours() * 24
|
||||
|
||||
if daysDiff > float64(app.Settings().Logs.MaxDays) {
|
||||
deleteErr := app.LogsDao().DeleteOldRequests(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays))
|
||||
|
||||
@@ -291,6 +291,125 @@ func TestRequireAdminAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "guest (while having at least 1 existing admin)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.RequireAdminAuthOnlyIfAny(app),
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "guest (while having 0 existing admins)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// delete all admins
|
||||
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.RequireAdminAuthOnlyIfAny(app),
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
{
|
||||
Name: "expired/invalid token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.RequireAdminAuthOnlyIfAny(app),
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "valid user token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.RequireAdminAuthOnlyIfAny(app),
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "valid admin token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.RequireAdminAuthOnlyIfAny(app),
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdminOrUserAuth(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
|
||||
+24
-20
@@ -9,8 +9,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
@@ -159,22 +159,23 @@ func (api *realtimeApi) bindEvents() {
|
||||
adminTable := (&models.Admin{}).TableName()
|
||||
|
||||
// update user/admin auth state
|
||||
api.app.OnModelAfterUpdate().Add(func(data *core.ModelEvent) error {
|
||||
modelTable := data.Model.TableName()
|
||||
api.app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
|
||||
modelTable := e.Model.TableName()
|
||||
|
||||
var contextKey string
|
||||
if modelTable == userTable {
|
||||
switch modelTable {
|
||||
case userTable:
|
||||
contextKey = ContextUserKey
|
||||
} else if modelTable == adminTable {
|
||||
case adminTable:
|
||||
contextKey = ContextAdminKey
|
||||
} else {
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, client := range api.app.SubscriptionsBroker().Clients() {
|
||||
model, _ := client.Get(contextKey).(models.Model)
|
||||
if model != nil && model.GetId() == data.Model.GetId() {
|
||||
client.Set(contextKey, data.Model)
|
||||
if model != nil && model.GetId() == e.Model.GetId() {
|
||||
client.Set(contextKey, e.Model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,21 +183,22 @@ func (api *realtimeApi) bindEvents() {
|
||||
})
|
||||
|
||||
// remove user/admin client(s)
|
||||
api.app.OnModelAfterDelete().Add(func(data *core.ModelEvent) error {
|
||||
modelTable := data.Model.TableName()
|
||||
api.app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error {
|
||||
modelTable := e.Model.TableName()
|
||||
|
||||
var contextKey string
|
||||
if modelTable == userTable {
|
||||
switch modelTable {
|
||||
case userTable:
|
||||
contextKey = ContextUserKey
|
||||
} else if modelTable == adminTable {
|
||||
case adminTable:
|
||||
contextKey = ContextAdminKey
|
||||
} else {
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, client := range api.app.SubscriptionsBroker().Clients() {
|
||||
model, _ := client.Get(contextKey).(models.Model)
|
||||
if model != nil && model.GetId() == data.Model.GetId() {
|
||||
if model != nil && model.GetId() == e.Model.GetId() {
|
||||
api.app.SubscriptionsBroker().Unregister(client.Id())
|
||||
}
|
||||
}
|
||||
@@ -204,18 +206,20 @@ func (api *realtimeApi) bindEvents() {
|
||||
return nil
|
||||
})
|
||||
|
||||
api.app.OnRecordAfterCreateRequest().Add(func(data *core.RecordCreateEvent) error {
|
||||
api.broadcastRecord("create", data.Record)
|
||||
api.app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error {
|
||||
api.broadcastRecord("create", e.Record)
|
||||
return nil
|
||||
})
|
||||
|
||||
api.app.OnRecordAfterUpdateRequest().Add(func(data *core.RecordUpdateEvent) error {
|
||||
api.broadcastRecord("update", data.Record)
|
||||
api.app.OnRecordAfterUpdateRequest().Add(func(e *core.RecordUpdateEvent) error {
|
||||
api.broadcastRecord("update", e.Record)
|
||||
return nil
|
||||
})
|
||||
|
||||
api.app.OnRecordAfterDeleteRequest().Add(func(data *core.RecordDeleteEvent) error {
|
||||
api.broadcastRecord("delete", data.Record)
|
||||
api.app.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error {
|
||||
if record, ok := e.Model.(*models.Record); ok {
|
||||
api.broadcastRecord("delete", record)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+28
-20
@@ -6,8 +6,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
@@ -193,7 +193,7 @@ func (api *recordApi) create(c echo.Context) error {
|
||||
testRecord := models.NewRecord(collection)
|
||||
testForm := forms.NewRecordUpsert(api.app, testRecord)
|
||||
if err := testForm.LoadData(c.Request()); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
|
||||
@@ -210,7 +210,7 @@ func (api *recordApi) create(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := form.LoadData(c.Request()); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.RecordCreateEvent{
|
||||
@@ -218,20 +218,24 @@ func (api *recordApi) create(c echo.Context) error {
|
||||
Record: record,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
|
||||
// create the record
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create record.", err)
|
||||
}
|
||||
// create the record
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create record.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnRecordAfterCreateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *recordApi) update(c echo.Context) error {
|
||||
@@ -276,7 +280,7 @@ func (api *recordApi) update(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := form.LoadData(c.Request()); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.RecordUpdateEvent{
|
||||
@@ -284,20 +288,24 @@ func (api *recordApi) update(c echo.Context) error {
|
||||
Record: record,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
|
||||
// update the record
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update record.", err)
|
||||
}
|
||||
// update the record
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update record.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnRecordAfterUpdateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *recordApi) delete(c echo.Context) error {
|
||||
|
||||
+19
-12
@@ -40,8 +40,10 @@ func (api *settingsApi) list(c echo.Context) error {
|
||||
|
||||
func (api *settingsApi) set(c echo.Context) error {
|
||||
form := forms.NewSettingsUpsert(api.app)
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
|
||||
}
|
||||
|
||||
event := &core.SettingsUpdateEvent{
|
||||
@@ -50,22 +52,27 @@ func (api *settingsApi) set(c echo.Context) error {
|
||||
NewSettings: form.Settings,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error {
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while submitting the form.", err)
|
||||
}
|
||||
// update the settings
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("An error occurred while submitting the form.", err)
|
||||
}
|
||||
|
||||
redactedSettings, err := api.app.Settings().RedactClone()
|
||||
if err != nil {
|
||||
return rest.NewBadRequestError("", err)
|
||||
}
|
||||
redactedSettings, err := api.app.Settings().RedactClone()
|
||||
if err != nil {
|
||||
return rest.NewBadRequestError("", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, redactedSettings)
|
||||
return e.HttpContext.JSON(http.StatusOK, redactedSettings)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnSettingsAfterUpdateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
@@ -139,9 +139,6 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"emailAuth":{"minPasswordLength":{"code":"validation_min_greater_equal_than_required","message":"Must be no less than 5."}}`,
|
||||
`"meta":{"appName":{"code":"validation_required","message":"Cannot be blank."}}`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnSettingsBeforeUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin submitting valid data",
|
||||
|
||||
+38
-30
@@ -152,12 +152,12 @@ func (api *userApi) authMethods(c echo.Context) error {
|
||||
func (api *userApi) oauth2Auth(c echo.Context) error {
|
||||
form := forms.NewUserOauth2Login(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, authData, submitErr := form.Submit()
|
||||
if submitErr != nil {
|
||||
return rest.NewBadRequestError("Failed to authenticated.", submitErr)
|
||||
return rest.NewBadRequestError("Failed to authenticate.", submitErr)
|
||||
}
|
||||
|
||||
return api.authResponse(c, user, authData)
|
||||
@@ -170,7 +170,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
|
||||
|
||||
form := forms.NewUserEmailLogin(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -184,11 +184,11 @@ func (api *userApi) emailAuth(c echo.Context) error {
|
||||
func (api *userApi) requestPasswordReset(c echo.Context) error {
|
||||
form := forms.NewUserPasswordResetRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
|
||||
}
|
||||
|
||||
if err := form.Validate(); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while validating the form.", err)
|
||||
return rest.NewBadRequestError("An error occurred while validating the form.", err)
|
||||
}
|
||||
|
||||
// run in background because we don't need to show
|
||||
@@ -205,7 +205,7 @@ func (api *userApi) requestPasswordReset(c echo.Context) error {
|
||||
func (api *userApi) confirmPasswordReset(c echo.Context) error {
|
||||
form := forms.NewUserPasswordResetConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -224,7 +224,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
|
||||
|
||||
form := forms.NewUserEmailChangeRequest(api.app, loggedUser)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
|
||||
}
|
||||
|
||||
if err := form.Submit(); err != nil {
|
||||
@@ -237,7 +237,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
|
||||
func (api *userApi) confirmEmailChange(c echo.Context) error {
|
||||
form := forms.NewUserEmailChangeConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -251,11 +251,11 @@ func (api *userApi) confirmEmailChange(c echo.Context) error {
|
||||
func (api *userApi) requestVerification(c echo.Context) error {
|
||||
form := forms.NewUserVerificationRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
|
||||
}
|
||||
|
||||
if err := form.Validate(); err != nil {
|
||||
return rest.NewBadRequestError("An error occured while validating the form.", err)
|
||||
return rest.NewBadRequestError("An error occurred while validating the form.", err)
|
||||
}
|
||||
|
||||
// run in background because we don't need to show
|
||||
@@ -272,12 +272,12 @@ func (api *userApi) requestVerification(c echo.Context) error {
|
||||
func (api *userApi) confirmVerification(c echo.Context) error {
|
||||
form := forms.NewUserVerificationConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
if submitErr != nil {
|
||||
return rest.NewBadRequestError("An error occured while submitting the form.", submitErr)
|
||||
return rest.NewBadRequestError("An error occurred while submitting the form.", submitErr)
|
||||
}
|
||||
|
||||
return api.authResponse(c, user, nil)
|
||||
@@ -348,7 +348,7 @@ func (api *userApi) create(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.UserCreateEvent{
|
||||
@@ -356,20 +356,24 @@ func (api *userApi) create(c echo.Context) error {
|
||||
User: user,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error {
|
||||
// create the user
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create user.", err)
|
||||
}
|
||||
// create the user
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to create user.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.User)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.User)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnUserAfterCreateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *userApi) update(c echo.Context) error {
|
||||
@@ -387,7 +391,7 @@ func (api *userApi) update(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
|
||||
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
|
||||
}
|
||||
|
||||
event := &core.UserUpdateEvent{
|
||||
@@ -395,20 +399,24 @@ func (api *userApi) update(c echo.Context) error {
|
||||
User: user,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error {
|
||||
// update the user
|
||||
if err := form.Submit(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update user.", err)
|
||||
}
|
||||
// update the user
|
||||
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
return api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error {
|
||||
if err := next(); err != nil {
|
||||
return rest.NewBadRequestError("Failed to update user.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.User)
|
||||
return e.HttpContext.JSON(http.StatusOK, e.User)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
if submitErr == nil {
|
||||
api.app.OnUserAfterUpdateRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
return submitErr
|
||||
}
|
||||
|
||||
func (api *userApi) delete(c echo.Context) error {
|
||||
|
||||
@@ -748,9 +748,6 @@ func TestUserCreate(t *testing.T) {
|
||||
`"email":{"code":"validation_required"`,
|
||||
`"password":{"code":"validation_required"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnUserBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "invalid data",
|
||||
@@ -764,9 +761,6 @@ func TestUserCreate(t *testing.T) {
|
||||
`"password":{"code":"validation_length_out_of_range"`,
|
||||
`"passwordConfirm":{"code":"validation_values_mismatch"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnUserBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "valid data but with disabled email/pass auth",
|
||||
@@ -868,9 +862,6 @@ func TestUserUpdate(t *testing.T) {
|
||||
`"data":{`,
|
||||
`"email":{"code":"validation_user_email_exists"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnUserBeforeUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin - valid data",
|
||||
|
||||
+10
-83
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -10,14 +9,10 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/migrate"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/acme"
|
||||
@@ -35,18 +30,6 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
Use: "serve",
|
||||
Short: "Starts the web server (default to localhost:8090)",
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
router, err := apis.InitApi(app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// configure cors
|
||||
router.Use(middleware.CORSWithConfig(middleware.CORSConfig(middleware.CORSConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
AllowOrigins: allowedOrigins,
|
||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||
})))
|
||||
|
||||
// ensure that the latest migrations are applied before starting the server
|
||||
if err := runMigrations(app); err != nil {
|
||||
panic(err)
|
||||
@@ -61,19 +44,18 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
color.Yellow("=====================================")
|
||||
}
|
||||
|
||||
// if no admins are found, create the first one
|
||||
totalAdmins, err := app.Dao().TotalAdmins()
|
||||
router, err := apis.InitApi(app)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
return
|
||||
}
|
||||
if totalAdmins == 0 {
|
||||
if err := promptCreateAdmin(app); err != nil {
|
||||
log.Fatalln(err)
|
||||
return
|
||||
}
|
||||
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
|
||||
@@ -149,7 +131,7 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
&httpsAddr,
|
||||
"https",
|
||||
"",
|
||||
"api HTTPS server address (auto TLS via Let's Encrypt)\nthe incomming --http address traffic also will be redirected to this address",
|
||||
"api HTTPS server address (auto TLS via Let's Encrypt)\nthe incoming --http address traffic also will be redirected to this address",
|
||||
)
|
||||
|
||||
return command
|
||||
@@ -171,58 +153,3 @@ func runMigrations(app core.App) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptCreateAdmin(app core.App) error {
|
||||
color.White("-------------------------------------")
|
||||
color.Cyan("Lets create your first admin account:")
|
||||
color.White("-------------------------------------")
|
||||
|
||||
prompts := []*survey.Question{
|
||||
{
|
||||
Name: "Email",
|
||||
Prompt: &survey.Input{Message: "Email:"},
|
||||
Validate: func(val any) error {
|
||||
if err := survey.Required(val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := is.Email.Validate(val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Password",
|
||||
Prompt: &survey.Password{Message: "Pass (min 10 chars):"},
|
||||
Validate: func(val any) error {
|
||||
if str, ok := val.(string); !ok || len(str) < 10 {
|
||||
return errors.New("The password must be at least 10 characters.")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Email string
|
||||
Password string
|
||||
}{}
|
||||
if err := survey.Ask(prompts, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := forms.NewAdminUpsert(app, &models.Admin{})
|
||||
form.Email = result.Email
|
||||
form.Password = result.Password
|
||||
form.PasswordConfirm = result.Password
|
||||
|
||||
if err := form.Submit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Green("Successfully created admin %s!", result.Email)
|
||||
fmt.Println("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Package cmd implements various PocketBase system commands.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewVersionCommand creates and returns new command that prints
|
||||
// the current PocketBase version.
|
||||
func NewVersionCommand(app core.App, version string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the current PocketBase app version",
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
fmt.Printf("PocketBase v%s\n", version)
|
||||
},
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -91,7 +91,7 @@ type App interface {
|
||||
// entry in the DB, allowing you to modify or validate the stored data.
|
||||
OnModelBeforeCreate() *hook.Hook[*ModelEvent]
|
||||
|
||||
// OnModelAfterCreate hook is triggered after successfuly
|
||||
// OnModelAfterCreate hook is triggered after successfully
|
||||
// inserting a new entry in the DB.
|
||||
OnModelAfterCreate() *hook.Hook[*ModelEvent]
|
||||
|
||||
@@ -99,7 +99,7 @@ type App interface {
|
||||
// entry in the DB, allowing you to modify or validate the stored data.
|
||||
OnModelBeforeUpdate() *hook.Hook[*ModelEvent]
|
||||
|
||||
// OnModelAfterUpdate hook is triggered after successfuly updating
|
||||
// OnModelAfterUpdate hook is triggered after successfully updating
|
||||
// existing entry in the DB.
|
||||
OnModelAfterUpdate() *hook.Hook[*ModelEvent]
|
||||
|
||||
@@ -107,7 +107,7 @@ type App interface {
|
||||
// existing entry from the DB.
|
||||
OnModelBeforeDelete() *hook.Hook[*ModelEvent]
|
||||
|
||||
// OnModelAfterDelete is triggered after successfuly deleting an
|
||||
// OnModelAfterDelete is triggered after successfully deleting an
|
||||
// existing entry from the DB.
|
||||
OnModelAfterDelete() *hook.Hook[*ModelEvent]
|
||||
|
||||
@@ -123,7 +123,7 @@ type App interface {
|
||||
OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent]
|
||||
|
||||
// OnMailerAfterAdminResetPasswordSend hook is triggered after
|
||||
// admin password reset email was successfuly sent.
|
||||
// admin password reset email was successfully sent.
|
||||
OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent]
|
||||
|
||||
// OnMailerBeforeUserResetPasswordSend hook is triggered right before
|
||||
@@ -134,7 +134,7 @@ type App interface {
|
||||
OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// OnMailerAfterUserResetPasswordSend hook is triggered after
|
||||
// a user password reset email was successfuly sent.
|
||||
// a user password reset email was successfully sent.
|
||||
OnMailerAfterUserResetPasswordSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// OnMailerBeforeUserVerificationSend hook is triggered right before
|
||||
@@ -145,7 +145,7 @@ type App interface {
|
||||
OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// OnMailerAfterUserVerificationSend hook is triggered after a user
|
||||
// verification email was successfuly sent.
|
||||
// verification email was successfully sent.
|
||||
OnMailerAfterUserVerificationSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// OnMailerBeforeUserChangeEmailSend hook is triggered right before
|
||||
@@ -156,7 +156,7 @@ type App interface {
|
||||
OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// OnMailerAfterUserChangeEmailSend hook is triggered after a user
|
||||
// change address email was successfuly sent.
|
||||
// change address email was successfully sent.
|
||||
OnMailerAfterUserChangeEmailSend() *hook.Hook[*MailerUserEvent]
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -180,7 +180,7 @@ type App interface {
|
||||
// Settings API event hooks
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// OnSettingsListRequest hook is triggered on each successfull
|
||||
// OnSettingsListRequest hook is triggered on each successful
|
||||
// API Settings list request.
|
||||
//
|
||||
// Could be used to validate or modify the response before
|
||||
|
||||
+1
-1
@@ -5,8 +5,8 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
func connectDB(dbPath string) (*dbx.DB, error) {
|
||||
|
||||
+7
-7
@@ -148,12 +148,12 @@ func (s *Settings) Merge(other *Settings) error {
|
||||
}
|
||||
|
||||
// Clone creates a new deep copy of the current settings.
|
||||
func (c *Settings) Clone() (*Settings, error) {
|
||||
new := &Settings{}
|
||||
if err := new.Merge(c); err != nil {
|
||||
func (s *Settings) Clone() (*Settings, error) {
|
||||
settings := &Settings{}
|
||||
if err := settings.Merge(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new, nil
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// RedactClone creates a new deep copy of the current settings,
|
||||
@@ -255,7 +255,7 @@ type S3Config struct {
|
||||
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
|
||||
func (c S3Config) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Endpoint, is.Host, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)),
|
||||
@@ -360,11 +360,11 @@ func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
|
||||
}
|
||||
|
||||
if c.ClientId != "" {
|
||||
provider.SetClientId(string(c.ClientId))
|
||||
provider.SetClientId(c.ClientId)
|
||||
}
|
||||
|
||||
if c.ClientSecret != "" {
|
||||
provider.SetClientSecret(string(c.ClientSecret))
|
||||
provider.SetClientSecret(c.ClientSecret)
|
||||
}
|
||||
|
||||
if c.AuthUrl != "" {
|
||||
|
||||
+13
-1
@@ -315,7 +315,19 @@ func TestS3ConfigValidate(t *testing.T) {
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
// valid data (url endpoint)
|
||||
{
|
||||
core.S3Config{
|
||||
Enabled: true,
|
||||
Endpoint: "https://localhost:8090",
|
||||
Bucket: "test",
|
||||
Region: "test",
|
||||
AccessKey: "test",
|
||||
Secret: "test",
|
||||
},
|
||||
false,
|
||||
},
|
||||
// valid data (hostname endpoint)
|
||||
{
|
||||
core.S3Config{
|
||||
Enabled: true,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func (dao *Dao) FindAdminByEmail(email string) (*models.Admin, error) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// FindAdminByEmail finds the admin associated with the provided JWT token.
|
||||
// FindAdminByToken finds the admin associated with the provided JWT token.
|
||||
//
|
||||
// Returns an error if the JWT token is invalid or expired.
|
||||
func (dao *Dao) FindAdminByToken(token string, baseTokenKey string) (*models.Admin, error) {
|
||||
|
||||
@@ -166,7 +166,6 @@ func TestDeleteCollection(t *testing.T) {
|
||||
t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSaveCollectionCreate(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -309,7 +309,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
newSchema := newCollection.Schema
|
||||
|
||||
// check for renamed table
|
||||
if strings.ToLower(oldTableName) != strings.ToLower(newTableName) {
|
||||
if !strings.EqualFold(oldTableName, newTableName) {
|
||||
_, err := dao.DB().RenameTable(oldTableName, newTableName).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+8
-10
@@ -64,7 +64,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||
}
|
||||
|
||||
// extract the id of the relations to expand
|
||||
relIds := []string{}
|
||||
relIds := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
relIds = append(relIds, record.GetStringSliceDataValue(relField.Name)...)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||
for _, model := range records {
|
||||
relIds := model.GetStringSliceDataValue(relField.Name)
|
||||
|
||||
validRels := []*models.Record{}
|
||||
validRels := make([]*models.Record, 0, len(relIds))
|
||||
for _, id := range relIds {
|
||||
if rel, ok := indexedRels[id]; ok {
|
||||
validRels = append(validRels, rel)
|
||||
@@ -120,20 +120,18 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||
// normalizeExpands normalizes expand strings and merges self containing paths
|
||||
// (eg. ["a.b.c", "a.b", " test ", " ", "test"] -> ["a.b.c", "test"]).
|
||||
func normalizeExpands(paths []string) []string {
|
||||
result := []string{}
|
||||
|
||||
// normalize paths
|
||||
normalized := []string{}
|
||||
normalized := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
p := strings.ReplaceAll(p, " ", "") // replace spaces
|
||||
p = strings.Trim(p, ".") // trim incomplete paths
|
||||
if p == "" {
|
||||
continue
|
||||
p = strings.ReplaceAll(p, " ", "") // replace spaces
|
||||
p = strings.Trim(p, ".") // trim incomplete paths
|
||||
if p != "" {
|
||||
normalized = append(normalized, p)
|
||||
}
|
||||
normalized = append(normalized, p)
|
||||
}
|
||||
|
||||
// merge containing paths
|
||||
result := make([]string, 0, len(normalized))
|
||||
for i, p1 := range normalized {
|
||||
var skip bool
|
||||
for j, p2 := range normalized {
|
||||
|
||||
@@ -396,6 +396,9 @@ func TestSyncRecordTableSchema(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updatedCollection.Name = "demo_renamed"
|
||||
updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("file").Id)
|
||||
updatedCollection.Schema.AddField(
|
||||
|
||||
+3
-3
@@ -43,10 +43,10 @@ func (dao *Dao) LoadProfiles(users []*models.User) error {
|
||||
}
|
||||
|
||||
// extract user ids
|
||||
ids := []string{}
|
||||
ids := make([]string, len(users))
|
||||
usersMap := map[string]*models.User{}
|
||||
for _, user := range users {
|
||||
ids = append(ids, user.Id)
|
||||
for i, user := range users {
|
||||
ids[i] = user.Id
|
||||
usersMap[user.Id] = user
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestAdminPasswordResetConfirmSubmit(t *testing.T) {
|
||||
}
|
||||
|
||||
claims, _ := security.ParseUnverifiedJWT(s.token)
|
||||
tokenAdminId, _ := claims["id"]
|
||||
tokenAdminId := claims["id"]
|
||||
|
||||
if admin.Id != tokenAdminId {
|
||||
t.Errorf("(%d) Expected admin with id %s to be returned, got %v", i, tokenAdminId, admin)
|
||||
|
||||
@@ -74,8 +74,11 @@ func (form *AdminUpsert) checkUniqueEmail(value any) error {
|
||||
return validation.NewError("validation_admin_email_exists", "Admin email already exists.")
|
||||
}
|
||||
|
||||
// Submit validates the form and upserts the form's admin model.
|
||||
func (form *AdminUpsert) Submit() error {
|
||||
// Submit validates the form and upserts the form admin model.
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -87,5 +90,7 @@ func (form *AdminUpsert) Submit() error {
|
||||
form.admin.SetPassword(form.Password)
|
||||
}
|
||||
|
||||
return form.app.Dao().SaveAdmin(form.admin)
|
||||
return runInterceptors(func() error {
|
||||
return form.app.Dao().SaveAdmin(form.admin)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forms_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -252,7 +253,14 @@ func TestAdminUpsertSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
err := form.Submit()
|
||||
interceptorCalls := 0
|
||||
|
||||
err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
})
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
@@ -266,6 +274,14 @@ func TestAdminUpsertSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
expectInterceptorCall := 1
|
||||
if s.expectError {
|
||||
expectInterceptorCall = 0
|
||||
}
|
||||
if interceptorCalls != expectInterceptorCall {
|
||||
t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls)
|
||||
}
|
||||
|
||||
if s.expectError {
|
||||
continue // skip persistence check
|
||||
}
|
||||
@@ -283,3 +299,51 @@ func TestAdminUpsertSubmit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUpsertSubmitInterceptors(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
admin := &models.Admin{}
|
||||
form := forms.NewAdminUpsert(app, admin)
|
||||
form.Email = "test_new@example.com"
|
||||
form.Password = "1234567890"
|
||||
form.PasswordConfirm = form.Password
|
||||
|
||||
testErr := errors.New("test_error")
|
||||
interceptorAdminEmail := ""
|
||||
|
||||
interceptor1Called := false
|
||||
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor1Called = true
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
interceptor2Called := false
|
||||
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorAdminEmail = admin.Email // to check if the record was filled
|
||||
interceptor2Called = true
|
||||
return testErr
|
||||
}
|
||||
}
|
||||
|
||||
err := form.Submit(interceptor1, interceptor2)
|
||||
if err != testErr {
|
||||
t.Fatalf("Expected error %v, got %v", testErr, err)
|
||||
}
|
||||
|
||||
if !interceptor1Called {
|
||||
t.Fatalf("Expected interceptor1 to be called")
|
||||
}
|
||||
|
||||
if !interceptor2Called {
|
||||
t.Fatalf("Expected interceptor2 to be called")
|
||||
}
|
||||
|
||||
if interceptorAdminEmail != form.Email {
|
||||
t.Fatalf("Expected the form model to be filled before calling the interceptors")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Package models implements various services used for request data
|
||||
// validation and applying changes to existing DB models through the app Dao.
|
||||
package forms
|
||||
|
||||
// InterceptorNextFunc is a interceptor handler function.
|
||||
// Usually used in combination with InterceptorFunc.
|
||||
type InterceptorNextFunc = func() error
|
||||
|
||||
// InterceptorFunc defines a single interceptor function that will execute the provided next func handler.
|
||||
type InterceptorFunc func(next InterceptorNextFunc) InterceptorNextFunc
|
||||
|
||||
// runInterceptors executes the provided list of interceptors.
|
||||
func runInterceptors(next InterceptorNextFunc, interceptors ...InterceptorFunc) error {
|
||||
for i := len(interceptors) - 1; i >= 0; i-- {
|
||||
next = interceptors[i](next)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func (form *CollectionUpsert) checkUniqueName(value any) error {
|
||||
return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).")
|
||||
}
|
||||
|
||||
if (form.isCreate || strings.ToLower(v) != strings.ToLower(form.collection.Name)) && form.app.Dao().HasTable(v) {
|
||||
if (form.isCreate || !strings.EqualFold(v, form.collection.Name)) && form.app.Dao().HasTable(v) {
|
||||
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
|
||||
}
|
||||
|
||||
@@ -189,7 +189,10 @@ func (form *CollectionUpsert) checkRule(value any) error {
|
||||
// Submit validates the form and upserts the form's Collection model.
|
||||
//
|
||||
// On success the related record table schema will be auto updated.
|
||||
func (form *CollectionUpsert) Submit() error {
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,5 +214,7 @@ func (form *CollectionUpsert) Submit() error {
|
||||
form.collection.UpdateRule = form.UpdateRule
|
||||
form.collection.DeleteRule = form.DeleteRule
|
||||
|
||||
return form.app.Dao().SaveCollection(form.collection)
|
||||
return runInterceptors(func() error {
|
||||
return form.app.Dao().SaveCollection(form.collection)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forms_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -387,14 +388,31 @@ func TestCollectionUpsertSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
interceptorCalls := 0
|
||||
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
// parse errors
|
||||
result := form.Submit()
|
||||
result := form.Submit(interceptor)
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%d) Failed to parse errors %v", i, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check interceptor calls
|
||||
expectInterceptorCall := 1
|
||||
if len(s.expectedErrors) > 0 {
|
||||
expectInterceptorCall = 0
|
||||
}
|
||||
if interceptorCalls != expectInterceptorCall {
|
||||
t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls)
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
|
||||
@@ -450,3 +468,53 @@ func TestCollectionUpsertSubmit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionUpsertSubmitInterceptors(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := forms.NewCollectionUpsert(app, collection)
|
||||
form.Name = "test_new"
|
||||
|
||||
testErr := errors.New("test_error")
|
||||
interceptorCollectionName := ""
|
||||
|
||||
interceptor1Called := false
|
||||
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor1Called = true
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
interceptor2Called := false
|
||||
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCollectionName = collection.Name // to check if the record was filled
|
||||
interceptor2Called = true
|
||||
return testErr
|
||||
}
|
||||
}
|
||||
|
||||
submitErr := form.Submit(interceptor1, interceptor2)
|
||||
if submitErr != testErr {
|
||||
t.Fatalf("Expected submitError %v, got %v", testErr, submitErr)
|
||||
}
|
||||
|
||||
if !interceptor1Called {
|
||||
t.Fatalf("Expected interceptor1 to be called")
|
||||
}
|
||||
|
||||
if !interceptor2Called {
|
||||
t.Fatalf("Expected interceptor2 to be called")
|
||||
}
|
||||
|
||||
if interceptorCollectionName != form.Name {
|
||||
t.Fatalf("Expected the form model to be filled before calling the interceptors")
|
||||
}
|
||||
}
|
||||
|
||||
+99
-83
@@ -3,6 +3,7 @@ package forms
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -44,7 +45,6 @@ func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
|
||||
form.Data = map[string]any{}
|
||||
for _, field := range record.Collection().Schema.Fields() {
|
||||
form.Data[field.Name] = record.GetDataValue(field.Name)
|
||||
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -147,79 +147,80 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
|
||||
|
||||
for _, field := range form.record.Collection().Schema.Fields() {
|
||||
key := field.Name
|
||||
value, _ := extendedData[key]
|
||||
value := extendedData[key]
|
||||
value = field.PrepareValue(value)
|
||||
|
||||
if field.Type == schema.FieldTypeFile {
|
||||
options, _ := field.Options.(*schema.FileOptions)
|
||||
oldNames := list.ToUniqueStringSlice(form.Data[key])
|
||||
if field.Type != schema.FieldTypeFile {
|
||||
form.Data[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
// delete previously uploaded file(s)
|
||||
if options.MaxSelect == 1 {
|
||||
// search for unset zero indexed key as a fallback
|
||||
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
|
||||
options, _ := field.Options.(*schema.FileOptions)
|
||||
oldNames := list.ToUniqueStringSlice(form.Data[key])
|
||||
|
||||
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") {
|
||||
if len(oldNames) > 0 {
|
||||
form.filesToDelete = append(form.filesToDelete, oldNames...)
|
||||
}
|
||||
form.Data[key] = nil
|
||||
// delete previously uploaded file(s)
|
||||
if options.MaxSelect == 1 {
|
||||
// search for unset zero indexed key as a fallback
|
||||
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
|
||||
|
||||
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") {
|
||||
if len(oldNames) > 0 {
|
||||
form.filesToDelete = append(form.filesToDelete, oldNames...)
|
||||
}
|
||||
} else if options.MaxSelect > 1 {
|
||||
// search for individual file index to delete (eg. "file.0")
|
||||
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
|
||||
indexesToDelete := []int{}
|
||||
for indexedKey := range extendedData {
|
||||
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
|
||||
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
|
||||
if indexErr != nil || index >= len(oldNames) {
|
||||
continue
|
||||
}
|
||||
indexesToDelete = append(indexesToDelete, index)
|
||||
}
|
||||
}
|
||||
|
||||
// slice to fill only with the non-deleted indexes
|
||||
nonDeleted := []string{}
|
||||
for i, name := range oldNames {
|
||||
// not marked for deletion
|
||||
if !list.ExistInSlice(i, indexesToDelete) {
|
||||
nonDeleted = append(nonDeleted, name)
|
||||
form.Data[key] = nil
|
||||
}
|
||||
} else if options.MaxSelect > 1 {
|
||||
// search for individual file index to delete (eg. "file.0")
|
||||
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
|
||||
indexesToDelete := make([]int, 0, len(extendedData))
|
||||
for indexedKey := range extendedData {
|
||||
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
|
||||
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
|
||||
if indexErr != nil || index >= len(oldNames) {
|
||||
continue
|
||||
}
|
||||
|
||||
// store the id to actually delete the file later
|
||||
form.filesToDelete = append(form.filesToDelete, name)
|
||||
indexesToDelete = append(indexesToDelete, index)
|
||||
}
|
||||
form.Data[key] = nonDeleted
|
||||
}
|
||||
|
||||
// check if there are any new uploaded form files
|
||||
files, err := rest.FindUploadedFiles(r, key)
|
||||
if err != nil {
|
||||
continue // skip invalid or missing file(s)
|
||||
}
|
||||
|
||||
// refresh oldNames list
|
||||
oldNames = list.ToUniqueStringSlice(form.Data[key])
|
||||
|
||||
if options.MaxSelect == 1 {
|
||||
// delete previous file(s) before replacing
|
||||
if len(oldNames) > 0 {
|
||||
form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
|
||||
// slice to fill only with the non-deleted indexes
|
||||
nonDeleted := make([]string, 0, len(oldNames))
|
||||
for i, name := range oldNames {
|
||||
// not marked for deletion
|
||||
if !list.ExistInSlice(i, indexesToDelete) {
|
||||
nonDeleted = append(nonDeleted, name)
|
||||
continue
|
||||
}
|
||||
form.filesToUpload = append(form.filesToUpload, files[0])
|
||||
form.Data[key] = files[0].Name()
|
||||
} else if options.MaxSelect > 1 {
|
||||
// append the id of each uploaded file instance
|
||||
form.filesToUpload = append(form.filesToUpload, files...)
|
||||
for _, file := range files {
|
||||
oldNames = append(oldNames, file.Name())
|
||||
}
|
||||
form.Data[key] = oldNames
|
||||
|
||||
// store the id to actually delete the file later
|
||||
form.filesToDelete = append(form.filesToDelete, name)
|
||||
}
|
||||
} else {
|
||||
form.Data[key] = value
|
||||
form.Data[key] = nonDeleted
|
||||
}
|
||||
|
||||
// check if there are any new uploaded form files
|
||||
files, err := rest.FindUploadedFiles(r, key)
|
||||
if err != nil {
|
||||
continue // skip invalid or missing file(s)
|
||||
}
|
||||
|
||||
// refresh oldNames list
|
||||
oldNames = list.ToUniqueStringSlice(form.Data[key])
|
||||
|
||||
if options.MaxSelect == 1 {
|
||||
// delete previous file(s) before replacing
|
||||
if len(oldNames) > 0 {
|
||||
form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
|
||||
}
|
||||
form.filesToUpload = append(form.filesToUpload, files[0])
|
||||
form.Data[key] = files[0].Name()
|
||||
} else if options.MaxSelect > 1 {
|
||||
// append the id of each uploaded file instance
|
||||
form.filesToUpload = append(form.filesToUpload, files...)
|
||||
for _, file := range files {
|
||||
oldNames = append(oldNames, file.Name())
|
||||
}
|
||||
form.Data[key] = oldNames
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +272,10 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
|
||||
}
|
||||
|
||||
// Submit validates the form and upserts the form Record model.
|
||||
func (form *RecordUpsert) Submit() error {
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -281,25 +285,27 @@ func (form *RecordUpsert) Submit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
// persist record model
|
||||
if err := txDao.SaveRecord(form.record); err != nil {
|
||||
return err
|
||||
}
|
||||
return runInterceptors(func() error {
|
||||
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
// persist record model
|
||||
if err := txDao.SaveRecord(form.record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// upload new files (if any)
|
||||
if err := form.processFilesToUpload(); err != nil {
|
||||
return err
|
||||
}
|
||||
// upload new files (if any)
|
||||
if err := form.processFilesToUpload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete old files (if any)
|
||||
if err := form.processFilesToDelete(); err != nil {
|
||||
// for now fail silently to avoid reupload when `form.Submit()`
|
||||
// is called manually (aka. not from an api request)...
|
||||
}
|
||||
// delete old files (if any)
|
||||
if err := form.processFilesToDelete(); err != nil { //nolint:staticcheck
|
||||
// for now fail silently to avoid reupload when `form.Submit()`
|
||||
// is called manually (aka. not from an api request)...
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
func (form *RecordUpsert) processFilesToUpload() error {
|
||||
@@ -317,17 +323,22 @@ func (form *RecordUpsert) processFilesToUpload() error {
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
var uploadErrors []error
|
||||
for i := len(form.filesToUpload) - 1; i >= 0; i-- {
|
||||
file := form.filesToUpload[i]
|
||||
path := form.record.BaseFilesPath() + "/" + file.Name()
|
||||
|
||||
if err := fs.Upload(file.Bytes(), path); err == nil {
|
||||
// remove the uploaded file from the list
|
||||
form.filesToUpload = append(form.filesToUpload[:i], form.filesToUpload[i+1:]...)
|
||||
} else {
|
||||
// store the upload error
|
||||
uploadErrors = append(uploadErrors, fmt.Errorf("File %d: %v", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(form.filesToUpload) > 0 {
|
||||
return errors.New("Failed to upload all files.")
|
||||
if len(uploadErrors) > 0 {
|
||||
return fmt.Errorf("Failed to upload all files: %v", uploadErrors)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -348,20 +359,25 @@ func (form *RecordUpsert) processFilesToDelete() error {
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
var deleteErrors []error
|
||||
for i := len(form.filesToDelete) - 1; i >= 0; i-- {
|
||||
filename := form.filesToDelete[i]
|
||||
path := form.record.BaseFilesPath() + "/" + filename
|
||||
|
||||
if err := fs.Delete(path); err == nil {
|
||||
// remove the deleted file from the list
|
||||
form.filesToDelete = append(form.filesToDelete[:i], form.filesToDelete[i+1:]...)
|
||||
} else {
|
||||
// store the delete error
|
||||
deleteErrors = append(deleteErrors, fmt.Errorf("File %d: %v", i, err))
|
||||
}
|
||||
|
||||
// try to delete the related file thumbs (if any)
|
||||
fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/")
|
||||
}
|
||||
|
||||
if len(form.filesToDelete) > 0 {
|
||||
return errors.New("Failed to delete all files.")
|
||||
if len(deleteErrors) > 0 {
|
||||
return fmt.Errorf("Failed to delete all files: %v", deleteErrors)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package forms_test
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
@@ -29,7 +30,7 @@ func TestNewRecordUpsert(t *testing.T) {
|
||||
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
|
||||
val, _ := form.Data["title"]
|
||||
val := form.Data["title"]
|
||||
if val != "test_value" {
|
||||
t.Errorf("Expected record data to be load, got %v", form.Data)
|
||||
}
|
||||
@@ -400,13 +401,27 @@ func TestRecordUpsertSubmitFailure(t *testing.T) {
|
||||
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
||||
form.LoadData(req)
|
||||
|
||||
interceptorCalls := 0
|
||||
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that validate is triggered
|
||||
// ---
|
||||
result := form.Submit()
|
||||
result := form.Submit(interceptor)
|
||||
if result == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
// check interceptor calls
|
||||
// ---
|
||||
if interceptorCalls != 0 {
|
||||
t.Fatalf("Expected interceptor to be called 0 times, got %d", interceptorCalls)
|
||||
}
|
||||
|
||||
// ensure that the record changes weren't persisted
|
||||
// ---
|
||||
recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id)
|
||||
@@ -451,11 +466,25 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
|
||||
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
||||
form.LoadData(req)
|
||||
|
||||
result := form.Submit()
|
||||
interceptorCalls := 0
|
||||
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
result := form.Submit(interceptor)
|
||||
if result != nil {
|
||||
t.Fatalf("Expected nil, got error %v", result)
|
||||
}
|
||||
|
||||
// check interceptor calls
|
||||
// ---
|
||||
if interceptorCalls != 1 {
|
||||
t.Fatalf("Expected interceptor to be called 1 time, got %d", interceptorCalls)
|
||||
}
|
||||
|
||||
// ensure that the record changes were persisted
|
||||
// ---
|
||||
recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id)
|
||||
@@ -482,6 +511,57 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordUpsertSubmitInterceptors(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, _ := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
form.Data["title"] = "test_new"
|
||||
|
||||
testErr := errors.New("test_error")
|
||||
interceptorRecordTitle := ""
|
||||
|
||||
interceptor1Called := false
|
||||
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor1Called = true
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
interceptor2Called := false
|
||||
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorRecordTitle = record.GetStringDataValue("title") // to check if the record was filled
|
||||
interceptor2Called = true
|
||||
return testErr
|
||||
}
|
||||
}
|
||||
|
||||
submitErr := form.Submit(interceptor1, interceptor2)
|
||||
if submitErr != testErr {
|
||||
t.Fatalf("Expected submitError %v, got %v", testErr, submitErr)
|
||||
}
|
||||
|
||||
if !interceptor1Called {
|
||||
t.Fatalf("Expected interceptor1 to be called")
|
||||
}
|
||||
|
||||
if !interceptor2Called {
|
||||
t.Fatalf("Expected interceptor2 to be called")
|
||||
}
|
||||
|
||||
if interceptorRecordTitle != form.Data["title"].(string) {
|
||||
t.Fatalf("Expected the form model to be filled before calling the interceptors")
|
||||
}
|
||||
}
|
||||
|
||||
func hasRecordFile(app core.App, record *models.Record, filename string) bool {
|
||||
fs, _ := app.NewFilesystem()
|
||||
defer fs.Close()
|
||||
|
||||
+20
-15
@@ -33,27 +33,32 @@ func (form *SettingsUpsert) Validate() error {
|
||||
// Submit validates the form and upserts the loaded settings.
|
||||
//
|
||||
// On success the app settings will be refreshed with the form ones.
|
||||
func (form *SettingsUpsert) Submit() error {
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptionKey := os.Getenv(form.app.EncryptionEnv())
|
||||
|
||||
saveErr := form.app.Dao().SaveParam(
|
||||
models.ParamAppSettings,
|
||||
form.Settings,
|
||||
encryptionKey,
|
||||
)
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
return runInterceptors(func() error {
|
||||
saveErr := form.app.Dao().SaveParam(
|
||||
models.ParamAppSettings,
|
||||
form.Settings,
|
||||
encryptionKey,
|
||||
)
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
|
||||
// explicitly trigger old logs deletion
|
||||
form.app.LogsDao().DeleteOldRequests(
|
||||
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
|
||||
)
|
||||
// explicitly trigger old logs deletion
|
||||
form.app.LogsDao().DeleteOldRequests(
|
||||
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
|
||||
)
|
||||
|
||||
// merge the application settings with the form ones
|
||||
return form.app.Settings().Merge(form.Settings)
|
||||
// merge the application settings with the form ones
|
||||
return form.app.Settings().Merge(form.Settings)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forms_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -98,14 +99,31 @@ func TestSettingsUpsertSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
interceptorCalls := 0
|
||||
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
// parse errors
|
||||
result := form.Submit()
|
||||
result := form.Submit(interceptor)
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%d) Failed to parse errors %v", i, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check interceptor calls
|
||||
expectInterceptorCall := 1
|
||||
if len(s.expectedErrors) > 0 {
|
||||
expectInterceptorCall = 0
|
||||
}
|
||||
if interceptorCalls != expectInterceptorCall {
|
||||
t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls)
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
|
||||
@@ -128,3 +146,42 @@ func TestSettingsUpsertSubmit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsUpsertSubmitInterceptors(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
form := forms.NewSettingsUpsert(app)
|
||||
form.Meta.AppName = "test_new"
|
||||
|
||||
testErr := errors.New("test_error")
|
||||
|
||||
interceptor1Called := false
|
||||
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor1Called = true
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
interceptor2Called := false
|
||||
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor2Called = true
|
||||
return testErr
|
||||
}
|
||||
}
|
||||
|
||||
submitErr := form.Submit(interceptor1, interceptor2)
|
||||
if submitErr != testErr {
|
||||
t.Fatalf("Expected submitError %v, got %v", testErr, submitErr)
|
||||
}
|
||||
|
||||
if !interceptor1Called {
|
||||
t.Fatalf("Expected interceptor1 to be called")
|
||||
}
|
||||
|
||||
if !interceptor2Called {
|
||||
t.Fatalf("Expected interceptor2 to be called")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
// UserEmailChangeConfirm defines a user email change request form.
|
||||
// UserEmailChangeRequest defines a user email change request form.
|
||||
type UserEmailChangeRequest struct {
|
||||
app core.App
|
||||
user *models.User
|
||||
|
||||
+26
-25
@@ -68,7 +68,7 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
config, _ := form.app.Settings().NamedAuthProviderConfigs()[form.Provider]
|
||||
config := form.app.Settings().NamedAuthProviderConfigs()[form.Provider]
|
||||
config.SetupProvider(provider)
|
||||
|
||||
provider.SetRedirectUrl(form.RedirectUrl)
|
||||
@@ -98,35 +98,36 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
|
||||
return nil, authData, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !config.AllowRegistrations {
|
||||
// registration of new users is not allowed via the Oauth2 provider
|
||||
return nil, authData, errors.New("Cannot find user with the authorized email.")
|
||||
}
|
||||
return user, authData, nil
|
||||
}
|
||||
|
||||
// create new user
|
||||
user = &models.User{Verified: true}
|
||||
upsertForm := NewUserUpsert(form.app, user)
|
||||
upsertForm.Email = authData.Email
|
||||
upsertForm.Password = security.RandomString(30)
|
||||
upsertForm.PasswordConfirm = upsertForm.Password
|
||||
if !config.AllowRegistrations {
|
||||
// registration of new users is not allowed via the Oauth2 provider
|
||||
return nil, authData, errors.New("Cannot find user with the authorized email.")
|
||||
}
|
||||
|
||||
event := &core.UserOauth2RegisterEvent{
|
||||
User: user,
|
||||
AuthData: authData,
|
||||
}
|
||||
// create new user
|
||||
user = &models.User{Verified: true}
|
||||
upsertForm := NewUserUpsert(form.app, user)
|
||||
upsertForm.Email = authData.Email
|
||||
upsertForm.Password = security.RandomString(30)
|
||||
upsertForm.PasswordConfirm = upsertForm.Password
|
||||
|
||||
if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
event := &core.UserOauth2RegisterEvent{
|
||||
User: user,
|
||||
AuthData: authData,
|
||||
}
|
||||
|
||||
if err := upsertForm.Submit(); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
|
||||
if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
if err := upsertForm.Submit(); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
|
||||
if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
|
||||
return user, authData, nil
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestUserPasswordResetConfirmSubmit(t *testing.T) {
|
||||
}
|
||||
|
||||
claims, _ := security.ParseUnverifiedJWT(form.Token)
|
||||
tokenUserId, _ := claims["id"]
|
||||
tokenUserId := claims["id"]
|
||||
|
||||
if user.Id != tokenUserId {
|
||||
t.Errorf("(%d) Expected user with id %s, got %v", i, tokenUserId, user)
|
||||
|
||||
@@ -98,7 +98,10 @@ func (form *UserUpsert) checkEmailDomain(value any) error {
|
||||
}
|
||||
|
||||
// Submit validates the form and upserts the form user model.
|
||||
func (form *UserUpsert) Submit() error {
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,5 +117,7 @@ func (form *UserUpsert) Submit() error {
|
||||
|
||||
form.user.Email = form.Email
|
||||
|
||||
return form.app.Dao().SaveUser(form.user)
|
||||
return runInterceptors(func() error {
|
||||
return form.app.Dao().SaveUser(form.user)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forms_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -212,13 +213,28 @@ func TestUserUpsertSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
err := form.Submit()
|
||||
interceptorCalls := 0
|
||||
|
||||
err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorCalls++
|
||||
return next()
|
||||
}
|
||||
})
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
expectInterceptorCall := 1
|
||||
if s.expectError {
|
||||
expectInterceptorCall = 0
|
||||
}
|
||||
if interceptorCalls != expectInterceptorCall {
|
||||
t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls)
|
||||
}
|
||||
|
||||
if s.expectError {
|
||||
continue
|
||||
}
|
||||
@@ -240,3 +256,51 @@ func TestUserUpsertSubmit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserUpsertSubmitInterceptors(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
user := &models.User{}
|
||||
form := forms.NewUserUpsert(app, user)
|
||||
form.Email = "test_new@example.com"
|
||||
form.Password = "1234567890"
|
||||
form.PasswordConfirm = form.Password
|
||||
|
||||
testErr := errors.New("test_error")
|
||||
interceptorUserEmail := ""
|
||||
|
||||
interceptor1Called := false
|
||||
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptor1Called = true
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
interceptor2Called := false
|
||||
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
|
||||
return func() error {
|
||||
interceptorUserEmail = user.Email // to check if the record was filled
|
||||
interceptor2Called = true
|
||||
return testErr
|
||||
}
|
||||
}
|
||||
|
||||
err := form.Submit(interceptor1, interceptor2)
|
||||
if err != testErr {
|
||||
t.Fatalf("Expected error %v, got %v", testErr, err)
|
||||
}
|
||||
|
||||
if !interceptor1Called {
|
||||
t.Fatalf("Expected interceptor1 to be called")
|
||||
}
|
||||
|
||||
if !interceptor2Called {
|
||||
t.Fatalf("Expected interceptor2 to be called")
|
||||
}
|
||||
|
||||
if interceptorUserEmail != form.Email {
|
||||
t.Fatalf("Expected the form model to be filled before calling the interceptors")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestUserVerificationConfirmSubmit(t *testing.T) {
|
||||
}
|
||||
|
||||
claims, _ := security.ParseUnverifiedJWT(form.Token)
|
||||
tokenUserId, _ := claims["id"]
|
||||
tokenUserId := claims["id"]
|
||||
|
||||
if user.Id != tokenUserId {
|
||||
t.Errorf("(%d) Expected user.Id %q, got %q", i, tokenUserId, user.Id)
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
@@ -318,12 +318,10 @@ func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField,
|
||||
}
|
||||
|
||||
// extract the uploaded files
|
||||
files := []*rest.UploadedFile{}
|
||||
if len(validator.uploadedFiles) > 0 {
|
||||
for _, file := range validator.uploadedFiles {
|
||||
if list.ExistInSlice(file.Name(), names) {
|
||||
files = append(files, file)
|
||||
}
|
||||
files := make([]*rest.UploadedFile, 0, len(validator.uploadedFiles))
|
||||
for _, file := range validator.uploadedFiles {
|
||||
if list.ExistInSlice(file.Name(), names) {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1189,7 +1189,7 @@ func TestRecordDataValidatorValidateRelation(t *testing.T) {
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{
|
||||
MaxSelect: 3,
|
||||
CollectionId: "", // missing or nonexisting colleciton id
|
||||
CollectionId: "", // missing or non-existing collection id
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
run:
|
||||
go: 1.18
|
||||
concurrency: 4
|
||||
timeout: 10m
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- deadcode
|
||||
- depguard
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- gofmt
|
||||
- goimports
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
@@ -3,6 +3,8 @@ package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
@@ -20,7 +22,14 @@ func Register(
|
||||
down func(db dbx.Builder) error,
|
||||
optFilename ...string,
|
||||
) {
|
||||
AppMigrations.Register(up, down, optFilename...)
|
||||
var optFiles []string
|
||||
if len(optFilename) > 0 {
|
||||
optFiles = optFilename
|
||||
} else {
|
||||
_, path, _, _ := runtime.Caller(1)
|
||||
optFiles = append(optFiles, filepath.Base(path))
|
||||
}
|
||||
AppMigrations.Register(up, down, optFiles...)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ func (m *BaseModel) GetCreated() types.DateTime {
|
||||
return m.Created
|
||||
}
|
||||
|
||||
// GetCreated returns the model's Updated datetime.
|
||||
// GetUpdated returns the model's Updated datetime.
|
||||
func (m *BaseModel) GetUpdated() types.DateTime {
|
||||
return m.Updated
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (m *BaseModel) RefreshCreated() {
|
||||
m.Created = types.NowDateTime()
|
||||
}
|
||||
|
||||
// RefreshCreated updates the model's Created field with the current datetime.
|
||||
// RefreshUpdated updates the model's Created field with the current datetime.
|
||||
func (m *BaseModel) RefreshUpdated() {
|
||||
m.Updated = types.NowDateTime()
|
||||
}
|
||||
|
||||
+5
-5
@@ -68,21 +68,21 @@ func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap)
|
||||
// NewRecordsFromNullStringMaps initializes a new Record model for
|
||||
// each row in the provided NullStringMap slice.
|
||||
func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record {
|
||||
result := []*Record{}
|
||||
result := make([]*Record, len(rows))
|
||||
|
||||
for _, row := range rows {
|
||||
result = append(result, NewRecordFromNullStringMap(collection, row))
|
||||
for i, row := range rows {
|
||||
result[i] = NewRecordFromNullStringMap(collection, row)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the table name associated to the current Record model.
|
||||
// TableName returns the table name associated to the current Record model.
|
||||
func (m *Record) TableName() string {
|
||||
return m.collection.Name
|
||||
}
|
||||
|
||||
// Returns the Collection model associated to the current Record model.
|
||||
// Collection returns the Collection model associated to the current Record model.
|
||||
func (m *Record) Collection() *Collection {
|
||||
return m.collection
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (s *Schema) AddField(newField *SchemaField) {
|
||||
// checks for invalid renamed fields and field name duplications.
|
||||
func (s Schema) Validate() error {
|
||||
return validation.Validate(&s.fields, validation.Required, validation.By(func(value any) error {
|
||||
fields := s.fields // use directly the schema value to avoid unnecesary interface casting
|
||||
fields := s.fields // use directly the schema value to avoid unnecessary interface casting
|
||||
|
||||
if len(fields) == 0 {
|
||||
return validation.NewError("validation_invalid_schema", "Invalid schema format.")
|
||||
|
||||
@@ -493,9 +493,9 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
value any
|
||||
expectJson string
|
||||
}{
|
||||
{schema.SchemaField{Type: "unkown"}, "test", `"test"`},
|
||||
{schema.SchemaField{Type: "unkown"}, 123, "123"},
|
||||
{schema.SchemaField{Type: "unkown"}, []int{1, 2, 1}, "[1,2,1]"},
|
||||
{schema.SchemaField{Type: "unknown"}, "test", `"test"`},
|
||||
{schema.SchemaField{Type: "unknown"}, 123, "123"},
|
||||
{schema.SchemaField{Type: "unknown"}, []int{1, 2, 1}, "[1,2,1]"},
|
||||
|
||||
// text
|
||||
{schema.SchemaField{Type: schema.FieldTypeText}, nil, `null`},
|
||||
@@ -1281,7 +1281,6 @@ func TestFileOptionsValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRelationOptionsValidate(t *testing.T) {
|
||||
|
||||
scenarios := []fieldOptionsScenario{
|
||||
{
|
||||
"empty",
|
||||
|
||||
+2
-2
@@ -9,10 +9,10 @@ import (
|
||||
var _ Model = (*User)(nil)
|
||||
|
||||
const (
|
||||
// The name of the system user profiles collection.
|
||||
// ProfileCollectionName is the name of the system user profiles collection.
|
||||
ProfileCollectionName = "profiles"
|
||||
|
||||
// The name of the user field from the system user profiles collection.
|
||||
// ProfileCollectionUserFieldName is the name of the user field from the system user profiles collection.
|
||||
ProfileCollectionUserFieldName = "userId"
|
||||
)
|
||||
|
||||
|
||||
+10
-9
@@ -17,7 +17,7 @@ import (
|
||||
var _ core.App = (*PocketBase)(nil)
|
||||
|
||||
// Version of PocketBase
|
||||
const Version = "0.1.0"
|
||||
var Version = "(untracked)"
|
||||
|
||||
// appWrapper serves as a private core.App instance wrapper.
|
||||
type appWrapper struct {
|
||||
@@ -51,9 +51,9 @@ type PocketBase struct {
|
||||
// New creates a new PocketBase instance.
|
||||
//
|
||||
// Note that the application will not be initialized/bootstrapped yet,
|
||||
// aka. DB connections, migrations, app settings, etc. will not be accessable.
|
||||
// aka. DB connections, migrations, app settings, etc. will not be accessible.
|
||||
// Everything will be initialized when Start() is executed.
|
||||
// If you want initialize the application before calling Start(),
|
||||
// If you want to initialize the application before calling Start(),
|
||||
// then you'll have to manually call Bootstrap().
|
||||
func New() *PocketBase {
|
||||
// try to find the base executable directory and how it was run
|
||||
@@ -73,11 +73,16 @@ func New() *PocketBase {
|
||||
|
||||
pb := &PocketBase{
|
||||
RootCmd: &cobra.Command{
|
||||
Use: "pocketbase",
|
||||
Short: "PocketBase CLI",
|
||||
Use: "pocketbase",
|
||||
Short: "PocketBase CLI",
|
||||
Version: Version,
|
||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||
UnknownFlags: true,
|
||||
},
|
||||
// no need to provide the default cobra completion command
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
},
|
||||
defaultDebug: withGoRun,
|
||||
defaultDataDir: defaultDir,
|
||||
@@ -85,9 +90,6 @@ func New() *PocketBase {
|
||||
showStartBanner: true,
|
||||
}
|
||||
|
||||
// no need to provide the default cobra completion command
|
||||
pb.RootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||
|
||||
// parse base flags
|
||||
// (errors are ignored, since the full flags parsing happens on Execute())
|
||||
pb.eagerParseFlags()
|
||||
@@ -130,7 +132,6 @@ func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
|
||||
func (pb *PocketBase) Start() error {
|
||||
// register system commands
|
||||
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, pb.showStartBanner))
|
||||
pb.RootCmd.AddCommand(cmd.NewVersionCommand(pb, Version))
|
||||
pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb))
|
||||
|
||||
return pb.Execute()
|
||||
|
||||
@@ -168,7 +168,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
|
||||
}
|
||||
newCollectionName := relCollection.Name
|
||||
newTableAlias := (currentTableAlias + "_" + field.Name)
|
||||
newTableAlias := currentTableAlias + "_" + field.Name
|
||||
|
||||
r.addJoin(
|
||||
newCollectionName,
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(scenario.Method, scenario.Url, scenario.Body)
|
||||
|
||||
// add middeware to timeout long running requests (eg. keep-alive routes)
|
||||
// add middleware to timeout long-running requests (eg. keep-alive routes)
|
||||
e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx, cancelFunc := context.WithTimeout(c.Request().Context(), 100*time.Millisecond)
|
||||
@@ -109,7 +109,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
}
|
||||
|
||||
for event, expectedCalls := range scenario.ExpectedEvents {
|
||||
actualCalls, _ := testApp.EventCalls[event]
|
||||
actualCalls := testApp.EventCalls[event]
|
||||
if actualCalls != expectedCalls {
|
||||
t.Errorf("[%s] Expected event %s to be called %d, got %d", prefix, event, expectedCalls, actualCalls)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Pacakge tests provides common helpers and mocks used in PocketBase application tests.
|
||||
// Package tests provides common helpers and mocks used in PocketBase application tests.
|
||||
package tests
|
||||
|
||||
import (
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ type Provider interface {
|
||||
// SetClientId sets the provider client's ID.
|
||||
SetClientId(clientId string)
|
||||
|
||||
// ClientId returns the provider client's app secret.
|
||||
// ClientSecret returns the provider client's app secret.
|
||||
ClientSecret() string
|
||||
|
||||
// SetClientSecret sets the provider client's app secret.
|
||||
|
||||
@@ -127,7 +127,7 @@ func (s *System) DeletePrefix(prefix string) []error {
|
||||
Prefix: prefix,
|
||||
}
|
||||
|
||||
// delete all files witht the prefix
|
||||
// delete all files with the prefix
|
||||
// ---
|
||||
iter := s.bucket.List(&opts)
|
||||
for {
|
||||
@@ -182,14 +182,18 @@ func (s *System) Serve(response http.ResponseWriter, fileKey string, name string
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
|
||||
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
|
||||
location, _ := time.LoadLocation("GMT")
|
||||
|
||||
response.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
response.Header().Set("Content-Type", r.ContentType())
|
||||
response.Header().Set("Content-Length", strconv.FormatInt(r.Size(), 10))
|
||||
response.Header().Set("Last-Modified", r.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST"))
|
||||
|
||||
// All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
|
||||
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
|
||||
//
|
||||
// NB! time.LoadLocation may fail on non-Unix systems (see https://github.com/pocketbase/pocketbase/issues/45)
|
||||
location, locationErr := time.LoadLocation("GMT")
|
||||
if locationErr == nil {
|
||||
response.Header().Set("Last-Modified", r.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST"))
|
||||
}
|
||||
|
||||
// copy from the read range to response.
|
||||
_, err := io.Copy(response, r)
|
||||
|
||||
+5
-2
@@ -44,8 +44,11 @@ func (h *Hook[T]) Reset() {
|
||||
// - any non-nil error is returned in one of the handlers
|
||||
func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error {
|
||||
h.mux.Lock()
|
||||
handlers := append(h.handlers, oneOffHandlers...)
|
||||
h.mux.Unlock() // unlock is not deferred to avoid deadlocks when Trigger is called recursive in the handlers
|
||||
handlers := make([]Handler[T], 0, len(h.handlers)+len(oneOffHandlers))
|
||||
handlers = append(handlers, h.handlers...)
|
||||
handlers = append(handlers, oneOffHandlers...)
|
||||
// unlock is not deferred to avoid deadlocks when Trigger is called recursive by the handlers
|
||||
h.mux.Unlock()
|
||||
|
||||
for _, fn := range handlers {
|
||||
err := fn(data)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
var columnifyRemoveRegex = regexp.MustCompile(`[^\w\.\*\-\_\@\#]+`)
|
||||
var snakecaseSplitRegex = regexp.MustCompile(`[\W_]+`)
|
||||
var usernamifySplitRegex = regexp.MustCompile(`\W+`)
|
||||
|
||||
// UcFirst converts the first character of a string into uppercase.
|
||||
func UcFirst(str string) string {
|
||||
@@ -85,34 +84,3 @@ func Snakecase(str string) string {
|
||||
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// Usernamify generates a properly formatted username from the provided string.
|
||||
// Returns "unknown" if `str` is empty or contains only non word characters.
|
||||
//
|
||||
// ```go
|
||||
// Usernamify("John Doe, hello") // "john.doe.hello"
|
||||
// ```
|
||||
func Usernamify(str string) string {
|
||||
// split at any non word character
|
||||
words := usernamifySplitRegex.Split(strings.ToLower(str), -1)
|
||||
|
||||
// concatenate any non empty word with a dot
|
||||
var result strings.Builder
|
||||
for _, word := range words {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if result.Len() > 0 {
|
||||
result.WriteString(".")
|
||||
}
|
||||
|
||||
result.WriteString(word)
|
||||
}
|
||||
|
||||
if result.Len() == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestSanitize(t *testing.T) {
|
||||
{" ", ` `, "", false},
|
||||
{"", `[A-Z]`, "", false},
|
||||
{"abcABC", `[A-Z]`, "abc", false},
|
||||
{"abcABC", `[A-Z`, "", true}, // invlid pattern
|
||||
{"abcABC", `[A-Z`, "", true}, // invalid pattern
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
@@ -128,26 +128,3 @@ func TestSnakecase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsernamify(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", "unknown"},
|
||||
{" ", "unknown"},
|
||||
{"!@#$%^", "unknown"},
|
||||
{"...", "unknown"},
|
||||
{"_", "_"}, // underscore is valid word character
|
||||
{"John Doe", "john.doe"},
|
||||
{"John_Doe", "john_doe"},
|
||||
{".a!b@c#d$e%123. ", "a.b.c.d.e.123"},
|
||||
{"Hello, world", "hello.world"},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
if result := inflector.Usernamify(scenario.val); result != scenario.expected {
|
||||
t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-29
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
var cachedPatterns = map[string]*regexp.Regexp{}
|
||||
|
||||
// ExustInSlice checks whether a comparable element exists in a slice of the same type.
|
||||
// ExistInSlice checks whether a comparable element exists in a slice of the same type.
|
||||
func ExistInSlice[T comparable](item T, list []T) bool {
|
||||
if len(list) == 0 {
|
||||
return false
|
||||
@@ -38,21 +38,23 @@ func ExistInSliceWithRegex(str string, list []string) bool {
|
||||
if str == field {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// check for regex match
|
||||
pattern, ok := cachedPatterns[field]
|
||||
if !ok {
|
||||
var patternErr error
|
||||
pattern, patternErr = regexp.Compile(field)
|
||||
if patternErr != nil {
|
||||
continue
|
||||
}
|
||||
// "cache" the pattern to avoid compiling it every time
|
||||
cachedPatterns[field] = pattern
|
||||
}
|
||||
if pattern != nil && pattern.MatchString(str) {
|
||||
return true
|
||||
continue
|
||||
}
|
||||
|
||||
// check for regex match
|
||||
pattern, ok := cachedPatterns[field]
|
||||
if !ok {
|
||||
var err error
|
||||
pattern, err = regexp.Compile(field)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// "cache" the pattern to avoid compiling it every time
|
||||
cachedPatterns[field] = pattern
|
||||
}
|
||||
|
||||
if pattern != nil && pattern.MatchString(str) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,46 +74,45 @@ func ToInterfaceSlice[T any](list []T) []any {
|
||||
|
||||
// NonzeroUniques returns only the nonzero unique values from a slice.
|
||||
func NonzeroUniques[T comparable](list []T) []T {
|
||||
result := []T{}
|
||||
existMap := map[T]bool{}
|
||||
result := make([]T, 0, len(list))
|
||||
existMap := make(map[T]struct{}, len(list))
|
||||
|
||||
var zeroVal T
|
||||
|
||||
for _, val := range list {
|
||||
if !existMap[val] && val != zeroVal {
|
||||
existMap[val] = true
|
||||
result = append(result, val)
|
||||
if _, ok := existMap[val]; ok || val == zeroVal {
|
||||
continue
|
||||
}
|
||||
existMap[val] = struct{}{}
|
||||
result = append(result, val)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ToUniqueStringSlice casts `value` to a slice of non-zero unique strings.
|
||||
func ToUniqueStringSlice(value any) []string {
|
||||
strings := []string{}
|
||||
|
||||
func ToUniqueStringSlice(value any) (result []string) {
|
||||
switch val := value.(type) {
|
||||
case nil:
|
||||
// nothing to cast
|
||||
case []string:
|
||||
strings = val
|
||||
result = val
|
||||
case string:
|
||||
if val == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// check if it is a json encoded array of strings
|
||||
if err := json.Unmarshal([]byte(val), &strings); err != nil {
|
||||
if err := json.Unmarshal([]byte(val), &result); err != nil {
|
||||
// not a json array, just add the string as single array element
|
||||
strings = append(strings, val)
|
||||
result = append(result, val)
|
||||
}
|
||||
case json.Marshaler: // eg. JsonArray
|
||||
raw, _ := val.MarshalJSON()
|
||||
json.Unmarshal(raw, &strings)
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
default:
|
||||
strings = cast.ToStringSlice(value)
|
||||
result = cast.ToStringSlice(value)
|
||||
}
|
||||
|
||||
return NonzeroUniques(strings)
|
||||
return NonzeroUniques(result)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ func NewSmtpClient(
|
||||
// SmtpClient defines a SMTP mail client structure that implements
|
||||
// `mailer.Mailer` interface.
|
||||
type SmtpClient struct {
|
||||
mail *mailyak.MailYak
|
||||
|
||||
host string
|
||||
port int
|
||||
username string
|
||||
|
||||
+25
-42
@@ -27,21 +27,16 @@ var parsedFilterData = store.New(make(map[string][]fexpr.ExprGroup, 50))
|
||||
// BuildExpr parses the current filter data and returns a new db WHERE expression.
|
||||
func (f FilterData) BuildExpr(fieldResolver FieldResolver) (dbx.Expression, error) {
|
||||
raw := string(f)
|
||||
var data []fexpr.ExprGroup
|
||||
|
||||
if parsedFilterData.Has(raw) {
|
||||
data = parsedFilterData.Get(raw)
|
||||
} else {
|
||||
var err error
|
||||
data, err = fexpr.Parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// store in cache
|
||||
// (the limit size is arbitrary and it is there to prevent the cache growing too big)
|
||||
parsedFilterData.SetIfLessThanLimit(raw, data, 500)
|
||||
return f.build(parsedFilterData.Get(raw), fieldResolver)
|
||||
}
|
||||
|
||||
data, err := fexpr.Parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// store in cache
|
||||
// (the limit size is arbitrary and it is there to prevent the cache growing too big)
|
||||
parsedFilterData.SetIfLessThanLimit(raw, data, 500)
|
||||
return f.build(data, fieldResolver)
|
||||
}
|
||||
|
||||
@@ -94,15 +89,11 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
|
||||
|
||||
// merge both operands parameters (if any)
|
||||
params := dbx.Params{}
|
||||
if len(lParams) > 0 {
|
||||
for k, v := range lParams {
|
||||
params[k] = v
|
||||
}
|
||||
for k, v := range lParams {
|
||||
params[k] = v
|
||||
}
|
||||
if len(rParams) > 0 {
|
||||
for k, v := range rParams {
|
||||
params[k] = v
|
||||
}
|
||||
for k, v := range rParams {
|
||||
params[k] = v
|
||||
}
|
||||
|
||||
switch expr.Op {
|
||||
@@ -144,35 +135,31 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
|
||||
}
|
||||
|
||||
func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver) (name string, params dbx.Params, err error) {
|
||||
if token.Type == fexpr.TokenIdentifier {
|
||||
switch token.Type {
|
||||
case fexpr.TokenIdentifier:
|
||||
name, params, err := fieldResolver.Resolve(token.Literal)
|
||||
|
||||
if name == "" || err != nil {
|
||||
// if `null` field is missing, treat `null` identifier as NULL token
|
||||
if strings.ToLower(token.Literal) == "null" {
|
||||
return "NULL", nil, nil
|
||||
m := map[string]string{
|
||||
// if `null` field is missing, treat `null` identifier as NULL token
|
||||
"null": "NULL",
|
||||
// if `true` field is missing, treat `true` identifier as TRUE token
|
||||
"true": "1",
|
||||
// if `false` field is missing, treat `false` identifier as FALSE token
|
||||
"false": "0",
|
||||
}
|
||||
|
||||
// if `true` field is missing, treat `true` identifier as TRUE token
|
||||
if strings.ToLower(token.Literal) == "true" {
|
||||
return "1", nil, nil
|
||||
if v, ok := m[strings.ToLower(token.Literal)]; ok {
|
||||
return v, nil, nil
|
||||
}
|
||||
|
||||
// if `false` field is missing, treat `false` identifier as FALSE token
|
||||
if strings.ToLower(token.Literal) == "false" {
|
||||
return "0", nil, nil
|
||||
}
|
||||
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return name, params, err
|
||||
}
|
||||
|
||||
if token.Type == fexpr.TokenNumber || token.Type == fexpr.TokenText {
|
||||
case fexpr.TokenNumber, fexpr.TokenText:
|
||||
placeholder := "t" + security.RandomString(7)
|
||||
name := fmt.Sprintf("{:%s}", placeholder)
|
||||
params := dbx.Params{placeholder: token.Literal}
|
||||
|
||||
return name, params, nil
|
||||
}
|
||||
|
||||
@@ -182,10 +169,6 @@ func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver)
|
||||
func (f FilterData) normalizeLikeParams(params dbx.Params) dbx.Params {
|
||||
result := dbx.Params{}
|
||||
|
||||
if len(params) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for k, v := range params {
|
||||
vStr := cast.ToString(v)
|
||||
if !strings.Contains(vStr, "%") {
|
||||
|
||||
+21
-30
@@ -121,8 +121,7 @@ func (s *Provider) Parse(urlQuery string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
rawPage := params.Get(PageQueryParam)
|
||||
if rawPage != "" {
|
||||
if rawPage := params.Get(PageQueryParam); rawPage != "" {
|
||||
page, err := strconv.Atoi(rawPage)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,8 +129,7 @@ func (s *Provider) Parse(urlQuery string) error {
|
||||
s.Page(page)
|
||||
}
|
||||
|
||||
rawPerPage := params.Get(PerPageQueryParam)
|
||||
if rawPerPage != "" {
|
||||
if rawPerPage := params.Get(PerPageQueryParam); rawPerPage != "" {
|
||||
perPage, err := strconv.Atoi(rawPerPage)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -139,15 +137,13 @@ func (s *Provider) Parse(urlQuery string) error {
|
||||
s.PerPage(perPage)
|
||||
}
|
||||
|
||||
rawSort := params.Get(SortQueryParam)
|
||||
if rawSort != "" {
|
||||
if rawSort := params.Get(SortQueryParam); rawSort != "" {
|
||||
for _, sortField := range ParseSortFromString(rawSort) {
|
||||
s.AddSort(sortField)
|
||||
}
|
||||
}
|
||||
|
||||
rawFilter := params.Get(FilterQueryParam)
|
||||
if rawFilter != "" {
|
||||
if rawFilter := params.Get(FilterQueryParam); rawFilter != "" {
|
||||
s.AddFilter(FilterData(rawFilter))
|
||||
}
|
||||
|
||||
@@ -165,35 +161,30 @@ func (s *Provider) Exec(items any) (*Result, error) {
|
||||
modelsQuery := *s.query
|
||||
|
||||
// apply filters
|
||||
if len(s.filter) > 0 {
|
||||
for _, f := range s.filter {
|
||||
expr, err := f.BuildExpr(s.fieldResolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expr != nil {
|
||||
modelsQuery.AndWhere(expr)
|
||||
}
|
||||
for _, f := range s.filter {
|
||||
expr, err := f.BuildExpr(s.fieldResolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expr != nil {
|
||||
modelsQuery.AndWhere(expr)
|
||||
}
|
||||
}
|
||||
|
||||
// apply sorting
|
||||
if len(s.sort) > 0 {
|
||||
for _, sortField := range s.sort {
|
||||
expr, err := sortField.BuildExpr(s.fieldResolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expr != "" {
|
||||
modelsQuery.AndOrderBy(expr)
|
||||
}
|
||||
for _, sortField := range s.sort {
|
||||
expr, err := sortField.BuildExpr(s.fieldResolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expr != "" {
|
||||
modelsQuery.AndOrderBy(expr)
|
||||
}
|
||||
}
|
||||
|
||||
// apply field resolver query modifications (if any)
|
||||
updateQueryErr := s.fieldResolver.UpdateQuery(&modelsQuery)
|
||||
if updateQueryErr != nil {
|
||||
return nil, updateQueryErr
|
||||
if err := s.fieldResolver.UpdateQuery(&modelsQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// count
|
||||
@@ -210,7 +201,7 @@ func (s *Provider) Exec(items any) (*Result, error) {
|
||||
s.perPage = MaxPerPage
|
||||
}
|
||||
|
||||
// normalize page accoring to the total count
|
||||
// normalize page according to the total count
|
||||
if s.page <= 0 || totalCount == 0 {
|
||||
s.page = 1
|
||||
} else if totalPages := int(math.Ceil(float64(totalCount) / float64(s.perPage))); s.page > totalPages {
|
||||
|
||||
+4
-12
@@ -34,26 +34,18 @@ func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {
|
||||
//
|
||||
// Example:
|
||||
// fields := search.ParseSortFromString("-name,+created")
|
||||
func ParseSortFromString(str string) []SortField {
|
||||
result := []SortField{}
|
||||
|
||||
func ParseSortFromString(str string) (fields []SortField) {
|
||||
data := strings.Split(str, ",")
|
||||
|
||||
for _, field := range data {
|
||||
// trim whitespaces
|
||||
field = strings.TrimSpace(field)
|
||||
|
||||
var dir string
|
||||
if strings.HasPrefix(field, "-") {
|
||||
dir = SortDesc
|
||||
field = strings.TrimPrefix(field, "-")
|
||||
fields = append(fields, SortField{strings.TrimPrefix(field, "-"), SortDesc})
|
||||
} else {
|
||||
dir = SortAsc
|
||||
field = strings.TrimPrefix(field, "+")
|
||||
fields = append(fields, SortField{strings.TrimPrefix(field, "+"), SortAsc})
|
||||
}
|
||||
|
||||
result = append(result, SortField{field, dir})
|
||||
}
|
||||
|
||||
return result
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,10 +66,5 @@ func Decrypt(cipherText string, key string) ([]byte, error) {
|
||||
}
|
||||
|
||||
nonce, cipherByteClean := cipherByte[:nonceSize], cipherByte[nonceSize:]
|
||||
plainData, err := gcm.Open(nil, nonce, cipherByteClean, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plainData, nil
|
||||
return gcm.Open(nil, nonce, cipherByteClean, nil)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ func ParseUnverifiedJWT(token string) (jwt.MapClaims, error) {
|
||||
|
||||
// ParseJWT verifies and parses JWT token and returns its claims.
|
||||
func ParseJWT(token string, verificationKey string) (jwt.MapClaims, error) {
|
||||
parser := &jwt.Parser{
|
||||
ValidMethods: []string{"HS256"},
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}))
|
||||
|
||||
parsedToken, err := parser.Parse(token, func(t *jwt.Token) (any, error) {
|
||||
return []byte(verificationKey), nil
|
||||
@@ -50,10 +48,8 @@ func NewToken(payload jwt.MapClaims, signingKey string, secondsDuration int64) (
|
||||
"exp": time.Now().Add(seconds).Unix(),
|
||||
}
|
||||
|
||||
if len(payload) > 0 {
|
||||
for k, v := range payload {
|
||||
claims[k] = v
|
||||
}
|
||||
for k, v := range payload {
|
||||
claims[k] = v
|
||||
}
|
||||
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(signingKey))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
generated := []string{}
|
||||
reg := regexp.MustCompile(`[a-zA-Z0-9]+`)
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
length := 5 + i
|
||||
@@ -18,7 +19,7 @@ func TestRandomString(t *testing.T) {
|
||||
t.Errorf("(%d) Expected the length of the string to be %d, got %d", i, length, len(result))
|
||||
}
|
||||
|
||||
if match, _ := regexp.MatchString("[a-zA-Z0-9]+", result); !match {
|
||||
if match := reg.MatchString(result); !match {
|
||||
t.Errorf("(%d) The generated strings should have only [a-zA-Z0-9]+ characters, got %q", i, result)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,14 @@ func New[T any](data map[string]T) *Store[T] {
|
||||
return &Store[T]{data: data}
|
||||
}
|
||||
|
||||
// RemoveAll removes all the existing store entries.
|
||||
func (s *Store[T]) RemoveAll() {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
s.data = make(map[string]T)
|
||||
}
|
||||
|
||||
// Remove removes a single entry from the store.
|
||||
//
|
||||
// Remove does nothing if key doesn't exist in the store.
|
||||
|
||||
@@ -14,6 +14,20 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveAll(t *testing.T) {
|
||||
s := store.New(map[string]bool{"test1": true, "test2": true})
|
||||
|
||||
keys := []string{"test1", "test2"}
|
||||
|
||||
s.RemoveAll()
|
||||
|
||||
for i, key := range keys {
|
||||
if s.Has(key) {
|
||||
t.Errorf("(%d) Expected %q to be removed", i, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
s := store.New(map[string]bool{"test": true})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestNowDateTime(t *testing.T) {
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
PB_BACKEND_URL = /
|
||||
PB_PROFILE_COLLECTION = profiles
|
||||
PB_RULES_SYNTAX_DOCS = https://pocketbase.io/docs/manage-collections#rules-filters-syntax
|
||||
PB_INSTALLER_PARAM = installer
|
||||
|
||||
Vendored
-68
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
import{S as t,i as n,s as o,K as a}from"./index.a658f3b6.js";function l(e){return a("/collections"),[]}class c extends t{constructor(s){super(),n(this,s,l,null,o,{})}}export{c as default};
|
||||
@@ -1,2 +0,0 @@
|
||||
import{S as I,i as K,s as L,F as W,f as F,m as H,n as B,o as J,q as N,H as M,D as q,e as c,g as u,h as b,j as _,E as O,p as w,z,d as k,A as D,B as T,C as Q,k as U,v as V,I as X,y as E,J as Y,K as Z,G as S}from"./index.a658f3b6.js";function G(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&O(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=T(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=T(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,h,m=f[3]&&G(f);return r=new z({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
`),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],D(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),H(r,e,null),_(e,p),H(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(h=[T(e,"submit",Q(f[4])),U(V.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=G(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const j={};$&769&&(j.$$scope={dirty:$,ctx:a}),r.$set(j);const y={};$&770&&(y.$$scope={dirty:$,ctx:a}),d.$set(y),(!P||$&4)&&(i.disabled=a[2]),$&4&&D(i,"btn-loading",a[2])},i(a){P||(B(r.$$.fragment,a),B(d.$$.fragment,a),P=!0)},o(a){J(r.$$.fragment,a),J(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),N(r),N(d),a&&w(R),a&&w(C),A=!1,X(h)}}}function se(f){let e,o;return e=new W({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){H(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(B(e.$$.fragment,s),o=!0)},o(s){J(e.$$.fragment,s),o=!1},d(s){N(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await E.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),Y("Successfully set a new admin password."),Z("/")}catch(g){E.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends I{constructor(e){super(),K(this,e,le,se,L,{params:5})}}export{ae as default};
|
||||
@@ -0,0 +1,2 @@
|
||||
import{S as E,i as G,s as I,F as K,c as F,m as B,t as H,a as N,d as T,C as M,w as q,e as c,f as u,g as b,h as _,x as O,o as w,q as J,b as k,r as L,u as h,v as Q,j as U,l as V,A as X,p as W,B as Y,D as Z,z as S}from"./index.cb8e511c.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&O(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
`),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],L(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),B(r,e,null),_(e,p),B(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(j=[h(e,"submit",Q(f[4])),U(V.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=y(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:a}),r.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:a}),d.$set(D),(!P||$&4)&&(i.disabled=a[2]),$&4&&L(i,"btn-loading",a[2])},i(a){P||(H(r.$$.fragment,a),H(d.$$.fragment,a),P=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(C),A=!1,X(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){B(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(H(e.$$.fragment,s),o=!0)},o(s){N(e.$$.fragment,s),o=!1},d(s){T(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await W.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),Y("Successfully set a new admin password."),Z("/")}catch(g){W.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default};
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{S,i as B,s as M,F as T,f as A,m as E,n as w,o as y,q as H,d as g,e as _,g as p,h as k,j as d,k as j,v as z,w as D,x as G,p as v,y as C,z as N,A as F,B as L,C as I,D as h,E as J,u as P,G as R}from"./index.a658f3b6.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new N({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),A(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),E(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=L(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),H(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=L(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=j(z.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(D(),y(a[$],1,1,()=>{a[$]=null}),G(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new T({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){A(e.$$.fragment)},m(n,l){E(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){H(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends S{constructor(e){super(),B(this,e,W,V,M,{})}}export{Y as default};
|
||||
import{S as E,i as M,s as T,F as j,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as z,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as A,v as I,w as h,x as J,y as P,z as R}from"./index.cb8e511c.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),H(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),L(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=A(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),S(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=A(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=z(B.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(N(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new j({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){H(e.$$.fragment)},m(n,l){L(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends E{constructor(e){super(),M(this,e,W,V,T,{})}}export{Y as default};
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import{S as z,i as A,s as B,F as D,f as S,m as U,n as $,o as v,q as j,H as G,L as J,h as _,w as M,x as N,p as b,y as P,D as y,e as m,g as d,j as g,E as R,z as W,d as C,A as F,B as E,C as Y,u as h,G as L}from"./index.a658f3b6.js";function I(r){let e,s,t,l,n,o,c,a,i,u,k,q,p=r[3]&&T(r);return o=new W({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
|
||||
`),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],F(a,"btn-loading",r[1])},m(f,w){_(f,e,w),g(e,s),g(s,t),g(t,l),p&&p.m(t,null),g(e,n),U(o,e,null),g(e,c),g(e,a),g(a,i),u=!0,k||(q=E(e,"submit",Y(r[4])),k=!0)},p(f,w){f[3]?p?p.p(f,w):(p=T(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const H={};w&769&&(H.$$scope={dirty:w,ctx:f}),o.$set(H),(!u||w&2)&&(a.disabled=f[1]),w&2&&F(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),j(o),k=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
import{S as J,i as M,s as N,F as R,c as S,m as U,t as $,a as v,d as z,C as W,E as Y,g as _,k as j,n as A,o as b,p as F,w as y,e as m,f as d,h as k,x as B,q as D,b as C,r as H,u as E,v as G,y as h,z as T}from"./index.cb8e511c.js";function I(r){let e,s,t,l,n,o,c,a,i,u,g,q,p=r[3]&&L(r);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
|
||||
`),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],H(a,"btn-loading",r[1])},m(f,w){_(f,e,w),k(e,s),k(s,t),k(t,l),p&&p.m(t,null),k(e,n),U(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||(q=E(e,"submit",G(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=L(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const P={};w&769&&(P.$$scope={dirty:w,ctx:f}),o.$set(P),(!u||w&2)&&(a.disabled=f[1]),w&2&&H(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),z(o),g=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Email address changed</p>
|
||||
<p>You can now sign in with your new email address.</p></div>`,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function T(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),g(s,t)},p(l,n){n&8&&R(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),g(e,s),_(i,l,u),_(i,n,u),L(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&L(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=J()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(M(),v(o[u],1,1,()=>{o[u]=null}),N(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new D({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){j(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await P.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(k){P.errorResponseHandler(k)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=k=>{"params"in k&&s(5,l=k.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=G.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends z{constructor(e){super(),A(this,e,X,V,B,{params:5})}}export{x as default};
|
||||
<p>You can now sign in with your new email address.</p></div>`,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function L(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),k(s,t)},p(l,n){n&8&&B(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),k(e,s),_(i,l,u),_(i,n,u),T(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&T(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=Y()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(j(),v(o[u],1,1,()=>{o[u]=null}),A(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new R({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){z(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await F.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(g){F.errorResponseHandler(g)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=g=>{"params"in g&&s(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=W.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends J{constructor(e){super(),M(this,e,X,V,N,{params:5})}}export{x as default};
|
||||
@@ -1,4 +0,0 @@
|
||||
import{S as D,i as E,s as G,F as J,f as F,m as L,n as P,o as q,q as N,H as M,L as W,h as _,w as Y,x as I,p as m,y as j,D as y,e as b,j as w,E as K,z,d as C,g as c,A,B as R,C as O,u as h,G as H}from"./index.a658f3b6.js";function Q(r){let e,l,t,n,s,o,p,a,i,u,v,g,k,S,d=r[4]&&B(r);return o=new z({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),a=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
|
||||
`),d&&d.c(),s=C(),F(o.$$.fragment),p=C(),F(a.$$.fragment),i=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=r[2],A(u,"btn-loading",r[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),L(o,e,null),w(e,p),L(a,e,null),w(e,i),w(e,u),w(u,v),g=!0,k||(S=R(e,"submit",O(r[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=B(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&A(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),N(o),N(a),k=!1,S()}}}function V(r){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Password changed</p>
|
||||
<p>You can now sign in with your new password.</p></div>`,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=R(t,"click",r[7]),n=!0)},p:h,i:h,o:h,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function B(r){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(r[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&K(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0,s.autofocus=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[0]),s.focus(),p||(a=R(s,"input",r[8]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&1&&s.value!==i[0]&&H(s,i[0])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function Z(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[1]),p||(a=R(s,"input",r[9]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&2&&s.value!==i[1]&&H(s,i[1])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function x(r){let e,l,t,n;const s=[V,Q],o=[];function p(a,i){return a[3]?0:1}return e=p(r),l=o[e]=s[e](r),{c(){l.c(),t=W()},m(a,i){o[e].m(a,i),_(a,t,i),n=!0},p(a,i){let u=e;e=p(a),e===u?o[e].p(a,i):(Y(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,i):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(r){let e,l;return e=new J({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:r}}}),{c(){F(e.$$.fragment)},m(t,n){L(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){N(e,t)}}}function te(r,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function i(){if(!p){l(2,p=!0);try{await j.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){j.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return r.$$set=k=>{"params"in k&&l(6,n=k.params)},r.$$.update=()=>{r.$$.dirty&64&&l(4,t=M.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,i,n,u,v,g]}class le extends D{constructor(e){super(),E(this,e,te,ee,G,{params:6})}}export{le as default};
|
||||
@@ -0,0 +1,4 @@
|
||||
import{S as W,i as Y,s as j,F as A,c as H,m as N,t as P,a as q,d as S,C as B,E as D,g as _,k as G,n as I,o as m,p as z,w as y,e as b,h as w,x as K,q as E,b as C,f as c,r as J,u as h,v as O,y as F,z as R}from"./index.cb8e511c.js";function Q(i){let e,l,t,n,s,o,p,a,r,u,v,g,k,L,d=i[4]&&M(i);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),a=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
|
||||
`),d&&d.c(),s=C(),H(o.$$.fragment),p=C(),H(a.$$.fragment),r=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=i[2],J(u,"btn-loading",i[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),N(o,e,null),w(e,p),N(a,e,null),w(e,r),w(e,u),w(u,v),g=!0,k||(L=h(e,"submit",O(i[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=M(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&J(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),S(o),S(a),k=!1,L()}}}function V(i){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Password changed</p>
|
||||
<p>You can now sign in with your new password.</p></div>`,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=h(t,"click",i[7]),n=!0)},p:F,i:F,o:F,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function M(i){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(i[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&K(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0,s.autofocus=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[0]),s.focus(),p||(a=h(s,"input",i[8]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&1&&s.value!==r[0]&&R(s,r[0])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function Z(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[1]),p||(a=h(s,"input",i[9]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&2&&s.value!==r[1]&&R(s,r[1])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function x(i){let e,l,t,n;const s=[V,Q],o=[];function p(a,r){return a[3]?0:1}return e=p(i),l=o[e]=s[e](i),{c(){l.c(),t=D()},m(a,r){o[e].m(a,r),_(a,t,r),n=!0},p(a,r){let u=e;e=p(a),e===u?o[e].p(a,r):(G(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,r):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(i){let e,l;return e=new A({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:i}}}),{c(){H(e.$$.fragment)},m(t,n){N(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){S(e,t)}}}function te(i,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function r(){if(!p){l(2,p=!0);try{await z.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){z.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return i.$$set=k=>{"params"in k&&l(6,n=k.params)},i.$$.update=()=>{i.$$.dirty&64&&l(4,t=B.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,r,n,u,v,g]}class le extends W{constructor(e){super(),Y(this,e,te,ee,j,{params:6})}}export{le as default};
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
import{S as k,i as v,s as y,F as w,f as x,m as C,n as g,o as L,q as $,y as H,L as M,h as r,p as a,e as u,d as m,g as f,B as _,u as p}from"./index.a658f3b6.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
import{S as k,i as v,s as y,F as w,c as x,m as C,t as g,a as $,d as L,p as H,E as M,g as r,o as a,e as u,b as m,f,u as _,y as p}from"./index.cb8e511c.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
<div class="content txt-bold"><p>Invalid or expired verification token.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-danger"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[4]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function S(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function q(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function F(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[q]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){L(t.$$.fragment,e),s=!1},d(e){$(t,e)}}}function U(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class B extends k{constructor(t){super(),v(this,t,U,F,y,{params:2})}}export{B as default};
|
||||
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function F(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function U(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[F]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){$(t.$$.fragment,e),s=!1},d(e){L(t,e)}}}function V(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class E extends k{constructor(t){super(),v(this,t,V,U,y,{params:2})}}export{E as default};
|
||||
Vendored
-363
File diff suppressed because one or more lines are too long
Vendored
+372
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -20,8 +20,8 @@
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
</script>
|
||||
<script type="module" crossorigin src="/_/assets/index.a658f3b6.js"></script>
|
||||
<link rel="stylesheet" href="/_/assets/index.a5726992.css">
|
||||
<script type="module" crossorigin src="/_/assets/index.cb8e511c.js"></script>
|
||||
<link rel="stylesheet" href="/_/assets/index.d4a95b8f.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ var distDir embed.FS
|
||||
//go:embed dist/index.html
|
||||
var indexHTML embed.FS
|
||||
|
||||
// DistDirFS contains the embeded dist directory files (without the "dist" prefix)
|
||||
// DistDirFS contains the embedded dist directory files (without the "dist" prefix)
|
||||
var DistDirFS = echo.MustSubFS(distDir, "dist")
|
||||
|
||||
// DistIndexHTML contains the embeded dist/index.html file (without the "dist" prefix)
|
||||
// DistIndexHTML contains the embedded dist/index.html file (without the "dist" prefix)
|
||||
var DistIndexHTML = echo.MustSubFS(indexHTML, "dist")
|
||||
|
||||
Generated
+237
-415
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -19,16 +19,16 @@
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.30",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"chart.js": "^3.7.1",
|
||||
"chartjs-adapter-luxon": "^1.1.0",
|
||||
"luxon": "^2.3.2",
|
||||
"pocketbase": "^0.1.1",
|
||||
"pocketbase": "^0.2.1",
|
||||
"prismjs": "^1.28.0",
|
||||
"sass": "^1.45.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-flatpickr": "^3.2.6",
|
||||
"svelte-spa-router": "^3.2.0",
|
||||
"vite": "^2.7.0"
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -16,11 +16,15 @@
|
||||
|
||||
let oldLocation = undefined;
|
||||
|
||||
let showAppSidebar = false;
|
||||
|
||||
function handleRouteLoading(e) {
|
||||
if (e?.detail?.location === oldLocation) {
|
||||
return; // not an actual change
|
||||
}
|
||||
|
||||
showAppSidebar = !!e?.detail?.userData?.showAppSidebar;
|
||||
|
||||
oldLocation = e?.detail?.location;
|
||||
|
||||
// resets
|
||||
@@ -30,11 +34,7 @@
|
||||
}
|
||||
|
||||
function handleRouteFailure() {
|
||||
if (ApiClient.AuthStore.isValid) {
|
||||
replace("/");
|
||||
} else {
|
||||
ApiClient.logout();
|
||||
}
|
||||
replace("/");
|
||||
}
|
||||
|
||||
function logout() {
|
||||
@@ -43,7 +43,7 @@
|
||||
</script>
|
||||
|
||||
<div class="app-layout">
|
||||
{#if $admin?.id}
|
||||
{#if $admin?.id && showAppSidebar}
|
||||
<aside class="app-sidebar">
|
||||
<a href="/" class="logo logo-sm" use:link>
|
||||
<img
|
||||
|
||||
@@ -1,487 +0,0 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import { addInfoToast } from "@/stores/toasts";
|
||||
|
||||
let popupActive = true;
|
||||
|
||||
setTimeout(function () {
|
||||
addInfoToast("Hello world");
|
||||
}, 500);
|
||||
</script>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="">EXAMPLE</label>
|
||||
<ObjectSelect multiple searchable items={["test1", "test2"]} />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-field">
|
||||
<label for="">EXAMPLE</label>
|
||||
<ObjectSelect searchable items={["test1", "test2"]} />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-field disabled">
|
||||
<label for="">EXAMPLE</label>
|
||||
<ObjectSelect disabled searchable items={["test1", "test2"]} />
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="alert">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="content">Hello world!</div>
|
||||
<div class="close">
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="content">Hello world!</div>
|
||||
<div class="close">
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="content">Hello world!</div>
|
||||
<div class="close">
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<div class="icon">
|
||||
<i class="ri-error-warning-line" />
|
||||
</div>
|
||||
<div class="content">Hello world!</div>
|
||||
<div class="close">
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<div class="icon">
|
||||
<i class="ri-checkbox-circle-line" />
|
||||
</div>
|
||||
<div class="content">Hello world!</div>
|
||||
<div class="close">
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h1>H1 title</h1>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
<h2>H2 title</h2>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
<h3>H3 title</h3>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
<h4>H4 title</h4>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
<h5>H5 title</h5>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
<h6>H6 title</h6>
|
||||
<p>Lorem Ipsum dolor sit amet...</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-6">COL1</div>
|
||||
<div class="col-6">COL2</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Lorem Ipsum is <a href="/">simply dummy</a> text of the printing and typesetting industry. Lorem Ipsum has
|
||||
been the industry's
|
||||
<strong>standard</strong> dummy text ever since the 1500s, when an unknown printer took a galley of type
|
||||
and <em>scrambled</em> it to make a type specimen book. It has survived not only five centuries, but also
|
||||
the leap into electronic typesetting, remaining<sup>1</sup> essentially<sub>2</sub> unchanged.
|
||||
</p>
|
||||
<p>
|
||||
It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and
|
||||
more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><small>Option 1</small></li>
|
||||
<li>Option 2</li>
|
||||
<li>Option 3</li>
|
||||
</ul>
|
||||
|
||||
<ol>
|
||||
<li>Option 1</li>
|
||||
<li>Option 2</li>
|
||||
<li>Option 3</li>
|
||||
</ol>
|
||||
|
||||
<hr />
|
||||
|
||||
<span use:tooltip={"My tooltip"}>Lorem Ipsum</span>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn">Button default</button>
|
||||
<button class="btn btn-danger">Button danger</button>
|
||||
<button class="btn btn-warning">Button warning</button>
|
||||
<button class="btn btn-success">Button success</button>
|
||||
<button class="btn btn-info">Button info</button>
|
||||
<button class="btn btn-hint">Button hint</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-secondary">Button default</button>
|
||||
<button class="btn btn-secondary btn-danger">Button danger</button>
|
||||
<button class="btn btn-secondary btn-warning">Button danger</button>
|
||||
<button class="btn btn-secondary btn-success">Button success</button>
|
||||
<button class="btn btn-secondary btn-info">Button info</button>
|
||||
<button class="btn btn-secondary btn-hint">Button hint</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-outline">Button default</button>
|
||||
<button class="btn btn-outline btn-danger">Button danger</button>
|
||||
<button class="btn btn-outline btn-warning">Button danger</button>
|
||||
<button class="btn btn-outline btn-success">Button success</button>
|
||||
<button class="btn btn-outline btn-info">Button info</button>
|
||||
<button class="btn btn-outline btn-hint">Button hint</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button disabled class="btn">Button default</button>
|
||||
<button disabled class="btn btn-danger">Button danger</button>
|
||||
<button disabled class="btn btn-warning">Button warning</button>
|
||||
<button disabled class="btn btn-success">Button success</button>
|
||||
<button disabled class="btn btn-info">Button info</button>
|
||||
<button disabled class="btn btn-hint">Button hint</button>
|
||||
|
||||
<button disabled class="btn btn-secondary">Button default</button>
|
||||
<button disabled class="btn btn-secondary btn-danger">Button danger</button>
|
||||
<button disabled class="btn btn-secondary btn-warning">Button danger</button>
|
||||
<button disabled class="btn btn-secondary btn-success">Button success</button>
|
||||
<button disabled class="btn btn-secondary btn-info">Button info</button>
|
||||
<button disabled class="btn btn-secondary btn-hint">Button hint</button>
|
||||
<hr />
|
||||
|
||||
<button class="btn">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button default</span>
|
||||
</button>
|
||||
<button class="btn btn-danger">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button danger</span>
|
||||
</button>
|
||||
<button class="btn btn-warning">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button warning</span>
|
||||
</button>
|
||||
<button class="btn btn-success">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button success</span>
|
||||
</button>
|
||||
<button class="btn btn-hint">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button hint</span>
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button default</span>
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button danger</span>
|
||||
</button>
|
||||
<button class="btn btn-warning btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button warning</span>
|
||||
</button>
|
||||
<button class="btn btn-success btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button success</span>
|
||||
</button>
|
||||
<button class="btn btn-hint btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button hint</span>
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button default</span>
|
||||
</button>
|
||||
<button class="btn btn-danger btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button danger</span>
|
||||
</button>
|
||||
<button class="btn btn-warning btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button warning</span>
|
||||
</button>
|
||||
<button class="btn btn-success btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button success</span>
|
||||
</button>
|
||||
<button class="btn btn-hint btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button hint</span>
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-sm btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-lg btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-lg btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button class="btn btn-loading">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
<button class="btn btn-loading btn-primary btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
<button class="btn btn-loading btn-danger btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-loading btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-loading btn-primary btn-sm btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button class="btn btn-loading btn-danger btn-lg btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button disabled class="btn btn-loading">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
<button disabled class="btn btn-loading btn-primary btn-sm">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
<button disabled class="btn btn-loading btn-danger btn-lg">
|
||||
<i class="ri-mail-line" />
|
||||
<span class="txt">Button Loading</span>
|
||||
</button>
|
||||
|
||||
<button disabled class="btn btn-loading btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button disabled class="btn btn-loading btn-primary btn-sm btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
<button disabled class="btn btn-loading btn-danger btn-lg btn-circle">
|
||||
<i class="ri-mail-line" />
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<input type="text" />
|
||||
|
||||
<hr />
|
||||
<select>
|
||||
<option value="1" selected>Option 1</option>
|
||||
<option value="">Option 2</option>
|
||||
<option value="">Option 3</option>
|
||||
</select>
|
||||
<hr />
|
||||
|
||||
<textarea cols="30" rows="10" />
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field required">
|
||||
<label for="field_1">Name</label>
|
||||
<input type="text" id="field_1" placeholder="Name 123" />
|
||||
</div>
|
||||
|
||||
<div class="form-field required">
|
||||
<label for="field_2">Description</label>
|
||||
<textarea id="field_2" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="field_3">Choose value</label>
|
||||
<select id="field_3">
|
||||
<option value="1" selected>Option 1</option>
|
||||
<option value="">Option 2</option>
|
||||
<option value="">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field">
|
||||
<input type="text" placeholder="Lorem ipsum dolor sit amet..." />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<textarea />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<select>
|
||||
<option value="1" selected>Option 1</option>
|
||||
<option value="">Option 2</option>
|
||||
<option value="">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field">
|
||||
<input type="checkbox" id="field_check" />
|
||||
<label for="field_check">
|
||||
I agree with the <a href="/">terms and conditions</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<input type="radio" name="radio_check" id="field_radio1" value="1" />
|
||||
<label for="field_radio1">Radio 1</label>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<input type="radio" name="radio_check" id="field_radio2" value="2" />
|
||||
<label for="field_radio2">Radio 2</label>
|
||||
</div>
|
||||
|
||||
<div class="form-field form-field-toggle">
|
||||
<input type="checkbox" id="field_toggle" />
|
||||
<label for="field_toggle">Toggle check</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field error">
|
||||
<label for="field_error">Name + error</label>
|
||||
<input type="text" id="field_error" />
|
||||
<div class="help-block help-block-error">
|
||||
<p>Something went wrong</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="field_hint">Name + hint</label>
|
||||
<input type="text" id="field_hint" />
|
||||
<div class="help-block">
|
||||
<p>Lorem ipsum dolor <a href="/">sit</a> amet</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<input type="checkbox" id="field_check_hint" />
|
||||
<label for="field_check_hint">Checkbox hint</label>
|
||||
<div class="help-block">Lorem ipsum</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field has-error">
|
||||
<input type="checkbox" id="field_check_error" />
|
||||
<label for="field_check_error">Checkbox error</label>
|
||||
<div class="help-block help-error">Lorem ipsum</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field disabled">
|
||||
<input type="checkbox" id="field_check" disabled />
|
||||
<label for="field_check">
|
||||
I agree with the <a href="/">terms and conditions</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<input type="radio" name="radio_check" id="field_radio1" value="1" disabled />
|
||||
<label for="field_radio1">Radio 1</label>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<input type="radio" name="radio_check" id="field_radio2" value="2" disabled />
|
||||
<label for="field_radio2">Radio 2</label>
|
||||
</div>
|
||||
|
||||
<div class="form-field form-field-toggle disabled">
|
||||
<input type="checkbox" id="field_toggle" disabled />
|
||||
<label for="field_toggle" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-field disabled">
|
||||
<label for="field_addon1">Name</label>
|
||||
<div class="form-field-addon">
|
||||
<i class="ri-mail-line" />
|
||||
</div>
|
||||
<input disabled type="text" id="field_addon1" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<div class="form-field-addon">
|
||||
<i class="ri-mail-line" />
|
||||
</div>
|
||||
<input type="text" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-field">
|
||||
<label for="field_group1">Name</label>
|
||||
<div class="form-field-addon">
|
||||
<div class="btn btn-circle btn-secondary">
|
||||
<i class="ri-mail-line" />
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="field_group1" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="field_group2">Password</label>
|
||||
<div class="form-field-addon">
|
||||
<i class="ri-mail-line" />
|
||||
</div>
|
||||
<input type="password" id="field_group2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OverlayPanel bind:active={popupActive} popup={false}>
|
||||
<h4 slot="header">My title</h4>
|
||||
<p>Lorem ipsum dolor sit amet...</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="btn btn-secondary">Cancel</button>
|
||||
<button class="btn btn-expanded">Save</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import { replace } from "svelte-spa-router";
|
||||
|
||||
replace("/collections");
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import { tick } from "svelte";
|
||||
import { replace } from "svelte-spa-router";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
import Installer from "@/components/base/Installer.svelte";
|
||||
|
||||
let showInstaller = false;
|
||||
|
||||
handler();
|
||||
|
||||
function handler() {
|
||||
showInstaller = false;
|
||||
|
||||
const realQueryParams = new URLSearchParams(window.location.search);
|
||||
if (realQueryParams.has(import.meta.env.PB_INSTALLER_PARAM)) {
|
||||
ApiClient.logout(false);
|
||||
showInstaller = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ApiClient.AuthStore.isValid) {
|
||||
replace("/collections");
|
||||
} else {
|
||||
ApiClient.logout();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showInstaller}
|
||||
<FullPage>
|
||||
<Installer
|
||||
on:submit={async () => {
|
||||
showInstaller = false;
|
||||
|
||||
await tick();
|
||||
|
||||
// clear the installer param
|
||||
window.location.search = "";
|
||||
}}
|
||||
/>
|
||||
</FullPage>
|
||||
{/if}
|
||||
@@ -3,6 +3,7 @@
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { admin as loggedAdmin } from "@/stores/admin";
|
||||
import Searchbar from "@/components/base/Searchbar.svelte";
|
||||
import RefreshButton from "@/components/base/RefreshButton.svelte";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
@@ -31,6 +32,7 @@
|
||||
|
||||
export function loadAdmins() {
|
||||
isLoading = true;
|
||||
|
||||
admins = []; // reset
|
||||
|
||||
return ApiClient.Admins.getFullList(100, {
|
||||
@@ -42,7 +44,7 @@
|
||||
isLoading = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err !== null) {
|
||||
if (!err?.isAbort) {
|
||||
isLoading = false;
|
||||
console.warn(err);
|
||||
clearList();
|
||||
@@ -65,6 +67,8 @@
|
||||
<div class="breadcrumb-item">Admins</div>
|
||||
</nav>
|
||||
|
||||
<RefreshButton on:refresh={() => loadAdmins()} />
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
<button type="button" class="btn btn-expanded" on:click={() => adminUpsertPanel?.show()}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user