Compare commits

..
53 Commits
Author SHA1 Message Date
Gani Georgiev 353248c34a updated ui/dist 2022-09-29 12:54:58 +03:00
Gani Georgiev 93d48a85ac added fallback handling when both contains operands are table columns 2022-09-29 12:33:53 +03:00
Gani Georgiev b84930f21a records listing optimizations 2022-09-28 22:17:24 +03:00
Rohan Verma 3cbab96f51 [#661] serve css files with text/css content-type
Currently, css files are served as text/plain by the server. It is not
trivial to detect css file types similar to the issue with svg files.

When the css files are served as text/plain instead of
text/css they become unusable as stylesheets in the browser when served
via the api.

In this commit we generalize the svg detection to also detect css files
and serve specific extensions with their respective mimetypes.
2022-09-28 21:25:50 +03:00
Gani Georgiev 6c005c4a9a remove OrderBy nil variadic argument 2022-09-22 20:35:20 +03:00
Gani Georgiev ccc3707fb6 replaced empty slice literal with nil 2022-09-22 20:23:50 +03:00
Gani Georgiev 3d36ff7e96 unset ORDER BY for search count queries 2022-09-22 20:18:17 +03:00
Gani Georgiev 0b2eb24c6f updated go deps 2022-09-21 21:29:23 +03:00
Gani Georgiev 954067860c [#590] fixed realtime events bind order by adding hooks.PreAdd 2022-09-21 14:41:20 +03:00
Gani Georgiev 9a8c9dd115 [#586] fixed multiple nested relation expansions with shared base path 2022-09-21 13:34:34 +03:00
Aaron Schmied a1ad5004f8 [#585] respect the EXIF orientation tag when generating thumbs
@see: https://github.com/disintegration/imaging#incorrect-image-orientation-after-processing-eg-an-image-appears-rotated-after-resizing
2022-09-21 13:13:26 +03:00
Gani Georgiev 7006e1f5d7 [#567] resolve the direct user profile fields from the profiles table and not from the static auth model 2022-09-20 11:20:30 +03:00
Gani Georgiev 8be8f3f3cb updated search provider tests 2022-09-18 08:49:51 +03:00
Gani Georgiev 00fd007d50 raised MaxPerPage limit to 400 2022-09-18 08:41:42 +03:00
Gani Georgiev 96bfc69c8e updated ui/dist 2022-09-18 01:57:18 +03:00
Gani Georgiev e542b0d8c5 include only the words selection keymap for code inputs 2022-09-18 01:55:33 +03:00
Gani Georgiev 9814dda8e4 [#478] load selected relation items before the other options 2022-09-18 01:18:54 +03:00
Gani Georgiev 978fdd3ce7 [#478] preserve multiple selection order 2022-09-18 00:29:32 +03:00
Gani Georgiev 9cf89870e7 [#519] improved query performance for relations lookup 2022-09-17 22:55:56 +03:00
Gani Georgiev b8c54568e3 fixed readme typo 2022-09-16 13:24:02 +03:00
Gani Georgiev 843bbf99cc added note about pull requests 2022-09-16 13:06:28 +03:00
Gani Georgiev daffb0f86e [#488] added X-Accel-Buffering:no sse header 2022-09-16 11:19:31 +03:00
Gani Georgiev 1f5c3328e5 [#470] added --pbPublic flag 2022-09-16 11:18:15 +03:00
Gani Georgiev ed5f3b86f5 [#470] don't rely on the cwd and look for pb_public relative to pb_data 2022-09-15 22:52:24 +03:00
Travis Ray 1ba2d14231 [#446] Fixed spelling error on Realtime API page 2022-09-14 21:31:12 +03:00
Gani Georgiev 6cda610ede updated ui deps and generated ui/dist 2022-09-14 20:53:09 +03:00
Gani Georgiev 2fa5233fa6 [#440] try to use the original image format when creating thumbs 2022-09-14 17:12:47 +03:00
Gani Georgiev 030dfc2690 updated ui/dist 2022-09-10 22:54:26 +03:00
Gani Georgiev 8c11e2ef01 [#409] added pocketbase.NewWithConfig factory 2022-09-10 22:53:17 +03:00
Gani Georgiev 4b64e0910b removed commented pb.Bootstrap() code 2022-09-09 14:54:19 +03:00
Gani Georgiev 4a6bc453de updated ui deps and skip creating pb_data on --version or --help execution 2022-09-09 13:58:29 +03:00
Gani Georgiev 96d09a30c4 [#405] updated Google OAuth2 userinfo response data 2022-09-09 09:12:34 +03:00
Gani Georgiev a0d7f23d77 [#396] normalized tests.ApiScenario.TestAppFactory declaration 2022-09-07 20:51:03 +03:00
Gani Georgiev 74108d84ca [#396] renamed tests.CloneIntoTempDir to tests.TempDirClone 2022-09-07 20:34:18 +03:00
Gani Georgiev 4bc28abac4 [#396] updated tests helpers 2022-09-07 20:31:05 +03:00
Gani Georgiev b79a7982bf [#385] added username to the OAuth2 AuthUser model 2022-09-05 16:15:27 +03:00
Gani Georgiev b717896232 [#390] serve the mimetype detected during upload 2022-09-05 15:46:40 +03:00
Gani Georgiev 9d53aec9ee [#375] fixed windows console color output 2022-09-02 14:42:22 +03:00
Gani Georgiev 9d30ca81cb fixed comment typo 2022-09-02 12:45:59 +03:00
Gani Georgiev 93b3788448 added additional check for empty ExternalAuth data in case the provider api changes 2022-09-02 12:05:00 +03:00
Gani Georgiev 0ac4c9e1fc updated sdk 2022-09-02 10:12:36 +03:00
Gani Georgiev df1a947b61 updated response messages and rebuilt ui/dist 2022-09-02 10:00:36 +03:00
Gani Georgiev 06a7f1af05 replaced MX email validator with email format only 2022-09-01 17:08:55 +03:00
Gani Georgiev 07ac5bf6a2 [#33] added Twitter OAuth2 provider 2022-09-01 16:46:06 +03:00
Gani Georgiev f56c52a1f7 share auth providers UI configurations 2022-09-01 15:49:00 +03:00
Gani Georgiev f0b57c6b91 [#276] added option to list and unlink external user auth relations 2022-09-01 12:22:59 +03:00
Gani Georgiev f61d0ec6f7 synced with master 2022-08-31 21:33:17 +03:00
Gani Georgiev f5ff7193a9 [#276] added support for linking external auths by provider id 2022-08-31 13:38:31 +03:00
Gani Georgiev 9fe94f5c7d [#351] improved mimetype sniffing 2022-08-26 07:00:22 +03:00
Gani Georgiev 0f9ddbf7ec added auto html to plain text mail generation 2022-08-26 06:46:34 +03:00
Gani Georgiev f14105b04a synced with master 2022-08-25 16:50:28 +03:00
Gani Georgiev a908d20dcd increased max allowed token duration 2022-08-25 10:58:15 +03:00
Gani Georgiev 49b084cf50 [#335] added Discord OAuth2 provider 2022-08-21 20:04:38 +03:00
116 changed files with 3461 additions and 1785 deletions
+5 -1
View File
@@ -34,7 +34,7 @@ The easiest way to interact with the API is to use one of the official SDK clien
## Overview
PocketBase could be used as a standalone app or as a Go framework/toolkit that enables you to build
PocketBase could be [downloaded directly as a standalone app](https://github.com/pocketbase/pocketbase/releases) or it could be used as a Go framework/toolkit which allows you to build
your own custom app specific business logic and still have a single portable executable at the end.
### Installation
@@ -121,7 +121,11 @@ PocketBase is free and open source project licensed under the [MIT License](LICE
You could help continuing its development by:
- [Contribute to the source code](CONTRIBUTING.md)
> **NB! PR for new features will not be merged until we manage to complete the users refactoring in [#376](https://github.com/pocketbase/pocketbase/issues/376)!**
- [Suggest new features and report issues](https://github.com/pocketbase/pocketbase/issues)
- [Donate a small amount](https://pocketbase.io/support-us)
> Please note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)),
+3 -3
View File
@@ -119,7 +119,7 @@ func TestAdminRequestPasswordReset(t *testing.T) {
Body: strings.NewReader(`{"email":"test@example.com"}`),
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
// simulate recent password request
admin, err := app.Dao().FindAdminByEmail("test@example.com")
if err != nil {
@@ -446,7 +446,7 @@ func TestAdminDelete(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
// delete all admins except the authorized one
adminModel := &models.Admin{}
_, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{
@@ -483,7 +483,7 @@ func TestAdminCreate(t *testing.T) {
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) {
BeforeTestFunc: 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 {
+4 -4
View File
@@ -54,7 +54,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "custom route",
Method: http.MethodGet,
Url: "/custom",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/custom",
@@ -70,7 +70,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with HTTPError",
Method: http.MethodGet,
Url: "/http-error",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/http-error",
@@ -86,7 +86,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with api error",
Method: http.MethodGet,
Url: "/api-error",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api-error",
@@ -102,7 +102,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with plain error",
Method: http.MethodGet,
Url: "/plain-error",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/plain-error",
+5 -5
View File
@@ -475,7 +475,7 @@ func TestCollectionImport(t *testing.T) {
`"data":{`,
`"collections":{"code":"validation_required"`,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
@@ -501,7 +501,7 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{
"OnCollectionsBeforeImportRequest": 1,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
@@ -542,7 +542,7 @@ func TestCollectionImport(t *testing.T) {
"OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeCreate": 2,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
@@ -590,7 +590,7 @@ func TestCollectionImport(t *testing.T) {
"OnModelBeforeCreate": 2,
"OnModelAfterCreate": 2,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
@@ -689,7 +689,7 @@ func TestCollectionImport(t *testing.T) {
"OnModelBeforeCreate": 1,
"OnModelAfterCreate": 1,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
+6 -6
View File
@@ -34,7 +34,7 @@ func TestRequestsList(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
@@ -56,7 +56,7 @@ func TestRequestsList(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
@@ -103,7 +103,7 @@ func TestRequestView(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
@@ -118,7 +118,7 @@ func TestRequestView(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
@@ -161,7 +161,7 @@ func TestRequestsStats(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
@@ -178,7 +178,7 @@ func TestRequestsStats(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err)
}
+26 -26
View File
@@ -18,7 +18,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -40,7 +40,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -62,7 +62,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -81,7 +81,7 @@ func TestRequireGuestOnly(t *testing.T) {
Name: "guest",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -109,7 +109,7 @@ func TestRequireUserAuth(t *testing.T) {
Name: "guest",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -131,7 +131,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -153,7 +153,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -175,7 +175,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -203,7 +203,7 @@ func TestRequireAdminAuth(t *testing.T) {
Name: "guest",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -225,7 +225,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -247,7 +247,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -269,7 +269,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -297,7 +297,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
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) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -316,7 +316,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
Name: "guest (while having 0 existing admins)",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: 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 {
@@ -344,7 +344,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -366,7 +366,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -388,7 +388,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -416,7 +416,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
Name: "guest",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -438,7 +438,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -460,7 +460,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -482,7 +482,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test",
@@ -510,7 +510,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
Name: "guest",
Method: http.MethodGet,
Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test/:id",
@@ -532,7 +532,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test/:id",
@@ -555,7 +555,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
// test3@example.com
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test/:id",
@@ -577,7 +577,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test/:id",
@@ -599,7 +599,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/my/test/:custom",
+7 -4
View File
@@ -48,6 +48,9 @@ func (api *realtimeApi) connect(c echo.Context) error {
c.Response().Header().Set("Content-Type", "text/event-stream; charset=UTF-8")
c.Response().Header().Set("Cache-Control", "no-store")
c.Response().Header().Set("Connection", "keep-alive")
// https://github.com/pocketbase/pocketbase/discussions/480#discussioncomment-3657640
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering
c.Response().Header().Set("X-Accel-Buffering", "no")
event := &core.RealtimeConnectEvent{
HttpContext: c,
@@ -163,7 +166,7 @@ func (api *realtimeApi) bindEvents() {
adminTable := (&models.Admin{}).TableName()
// update user/admin auth state
api.app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
api.app.OnModelAfterUpdate().PreAdd(func(e *core.ModelEvent) error {
modelTable := e.Model.TableName()
var contextKey string
@@ -187,7 +190,7 @@ func (api *realtimeApi) bindEvents() {
})
// remove user/admin client(s)
api.app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error {
api.app.OnModelAfterDelete().PreAdd(func(e *core.ModelEvent) error {
modelTable := e.Model.TableName()
var contextKey string
@@ -210,14 +213,14 @@ func (api *realtimeApi) bindEvents() {
return nil
})
api.app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
api.app.OnModelAfterCreate().PreAdd(func(e *core.ModelEvent) error {
if record, ok := e.Model.(*models.Record); ok {
api.broadcastRecord("create", record)
}
return nil
})
api.app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
api.app.OnModelAfterUpdate().PreAdd(func(e *core.ModelEvent) error {
if record, ok := e.Model.(*models.Record); ok {
api.broadcastRecord("update", record)
}
+11 -11
View File
@@ -27,7 +27,7 @@ func TestRealtimeConnect(t *testing.T) {
ExpectedEvents: map[string]int{
"OnRealtimeConnectRequest": 1,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if len(app.SubscriptionsBroker().Clients()) != 0 {
t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients()))
}
@@ -68,11 +68,11 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 1,
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
client.Subscribe("test0")
app.SubscriptionsBroker().Register(client)
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if len(client.Subscriptions()) != 0 {
t.Errorf("Expected no subscriptions, got %v", client.Subscriptions())
}
@@ -89,11 +89,11 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 1,
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
client.Subscribe("test0")
app.SubscriptionsBroker().Register(client)
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
expectedSubs := []string{"test1", "test2"}
if len(expectedSubs) != len(client.Subscriptions()) {
t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions())
@@ -120,10 +120,10 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 1,
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
app.SubscriptionsBroker().Register(client)
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
admin, _ := client.Get(apis.ContextAdminKey).(*models.Admin)
if admin == nil {
t.Errorf("Expected admin auth model, got nil")
@@ -144,10 +144,10 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 1,
},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
app.SubscriptionsBroker().Register(client)
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
user, _ := client.Get(apis.ContextUserKey).(*models.User)
if user == nil {
t.Errorf("Expected user auth model, got nil")
@@ -165,14 +165,14 @@ func TestRealtimeSubscribe(t *testing.T) {
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
initialAuth := &models.User{}
initialAuth.RefreshId()
client.Set(apis.ContextUserKey, initialAuth)
app.SubscriptionsBroker().Register(client)
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
user, _ := client.Get(apis.ContextUserKey).(*models.User)
if user == nil {
t.Errorf("Expected user auth model, got nil")
+5 -5
View File
@@ -459,7 +459,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 3, // +2 cascade delete related records
"OnModelAfterDelete": 3, // +2 cascade delete related records
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
@@ -481,7 +481,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 3, // +2 cascade delete related records
"OnModelAfterDelete": 3, // +2 cascade delete related records
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
@@ -501,7 +501,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 1,
"OnModelAfterDelete": 1,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
},
},
@@ -529,7 +529,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 1,
"OnModelAfterDelete": 1,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
},
},
@@ -562,7 +562,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 3,
"OnModelAfterDelete": 3,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
recId := "63c2ab80-84ab-4057-a592-4604a731f78f"
col, _ := app.Dao().FindCollectionByNameOrId("demo2")
rec, _ := app.Dao().FindRecordById(col, recId, nil)
+6 -3
View File
@@ -52,6 +52,7 @@ func TestSettingsList(t *testing.T) {
`"facebookAuth":{`,
`"githubAuth":{`,
`"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
},
@@ -114,6 +115,7 @@ func TestSettingsSet(t *testing.T) {
`"facebookAuth":{`,
`"githubAuth":{`,
`"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"Acme"`,
@@ -166,6 +168,7 @@ func TestSettingsSet(t *testing.T) {
`"facebookAuth":{`,
`"githubAuth":{`,
`"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"update_test"`,
@@ -285,7 +288,7 @@ func TestSettingsTestEmail(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
@@ -316,7 +319,7 @@ func TestSettingsTestEmail(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[password-reset] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
@@ -347,7 +350,7 @@ func TestSettingsTestEmail(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
+68 -1
View File
@@ -38,6 +38,8 @@ func BindUserApi(app core.App, rg *echo.Group) {
subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id"))
subGroup.PATCH("/:id", api.update, RequireAdminAuth())
subGroup.DELETE("/:id", api.delete, RequireAdminOrOwnerAuth("id"))
subGroup.GET("/:id/external-auths", api.listExternalAuths, RequireAdminOrOwnerAuth("id"))
subGroup.DELETE("/:id/external-auths/:provider", api.unlinkExternalAuth, RequireAdminOrOwnerAuth("id"))
}
type userApi struct {
@@ -129,7 +131,7 @@ func (api *userApi) authMethods(c echo.Context) error {
}
state := security.RandomString(30)
codeVerifier := security.RandomString(30)
codeVerifier := security.RandomString(43)
codeChallenge := security.S256Challenge(codeVerifier)
codeChallengeMethod := "S256"
result.AuthProviders = append(result.AuthProviders, providerInfo{
@@ -450,3 +452,68 @@ func (api *userApi) delete(c echo.Context) error {
return handlerErr
}
func (api *userApi) listExternalAuths(c echo.Context) error {
id := c.PathParam("id")
if id == "" {
return rest.NewNotFoundError("", nil)
}
user, err := api.app.Dao().FindUserById(id)
if err != nil || user == nil {
return rest.NewNotFoundError("", err)
}
externalAuths, err := api.app.Dao().FindAllExternalAuthsByUserId(user.Id)
if err != nil {
return rest.NewBadRequestError("Failed to fetch the external auths for the specified user.", err)
}
event := &core.UserListExternalAuthsEvent{
HttpContext: c,
User: user,
ExternalAuths: externalAuths,
}
return api.app.OnUserListExternalAuths().Trigger(event, func(e *core.UserListExternalAuthsEvent) error {
return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths)
})
}
func (api *userApi) unlinkExternalAuth(c echo.Context) error {
id := c.PathParam("id")
provider := c.PathParam("provider")
if id == "" || provider == "" {
return rest.NewNotFoundError("", nil)
}
user, err := api.app.Dao().FindUserById(id)
if err != nil || user == nil {
return rest.NewNotFoundError("", err)
}
externalAuth, err := api.app.Dao().FindExternalAuthByUserIdAndProvider(user.Id, provider)
if err != nil {
return rest.NewNotFoundError("Missing external auth provider relation.", err)
}
event := &core.UserUnlinkExternalAuthEvent{
HttpContext: c,
User: user,
ExternalAuth: externalAuth,
}
handlerErr := api.app.OnUserBeforeUnlinkExternalAuthRequest().Trigger(event, func(e *core.UserUnlinkExternalAuthEvent) error {
if err := api.app.Dao().DeleteExternalAuth(externalAuth); err != nil {
return rest.NewBadRequestError("Cannot unlink the external auth provider. Make sure that the user has other linked auth providers OR has an email address.", err)
}
return e.HttpContext.NoContent(http.StatusNoContent)
})
if handlerErr == nil {
api.app.OnUserAfterUnlinkExternalAuthRequest().Trigger(event)
}
return handlerErr
}
+192 -7
View File
@@ -84,7 +84,7 @@ func TestUserEmailAuth(t *testing.T) {
Method: http.MethodPost,
Url: "/api/users/auth-via-email",
Body: strings.NewReader(`{"email":"test@example.com","password":"123456"}`),
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
app.Settings().EmailAuth.Enabled = false
},
ExpectedStatus: 400,
@@ -159,7 +159,7 @@ func TestUserRequestPasswordReset(t *testing.T) {
Body: strings.NewReader(`{"email":"test@example.com"}`),
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
// simulate recent password request
user, err := app.Dao().FindUserByEmail("test@example.com")
if err != nil {
@@ -285,7 +285,7 @@ func TestUserRequestVerification(t *testing.T) {
Body: strings.NewReader(`{"email":"test2@example.com"}`),
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
// simulate recent verification sent
user, err := app.Dao().FindUserByEmail("test2@example.com")
if err != nil {
@@ -584,11 +584,12 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":3`,
`"totalItems":4`,
`"items":[{`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
`"id":"cx9u0dh2udo8xol"`,
},
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
},
@@ -603,8 +604,9 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{
`"page":2`,
`"perPage":2`,
`"totalItems":3`,
`"totalItems":4`,
`"items":[{`,
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
},
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
@@ -630,10 +632,11 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":2`,
`"totalItems":3`,
`"items":[{`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
`"id":"cx9u0dh2udo8xol"`,
},
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
},
@@ -804,7 +807,7 @@ func TestUserCreate(t *testing.T) {
Method: http.MethodPost,
Url: "/api/users",
Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`),
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
app.Settings().EmailAuth.Enabled = false
},
ExpectedStatus: 400,
@@ -926,3 +929,185 @@ func TestUserUpdate(t *testing.T) {
scenario.Test(t)
}
}
func TestUserListExternalsAuths(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodGet,
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin + nonexisting user id",
Method: http.MethodGet,
Url: "/api/users/000000000000000/external-auths",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin + existing user id and no external auths",
Method: http.MethodGet,
Url: "/api/users/97cc3d3d-6ba2-383f-b42a-7bc84d27410c/external-auths",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`[]`,
},
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
},
{
Name: "authorized as admin + existing user id and 2 external auths",
Method: http.MethodGet,
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"abcdefghijklmn1"`,
`"id":"abcdefghijklmn0"`,
`"userId":"cx9u0dh2udo8xol"`,
},
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
},
{
Name: "authorized as user - trying to list another user external auths",
Method: http.MethodGet,
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user - owner without external auths",
Method: http.MethodGet,
Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c/external-auths",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`[]`,
},
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
},
{
Name: "authorized as user - owner with 2 external auths",
Method: http.MethodGet,
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImN4OXUwZGgydWRvOHhvbCIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.NgFYG2D7PftFW1tcfe5E2oDi_AVakDR9J6WI6VUZQfw",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"abcdefghijklmn1"`,
`"id":"abcdefghijklmn0"`,
`"userId":"cx9u0dh2udo8xol"`,
},
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestUserUnlinkExternalsAuth(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodDelete,
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin - nonexisting user id",
Method: http.MethodDelete,
Url: "/api/users/000000000000000/external-auths/google",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin - nonexisting provider",
Method: http.MethodDelete,
Url: "/api/users/cx9u0dh2udo8xol/external-auths/facebook",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin - existing provider",
Method: http.MethodDelete,
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
"OnModelBeforeDelete": 1,
"OnUserAfterUnlinkExternalAuthRequest": 1,
"OnUserBeforeUnlinkExternalAuthRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
auth, _ := app.Dao().FindExternalAuthByUserIdAndProvider("cx9u0dh2udo8xol", "google")
if auth != nil {
t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth)
}
},
},
{
Name: "authorized as user - trying to unlink another user external auth",
Method: http.MethodDelete,
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user - owner with existing external auth",
Method: http.MethodDelete,
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImN4OXUwZGgydWRvOHhvbCIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.NgFYG2D7PftFW1tcfe5E2oDi_AVakDR9J6WI6VUZQfw",
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
"OnModelBeforeDelete": 1,
"OnUserAfterUnlinkExternalAuthRequest": 1,
"OnUserBeforeUnlinkExternalAuthRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
auth, _ := app.Dao().FindExternalAuthByUserIdAndProvider("cx9u0dh2udo8xol", "google")
if auth != nil {
t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth)
}
},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
+3 -3
View File
@@ -2,7 +2,6 @@ package cmd
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
@@ -87,10 +86,11 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
if httpsAddr != "" {
schema = "https"
}
regular := color.New()
bold := color.New(color.Bold).Add(color.FgGreen)
bold.Printf("> Server started at: %s\n", color.CyanString("%s://%s", schema, serverConfig.Addr))
fmt.Printf(" - REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
fmt.Printf(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
regular.Printf(" - REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
regular.Printf(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
}
var serveErr error
+13 -8
View File
@@ -317,16 +317,21 @@ type App interface {
// authenticated user data and token.
OnUserAuthRequest() *hook.Hook[*UserAuthEvent]
// OnUserBeforeOauth2Register hook is triggered before each User OAuth2
// authentication request (when the client config has enabled new users registration).
// OnUserListExternalAuths hook is triggered on each API user's external auths list request.
//
// Could be used to additionally validate or modify the new user
// before persisting in the DB.
OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent]
// Could be used to validate or modify the response before returning it to the client.
OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent]
// OnUserAfterOauth2Register hook is triggered after each successful User
// OAuth2 authentication sign-up request (right after the new user persistence).
OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent]
// OnUserBeforeUnlinkExternalAuthRequest hook is triggered before each API user's
// external auth unlink request (after models load and before the actual relation deletion).
//
// Could be used to additionally validate the request data or implement
// completely different delete behavior (returning [hook.StopPropagation]).
OnUserBeforeUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent]
// OnUserAfterUnlinkExternalAuthRequest hook is triggered after each
// successful API user's external auth unlink request.
OnUserAfterUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent]
// ---------------------------------------------------------------
// Record API event hooks
+36 -30
View File
@@ -85,18 +85,19 @@ type BaseApp struct {
onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent]
onAdminAuthRequest *hook.Hook[*AdminAuthEvent]
// user api event hooks
onUsersListRequest *hook.Hook[*UsersListEvent]
onUserViewRequest *hook.Hook[*UserViewEvent]
onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent]
onUserAfterCreateRequest *hook.Hook[*UserCreateEvent]
onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAuthRequest *hook.Hook[*UserAuthEvent]
onUserBeforeOauth2Register *hook.Hook[*UserOauth2RegisterEvent]
onUserAfterOauth2Register *hook.Hook[*UserOauth2RegisterEvent]
// user api event hooks
onUsersListRequest *hook.Hook[*UsersListEvent]
onUserViewRequest *hook.Hook[*UserViewEvent]
onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent]
onUserAfterCreateRequest *hook.Hook[*UserCreateEvent]
onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAuthRequest *hook.Hook[*UserAuthEvent]
onUserListExternalAuths *hook.Hook[*UserListExternalAuthsEvent]
onUserBeforeUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
onUserAfterUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
// record api event hooks
onRecordsListRequest *hook.Hook[*RecordsListEvent]
@@ -180,17 +181,18 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{},
// user API event hooks
onUsersListRequest: &hook.Hook[*UsersListEvent]{},
onUserViewRequest: &hook.Hook[*UserViewEvent]{},
onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAuthRequest: &hook.Hook[*UserAuthEvent]{},
onUserBeforeOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{},
onUserAfterOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{},
onUsersListRequest: &hook.Hook[*UsersListEvent]{},
onUserViewRequest: &hook.Hook[*UserViewEvent]{},
onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAuthRequest: &hook.Hook[*UserAuthEvent]{},
onUserListExternalAuths: &hook.Hook[*UserListExternalAuthsEvent]{},
onUserBeforeUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
onUserAfterUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
// record API event hooks
onRecordsListRequest: &hook.Hook[*RecordsListEvent]{},
@@ -611,12 +613,16 @@ func (app *BaseApp) OnUserAuthRequest() *hook.Hook[*UserAuthEvent] {
return app.onUserAuthRequest
}
func (app *BaseApp) OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
return app.onUserBeforeOauth2Register
func (app *BaseApp) OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent] {
return app.onUserListExternalAuths
}
func (app *BaseApp) OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
return app.onUserAfterOauth2Register
func (app *BaseApp) OnUserBeforeUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
return app.onUserBeforeUnlinkExternalAuthRequest
}
func (app *BaseApp) OnUserAfterUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
return app.onUserAfterUnlinkExternalAuthRequest
}
// -------------------------------------------------------------------
@@ -710,7 +716,7 @@ func (app *BaseApp) initLogsDB() error {
return connectErr
}
app.logsDao = app.createDao(app.logsDB)
app.logsDao = daos.New(app.logsDB)
return nil
}
@@ -734,12 +740,12 @@ func (app *BaseApp) initDataDB() error {
}
}
app.dao = app.createDao(app.db)
app.dao = app.createDaoWithHooks(app.db)
return nil
}
func (app *BaseApp) createDao(db dbx.Builder) *daos.Dao {
func (app *BaseApp) createDaoWithHooks(db dbx.Builder) *daos.Dao {
dao := daos.New(db)
dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
+8 -4
View File
@@ -319,12 +319,16 @@ func TestBaseAppGetters(t *testing.T) {
t.Fatalf("Getter app.OnUserAuthRequest does not match or nil (%v vs %v)", app.OnUserAuthRequest(), app.onUserAuthRequest)
}
if app.onUserBeforeOauth2Register != app.OnUserBeforeOauth2Register() || app.OnUserBeforeOauth2Register() == nil {
t.Fatalf("Getter app.OnUserBeforeOauth2Register does not match or nil (%v vs %v)", app.OnUserBeforeOauth2Register(), app.onUserBeforeOauth2Register)
if app.onUserListExternalAuths != app.OnUserListExternalAuths() || app.OnUserListExternalAuths() == nil {
t.Fatalf("Getter app.OnUserListExternalAuths does not match or nil (%v vs %v)", app.OnUserListExternalAuths(), app.onUserListExternalAuths)
}
if app.onUserAfterOauth2Register != app.OnUserAfterOauth2Register() || app.OnUserAfterOauth2Register() == nil {
t.Fatalf("Getter app.OnUserAfterOauth2Register does not match or nil (%v vs %v)", app.OnUserAfterOauth2Register(), app.onUserAfterOauth2Register)
if app.onUserBeforeUnlinkExternalAuthRequest != app.OnUserBeforeUnlinkExternalAuthRequest() || app.OnUserBeforeUnlinkExternalAuthRequest() == nil {
t.Fatalf("Getter app.OnUserBeforeUnlinkExternalAuthRequest does not match or nil (%v vs %v)", app.OnUserBeforeUnlinkExternalAuthRequest(), app.onUserBeforeUnlinkExternalAuthRequest)
}
if app.onUserAfterUnlinkExternalAuthRequest != app.OnUserAfterUnlinkExternalAuthRequest() || app.OnUserAfterUnlinkExternalAuthRequest() == nil {
t.Fatalf("Getter app.OnUserAfterUnlinkExternalAuthRequest does not match or nil (%v vs %v)", app.OnUserAfterUnlinkExternalAuthRequest(), app.onUserAfterUnlinkExternalAuthRequest)
}
if app.onRecordsListRequest != app.OnRecordsListRequest() || app.OnRecordsListRequest() == nil {
+10 -5
View File
@@ -4,7 +4,6 @@ import (
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/subscriptions"
@@ -180,10 +179,16 @@ type UserAuthEvent struct {
Meta any
}
type UserOauth2RegisterEvent struct {
HttpContext echo.Context
User *models.User
AuthData *auth.AuthUser
type UserListExternalAuthsEvent struct {
HttpContext echo.Context
User *models.User
ExternalAuths []*models.ExternalAuth
}
type UserUnlinkExternalAuthEvent struct {
HttpContext echo.Context
User *models.User
ExternalAuth *models.ExternalAuth
}
// -------------------------------------------------------------------
+18 -2
View File
@@ -35,6 +35,8 @@ type Settings struct {
FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"`
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
DiscordAuth AuthProviderConfig `form:"discordAuth" json:"discordAuth"`
TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
}
// NewSettings creates and returns a new default Settings instance.
@@ -106,6 +108,14 @@ func NewSettings() *Settings {
Enabled: false,
AllowRegistrations: true,
},
DiscordAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
TwitterAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
}
}
@@ -130,6 +140,8 @@ func (s *Settings) Validate() error {
validation.Field(&s.FacebookAuth),
validation.Field(&s.GithubAuth),
validation.Field(&s.GitlabAuth),
validation.Field(&s.DiscordAuth),
validation.Field(&s.TwitterAuth),
)
}
@@ -178,6 +190,8 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.FacebookAuth.ClientSecret,
&clone.GithubAuth.ClientSecret,
&clone.GitlabAuth.ClientSecret,
&clone.DiscordAuth.ClientSecret,
&clone.TwitterAuth.ClientSecret,
}
// mask all sensitive fields
@@ -201,6 +215,8 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameFacebook: s.FacebookAuth,
auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth,
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
}
}
@@ -215,7 +231,7 @@ type TokenConfig struct {
func (c TokenConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)),
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(31536000)),
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(63072000)),
)
}
@@ -285,7 +301,7 @@ func (c MetaConfig) Validate() error {
validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
validation.Field(&c.AppUrl, validation.Required, is.URL),
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
validation.Field(&c.SenderAddress, is.Email, validation.Required),
validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
validation.Field(&c.VerificationTemplate, validation.Required),
validation.Field(&c.ResetPasswordTemplate, validation.Required),
validation.Field(&c.ConfirmEmailChangeTemplate, validation.Required),
+25 -4
View File
@@ -37,6 +37,10 @@ func TestSettingsValidate(t *testing.T) {
s.GithubAuth.ClientId = ""
s.GitlabAuth.Enabled = true
s.GitlabAuth.ClientId = ""
s.DiscordAuth.Enabled = true
s.DiscordAuth.ClientId = ""
s.TwitterAuth.Enabled = true
s.TwitterAuth.ClientId = ""
// check if Validate() is triggering the members validate methods.
err := s.Validate()
@@ -60,6 +64,7 @@ func TestSettingsValidate(t *testing.T) {
`"facebookAuth":{`,
`"githubAuth":{`,
`"gitlabAuth":{`,
`"discordAuth":{`,
}
errBytes, _ := json.Marshal(err)
@@ -98,6 +103,10 @@ func TestSettingsMerge(t *testing.T) {
s2.GithubAuth.ClientId = "github_test"
s2.GitlabAuth.Enabled = true
s2.GitlabAuth.ClientId = "gitlab_test"
s2.DiscordAuth.Enabled = true
s2.DiscordAuth.ClientId = "discord_test"
s2.TwitterAuth.Enabled = true
s2.TwitterAuth.ClientId = "twitter_test"
if err := s1.Merge(s2); err != nil {
t.Fatal(err)
@@ -163,6 +172,8 @@ func TestSettingsRedactClone(t *testing.T) {
s1.FacebookAuth.ClientSecret = "test123"
s1.GithubAuth.ClientSecret = "test123"
s1.GitlabAuth.ClientSecret = "test123"
s1.DiscordAuth.ClientSecret = "test123"
s1.TwitterAuth.ClientSecret = "test123"
s2, err := s1.RedactClone()
if err != nil {
@@ -174,7 +185,7 @@ func TestSettingsRedactClone(t *testing.T) {
t.Fatal(err)
}
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/users/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/users/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/users/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/users/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/users/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/users/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"discordAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"twitterAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`
if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected %v, got \n%v", expected, encodedStr)
@@ -189,6 +200,8 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.GithubAuth.ClientId = "github_test"
s.GitlabAuth.ClientId = "gitlab_test"
s.GitlabAuth.Enabled = true
s.DiscordAuth.ClientId = "discord_test"
s.TwitterAuth.ClientId = "twitter_test"
result := s.NamedAuthProviderConfigs()
@@ -197,7 +210,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
t.Fatal(err)
}
expected := `{"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"}}`
expected := `{"discord":{"enabled":false,"allowRegistrations":true,"clientId":"discord_test"},"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"},"twitter":{"enabled":false,"allowRegistrations":true,"clientId":"twitter_test"}}`
if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected the same serialization, got %v", encodedStr)
@@ -217,15 +230,23 @@ func TestTokenConfigValidate(t *testing.T) {
// invalid data
{
core.TokenConfig{
Secret: "test",
Secret: strings.Repeat("a", 5),
Duration: 4,
},
true,
},
// valid secret but invalid duration
{
core.TokenConfig{
Secret: strings.Repeat("a", 30),
Duration: 63072000 + 1,
},
true,
},
// valid data
{
core.TokenConfig{
Secret: "testtesttesttesttesttesttestte",
Secret: strings.Repeat("a", 30),
Duration: 100,
},
false,
+108
View File
@@ -0,0 +1,108 @@
package daos
import (
"errors"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
)
// ExternalAuthQuery returns a new ExternalAuth select query.
func (dao *Dao) ExternalAuthQuery() *dbx.SelectQuery {
return dao.ModelQuery(&models.ExternalAuth{})
}
/// FindAllExternalAuthsByUserId returns all ExternalAuth models
/// linked to the provided userId.
func (dao *Dao) FindAllExternalAuthsByUserId(userId string) ([]*models.ExternalAuth, error) {
auths := []*models.ExternalAuth{}
err := dao.ExternalAuthQuery().
AndWhere(dbx.HashExp{"userId": userId}).
OrderBy("created ASC").
All(&auths)
if err != nil {
return nil, err
}
return auths, nil
}
// FindExternalAuthByProvider returns the first available
// ExternalAuth model for the specified provider and providerId.
func (dao *Dao) FindExternalAuthByProvider(provider, providerId string) (*models.ExternalAuth, error) {
model := &models.ExternalAuth{}
err := dao.ExternalAuthQuery().
AndWhere(dbx.Not(dbx.HashExp{"providerId": ""})). // exclude empty providerIds
AndWhere(dbx.HashExp{
"provider": provider,
"providerId": providerId,
}).
Limit(1).
One(model)
if err != nil {
return nil, err
}
return model, nil
}
// FindExternalAuthByUserIdAndProvider returns the first available
// ExternalAuth model for the specified userId and provider.
func (dao *Dao) FindExternalAuthByUserIdAndProvider(userId, provider string) (*models.ExternalAuth, error) {
model := &models.ExternalAuth{}
err := dao.ExternalAuthQuery().
AndWhere(dbx.HashExp{
"userId": userId,
"provider": provider,
}).
Limit(1).
One(model)
if err != nil {
return nil, err
}
return model, nil
}
// SaveExternalAuth upserts the provided ExternalAuth model.
func (dao *Dao) SaveExternalAuth(model *models.ExternalAuth) error {
// extra check the model data in case the provider's API response
// has changed and no longer returns the expected fields
if model.UserId == "" || model.Provider == "" || model.ProviderId == "" {
return errors.New("Missing required ExternalAuth fields.")
}
return dao.Save(model)
}
// DeleteExternalAuth deletes the provided ExternalAuth model.
//
// The delete may fail if the linked user doesn't have an email and
// there are no other linked ExternalAuth models available.
func (dao *Dao) DeleteExternalAuth(model *models.ExternalAuth) error {
user, err := dao.FindUserById(model.UserId)
if err != nil {
return err
}
// if the user doesn't have an email, make sure that there
// is at least one other external auth relation available
if user.Email == "" {
allExternalAuths, err := dao.FindAllExternalAuthsByUserId(user.Id)
if err != nil {
return err
}
if len(allExternalAuths) <= 1 {
return errors.New("You cannot delete the only available external auth relation because the user doesn't have an email address.")
}
}
return dao.Delete(model)
}
+195
View File
@@ -0,0 +1,195 @@
package daos_test
import (
"testing"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tests"
)
func TestExternalAuthQuery(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
expected := "SELECT {{_externalAuths}}.* FROM `_externalAuths`"
sql := app.Dao().ExternalAuthQuery().Build().SQL()
if sql != expected {
t.Errorf("Expected sql %s, got %s", expected, sql)
}
}
func TestFindAllExternalAuthsByUserId(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
userId string
expectedCount int
}{
{"", 0},
{"missing", 0},
{"97cc3d3d-6ba2-383f-b42a-7bc84d27410c", 0},
{"cx9u0dh2udo8xol", 2},
}
for i, s := range scenarios {
auths, err := app.Dao().FindAllExternalAuthsByUserId(s.userId)
if err != nil {
t.Errorf("(%d) Unexpected error %v", i, err)
continue
}
if len(auths) != s.expectedCount {
t.Errorf("(%d) Expected %d auths, got %d", i, s.expectedCount, len(auths))
}
for _, auth := range auths {
if auth.UserId != s.userId {
t.Errorf("(%d) Expected all auths to be linked to userId %s, got %v", i, s.userId, auth)
}
}
}
}
func TestFindExternalAuthByProvider(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
provider string
providerId string
expectedId string
}{
{"", "", ""},
{"github", "", ""},
{"github", "id1", ""},
{"github", "id2", ""},
{"google", "id1", "abcdefghijklmn0"},
{"gitlab", "id2", "abcdefghijklmn1"},
}
for i, s := range scenarios {
auth, err := app.Dao().FindExternalAuthByProvider(s.provider, s.providerId)
hasErr := err != nil
expectErr := s.expectedId == ""
if hasErr != expectErr {
t.Errorf("(%d) Expected hasErr %v, got %v", i, expectErr, err)
continue
}
if auth != nil && auth.Id != s.expectedId {
t.Errorf("(%d) Expected external auth with ID %s, got \n%v", i, s.expectedId, auth)
}
}
}
func TestFindExternalAuthByUserIdAndProvider(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
userId string
provider string
expectedId string
}{
{"", "", ""},
{"", "github", ""},
{"123456", "github", ""}, // missing user and provider record
{"123456", "google", ""}, // missing user but existing provider record
{"97cc3d3d-6ba2-383f-b42a-7bc84d27410c", "google", ""},
{"cx9u0dh2udo8xol", "google", "abcdefghijklmn0"},
{"cx9u0dh2udo8xol", "gitlab", "abcdefghijklmn1"},
}
for i, s := range scenarios {
auth, err := app.Dao().FindExternalAuthByUserIdAndProvider(s.userId, s.provider)
hasErr := err != nil
expectErr := s.expectedId == ""
if hasErr != expectErr {
t.Errorf("(%d) Expected hasErr %v, got %v", i, expectErr, err)
continue
}
if auth != nil && auth.Id != s.expectedId {
t.Errorf("(%d) Expected external auth with ID %s, got \n%v", i, s.expectedId, auth)
}
}
}
func TestSaveExternalAuth(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
// save with empty provider data
emptyAuth := &models.ExternalAuth{}
if err := app.Dao().SaveExternalAuth(emptyAuth); err == nil {
t.Fatal("Expected error, got nil")
}
auth := &models.ExternalAuth{
UserId: "97cc3d3d-6ba2-383f-b42a-7bc84d27410c",
Provider: "test",
ProviderId: "test_id",
}
if err := app.Dao().SaveExternalAuth(auth); err != nil {
t.Fatal(err)
}
// check if it was really saved
foundAuth, err := app.Dao().FindExternalAuthByProvider("test", "test_id")
if err != nil {
t.Fatal(err)
}
if auth.Id != foundAuth.Id {
t.Fatalf("Expected ExternalAuth with id %s, got \n%v", auth.Id, foundAuth)
}
}
func TestDeleteExternalAuth(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
user, err := app.Dao().FindUserById("cx9u0dh2udo8xol")
if err != nil {
t.Fatal(err)
}
auths, err := app.Dao().FindAllExternalAuthsByUserId(user.Id)
if err != nil {
t.Fatal(err)
}
if err := app.Dao().DeleteExternalAuth(auths[0]); err != nil {
t.Fatalf("Failed to delete the first ExternalAuth relation, got \n%v", err)
}
if err := app.Dao().DeleteExternalAuth(auths[1]); err == nil {
t.Fatal("Expected delete to fail, got nil")
}
// update the user model and try again
user.Email = "test_new@example.com"
if err := app.Dao().SaveUser(user); err != nil {
t.Fatal(err)
}
// try to delete auths[1] again
if err := app.Dao().DeleteExternalAuth(auths[1]); err != nil {
t.Fatalf("Failed to delete the last ExternalAuth relation, got \n%v", err)
}
// check if the relations were really deleted
newAuths, err := app.Dao().FindAllExternalAuthsByUserId(user.Id)
if err != nil {
t.Fatal(err)
}
if len(newAuths) != 0 {
t.Fatalf("Expected all user %s ExternalAuth relations to be deleted, got \n%v", user.Id, newAuths)
}
}
+30 -1
View File
@@ -114,12 +114,41 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
expandData := model.GetExpand()
// normalize and set the expanded relations
// normalize access to the previously expanded rel records (if any)
var oldExpandedRels []*models.Record
switch v := expandData[relField.Name].(type) {
case nil:
// no old expands
case *models.Record:
oldExpandedRels = []*models.Record{v}
case []*models.Record:
oldExpandedRels = v
}
// merge expands
for _, oldExpandedRel := range oldExpandedRels {
// find a matching rel record
for _, rel := range validRels {
if rel.Id != oldExpandedRel.Id {
continue
}
oldRelExpand := oldExpandedRel.GetExpand()
newRelExpand := rel.GetExpand()
for k, v := range oldRelExpand {
newRelExpand[k] = v
}
rel.SetExpand(newRelExpand)
}
}
// update the expanded data
if relFieldOptions.MaxSelect == 1 {
expandData[relField.Name] = validRels[0]
} else {
expandData[relField.Name] = validRels
}
model.SetExpand(expandData)
}
+12
View File
@@ -108,6 +108,18 @@ func TestExpandRecords(t *testing.T) {
9,
0,
},
// expand multiple relations sharing a common root path
{
[]string{
"i15r5aa28ad06c8",
},
[]string{"manyrels.onerel.manyrels.onerel", "manyrels.onerel.onerel"},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c, ids, nil)
},
4,
0,
},
// single expand
{
[]string{
+2 -1
View File
@@ -94,7 +94,7 @@ func (dao *Dao) FindUserById(id string) (*models.User, error) {
return model, nil
}
// FindUserByEmail finds a single User model by its email address.
// FindUserByEmail finds a single User model by its non-empty email address.
//
// This method also auto loads the related user profile record
// into the found model.
@@ -102,6 +102,7 @@ func (dao *Dao) FindUserByEmail(email string) (*models.User, error) {
model := &models.User{}
err := dao.UserQuery().
AndWhere(dbx.Not(dbx.HashExp{"email": ""})).
AndWhere(dbx.HashExp{"email": email}).
Limit(1).
One(model)
+1
View File
@@ -110,6 +110,7 @@ func TestFindUserByEmail(t *testing.T) {
email string
expectError bool
}{
{"", true},
{"invalid", true},
{"missing@example.com", true},
{"test@example.com", false},
+23 -3
View File
@@ -2,20 +2,40 @@ package main
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
func defaultPublicDir() string {
if strings.HasPrefix(os.Args[0], os.TempDir()) {
// most likely ran with go run
return "./pb_public"
}
return filepath.Join(os.Args[0], "../pb_public")
}
func main() {
app := pocketbase.New()
var publicDirFlag string
// add "--publicDir" option flag
app.RootCmd.PersistentFlags().StringVar(
&publicDirFlag,
"publicDir",
defaultPublicDir(),
"the directory to serve static files",
)
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// serves static files from the provided public dir (if exists)
subFs := echo.MustSubFS(e.Router.Filesystem, "pb_public")
e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false))
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDirFlag), false))
return nil
})
+6 -6
View File
@@ -22,15 +22,15 @@ type AdminLogin struct {
//
// NB! App is a required struct member.
type AdminLoginConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewAdminLogin creates a new [AdminLogin] form with initializer
// config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewAdminLoginWithConfig] with explicitly set TxDao.
// [NewAdminLoginWithConfig] with explicitly set Dao.
func NewAdminLogin(app core.App) *AdminLogin {
return NewAdminLoginWithConfig(AdminLoginConfig{
App: app,
@@ -46,8 +46,8 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin {
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -68,7 +68,7 @@ func (form *AdminLogin) Submit() (*models.Admin, error) {
return nil, err
}
admin, err := form.config.TxDao.FindAdminByEmail(form.Email)
admin, err := form.config.Dao.FindAdminByEmail(form.Email)
if err != nil {
return nil, err
}
+8 -8
View File
@@ -21,15 +21,15 @@ type AdminPasswordResetConfirm struct {
//
// NB! App is required struct member.
type AdminPasswordResetConfirmConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm]
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewAdminPasswordResetConfirmWithConfig] with explicitly set TxDao.
// [NewAdminPasswordResetConfirmWithConfig] with explicitly set Dao.
func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm {
return NewAdminPasswordResetConfirmWithConfig(AdminPasswordResetConfirmConfig{
App: app,
@@ -45,8 +45,8 @@ func NewAdminPasswordResetConfirmWithConfig(config AdminPasswordResetConfirmConf
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -67,7 +67,7 @@ func (form *AdminPasswordResetConfirm) checkToken(value any) error {
return nil // nothing to check
}
admin, err := form.config.TxDao.FindAdminByToken(
admin, err := form.config.Dao.FindAdminByToken(
v,
form.config.App.Settings().AdminPasswordResetToken.Secret,
)
@@ -85,7 +85,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
return nil, err
}
admin, err := form.config.TxDao.FindAdminByToken(
admin, err := form.config.Dao.FindAdminByToken(
form.Token,
form.config.App.Settings().AdminPasswordResetToken.Secret,
)
@@ -97,7 +97,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
return nil, err
}
if err := form.config.TxDao.SaveAdmin(admin); err != nil {
if err := form.config.Dao.SaveAdmin(admin); err != nil {
return nil, err
}
+7 -7
View File
@@ -24,7 +24,7 @@ type AdminPasswordResetRequest struct {
// NB! App is required struct member.
type AdminPasswordResetRequestConfig struct {
App core.App
TxDao *daos.Dao
Dao *daos.Dao
ResendThreshold float64 // in seconds
}
@@ -32,7 +32,7 @@ type AdminPasswordResetRequestConfig struct {
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewAdminPasswordResetRequestWithConfig] with explicitly set TxDao.
// [NewAdminPasswordResetRequestWithConfig] with explicitly set Dao.
func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest {
return NewAdminPasswordResetRequestWithConfig(AdminPasswordResetRequestConfig{
App: app,
@@ -49,8 +49,8 @@ func NewAdminPasswordResetRequestWithConfig(config AdminPasswordResetRequestConf
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -65,7 +65,7 @@ func (form *AdminPasswordResetRequest) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
),
)
}
@@ -77,7 +77,7 @@ func (form *AdminPasswordResetRequest) Submit() error {
return err
}
admin, err := form.config.TxDao.FindAdminByEmail(form.Email)
admin, err := form.config.Dao.FindAdminByEmail(form.Email)
if err != nil {
return err
}
@@ -95,5 +95,5 @@ func (form *AdminPasswordResetRequest) Submit() error {
// update last sent timestamp
admin.LastResetSentAt = types.NowDateTime()
return form.config.TxDao.SaveAdmin(admin)
return form.config.Dao.SaveAdmin(admin)
}
+8 -8
View File
@@ -25,8 +25,8 @@ type AdminUpsert struct {
//
// NB! App is a required struct member.
type AdminUpsertConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewAdminUpsert creates a new [AdminUpsert] form with initializer
@@ -34,7 +34,7 @@ type AdminUpsertConfig struct {
// (for create you could pass a pointer to an empty Admin - `&models.Admin{}`).
//
// If you want to submit the form as part of another transaction, use
// [NewAdminUpsertWithConfig] with explicitly set TxDao.
// [NewAdminUpsertWithConfig] with explicitly set Dao.
func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert {
return NewAdminUpsertWithConfig(AdminUpsertConfig{
App: app,
@@ -54,8 +54,8 @@ func NewAdminUpsertWithConfig(config AdminUpsertConfig, admin *models.Admin) *Ad
panic("Invalid initializer config or nil upsert model.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
// load defaults
@@ -86,7 +86,7 @@ func (form *AdminUpsert) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
validation.By(form.checkUniqueEmail),
),
validation.Field(
@@ -105,7 +105,7 @@ func (form *AdminUpsert) Validate() error {
func (form *AdminUpsert) checkUniqueEmail(value any) error {
v, _ := value.(string)
if form.config.TxDao.IsAdminEmailUnique(v, form.admin.Id) {
if form.config.Dao.IsAdminEmailUnique(v, form.admin.Id) {
return nil
}
@@ -135,6 +135,6 @@ func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error {
}
return runInterceptors(func() error {
return form.config.TxDao.SaveAdmin(form.admin)
return form.config.Dao.SaveAdmin(form.admin)
}, interceptors...)
}
+10 -10
View File
@@ -36,8 +36,8 @@ type CollectionUpsert struct {
//
// NB! App is a required struct member.
type CollectionUpsertConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer
@@ -45,7 +45,7 @@ type CollectionUpsertConfig struct {
// (for create you could pass a pointer to an empty Collection - `&models.Collection{}`).
//
// If you want to submit the form as part of another transaction, use
// [NewCollectionUpsertWithConfig] with explicitly set TxDao.
// [NewCollectionUpsertWithConfig] with explicitly set Dao.
func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert {
return NewCollectionUpsertWithConfig(CollectionUpsertConfig{
App: app,
@@ -65,8 +65,8 @@ func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *mo
panic("Invalid initializer config or nil upsert model.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
// load defaults
@@ -130,11 +130,11 @@ func (form *CollectionUpsert) Validate() error {
func (form *CollectionUpsert) checkUniqueName(value any) error {
v, _ := value.(string)
if !form.config.TxDao.IsCollectionNameUnique(v, form.collection.Id) {
if !form.config.Dao.IsCollectionNameUnique(v, form.collection.Id) {
return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).")
}
if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.TxDao.HasTable(v) {
if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.Dao.HasTable(v) {
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
}
@@ -191,7 +191,7 @@ func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) erro
continue
}
if _, err := form.config.TxDao.FindCollectionByNameOrId(options.CollectionId); err != nil {
if _, err := form.config.Dao.FindCollectionByNameOrId(options.CollectionId); err != nil {
return validation.Errors{fmt.Sprint(i): validation.NewError(
"validation_field_invalid_relation",
"The relation collection doesn't exist.",
@@ -228,7 +228,7 @@ func (form *CollectionUpsert) checkRule(value any) error {
}
dummy := &models.Collection{Schema: form.Schema}
r := resolvers.NewRecordFieldResolver(form.config.TxDao, dummy, nil)
r := resolvers.NewRecordFieldResolver(form.config.Dao, dummy, nil)
_, err := search.FilterData(*v).BuildExpr(r)
if err != nil {
@@ -273,6 +273,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
form.collection.DeleteRule = form.DeleteRule
return runInterceptors(func() error {
return form.config.TxDao.SaveCollection(form.collection)
return form.config.Dao.SaveCollection(form.collection)
}, interceptors...)
}
+8 -8
View File
@@ -24,15 +24,15 @@ type CollectionsImport struct {
//
// NB! App is a required struct member.
type CollectionsImportConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewCollectionsImport creates a new [CollectionsImport] form with
// initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewCollectionsImportWithConfig] with explicitly set TxDao.
// [NewCollectionsImportWithConfig] with explicitly set Dao.
func NewCollectionsImport(app core.App) *CollectionsImport {
return NewCollectionsImportWithConfig(CollectionsImportConfig{
App: app,
@@ -48,8 +48,8 @@ func NewCollectionsImportWithConfig(config CollectionsImportConfig) *Collections
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -79,7 +79,7 @@ func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc) error {
}
return runInterceptors(func() error {
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
importErr := txDao.ImportCollections(
form.Collections,
form.DeleteMissing,
@@ -122,8 +122,8 @@ func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, map
}
upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{
App: form.config.App,
TxDao: txDao,
App: form.config.App,
Dao: txDao,
}, upsertModel)
// load form fields with the refreshed collection state
+8 -8
View File
@@ -37,8 +37,8 @@ type RecordUpsert struct {
//
// NB! App is required struct member.
type RecordUpsertConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewRecordUpsert creates a new [RecordUpsert] form with initializer
@@ -46,7 +46,7 @@ type RecordUpsertConfig struct {
// (for create you could pass a pointer to an empty Record - `models.NewRecord(collection)`).
//
// If you want to submit the form as part of another transaction, use
// [NewRecordUpsertWithConfig] with explicitly set TxDao.
// [NewRecordUpsertWithConfig] with explicitly set Dao.
func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
return NewRecordUpsertWithConfig(RecordUpsertConfig{
App: app,
@@ -68,8 +68,8 @@ func NewRecordUpsertWithConfig(config RecordUpsertConfig, record *models.Record)
panic("Invalid initializer config or nil upsert model.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
form.Id = record.Id
@@ -298,7 +298,7 @@ func (form *RecordUpsert) Validate() error {
// record data validator
dataValidator := validators.NewRecordDataValidator(
form.config.TxDao,
form.config.Dao,
form.record,
form.filesToUpload,
)
@@ -328,7 +328,7 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
return err
}
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
tx, ok := txDao.DB().(*dbx.Tx)
if !ok {
return errors.New("failed to get transaction db")
@@ -378,7 +378,7 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
}
return runInterceptors(func() error {
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
// persist record model
if err := txDao.SaveRecord(form.record); err != nil {
return err
+1 -1
View File
@@ -139,7 +139,7 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
t.Fatalf("Expect only 1 manyfiles to remain, got \n%v", manyfiles)
}
onlyimages, ok := form.Data["onlyimages"]
onlyimages := form.Data["onlyimages"]
if len(list.ToUniqueStringSlice(onlyimages)) != 0 {
t.Fatalf("Expect onlyimages field to be deleted, got \n%v", onlyimages)
}
+10 -10
View File
@@ -20,16 +20,16 @@ type SettingsUpsert struct {
//
// NB! App is required struct member.
type SettingsUpsertConfig struct {
App core.App
TxDao *daos.Dao
TxLogsDao *daos.Dao
App core.App
Dao *daos.Dao
LogsDao *daos.Dao
}
// NewSettingsUpsert creates a new [SettingsUpsert] form with initializer
// config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewSettingsUpsertWithConfig] with explicitly set TxDao.
// [NewSettingsUpsertWithConfig] with explicitly set Dao.
func NewSettingsUpsert(app core.App) *SettingsUpsert {
return NewSettingsUpsertWithConfig(SettingsUpsertConfig{
App: app,
@@ -45,12 +45,12 @@ func NewSettingsUpsertWithConfig(config SettingsUpsertConfig) *SettingsUpsert {
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
if form.config.TxLogsDao == nil {
form.config.TxLogsDao = form.config.App.LogsDao()
if form.config.LogsDao == nil {
form.config.LogsDao = form.config.App.LogsDao()
}
// load the application settings into the form
@@ -78,7 +78,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
encryptionKey := os.Getenv(form.config.App.EncryptionEnv())
return runInterceptors(func() error {
saveErr := form.config.TxDao.SaveParam(
saveErr := form.config.Dao.SaveParam(
models.ParamAppSettings,
form.Settings,
encryptionKey,
@@ -88,7 +88,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
}
// explicitly trigger old logs deletion
form.config.TxLogsDao.DeleteOldRequests(
form.config.LogsDao.DeleteOldRequests(
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
)
+1 -1
View File
@@ -34,7 +34,7 @@ func (form *TestEmailSend) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
),
validation.Field(
&form.Template,
+8 -8
View File
@@ -20,8 +20,8 @@ type UserEmailChangeConfirm struct {
//
// NB! App is required struct member.
type UserEmailChangeConfirmConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserEmailChangeConfirm creates a new [UserEmailChangeConfirm]
@@ -29,7 +29,7 @@ type UserEmailChangeConfirmConfig struct {
//
// This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
func NewUserEmailChangeConfirm(app core.App) *UserEmailChangeConfirm {
return NewUserEmailChangeConfirmWithConfig(UserEmailChangeConfirmConfig{
App: app,
@@ -45,8 +45,8 @@ func NewUserEmailChangeConfirmWithConfig(config UserEmailChangeConfirmConfig) *U
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -103,12 +103,12 @@ func (form *UserEmailChangeConfirm) parseToken(token string) (*models.User, stri
}
// ensure that there aren't other users with the new email
if !form.config.TxDao.IsUserEmailUnique(newEmail, "") {
if !form.config.Dao.IsUserEmailUnique(newEmail, "") {
return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail)
}
// verify that the token is not expired and its signature is valid
user, err := form.config.TxDao.FindUserByToken(
user, err := form.config.Dao.FindUserByToken(
token,
form.config.App.Settings().UserEmailChangeToken.Secret,
)
@@ -135,7 +135,7 @@ func (form *UserEmailChangeConfirm) Submit() (*models.User, error) {
user.Verified = true
user.RefreshTokenKey() // invalidate old tokens
if err := form.config.TxDao.SaveUser(user); err != nil {
if err := form.config.Dao.SaveUser(user); err != nil {
return nil, err
}
+7 -7
View File
@@ -21,15 +21,15 @@ type UserEmailChangeRequest struct {
//
// NB! App is required struct member.
type UserEmailChangeRequestConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserEmailChangeRequest creates a new [UserEmailChangeRequest]
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
func NewUserEmailChangeRequest(app core.App, user *models.User) *UserEmailChangeRequest {
return NewUserEmailChangeRequestWithConfig(UserEmailChangeRequestConfig{
App: app,
@@ -48,8 +48,8 @@ func NewUserEmailChangeRequestWithConfig(config UserEmailChangeRequestConfig, us
panic("Invalid initializer config or nil user model.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -62,7 +62,7 @@ func (form *UserEmailChangeRequest) Validate() error {
&form.NewEmail,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
validation.By(form.checkUniqueEmail),
),
)
@@ -71,7 +71,7 @@ func (form *UserEmailChangeRequest) Validate() error {
func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error {
v, _ := value.(string)
if !form.config.TxDao.IsUserEmailUnique(v, "") {
if !form.config.Dao.IsUserEmailUnique(v, "") {
return validation.NewError("validation_user_email_exists", "User email already exists.")
}
+6 -6
View File
@@ -20,8 +20,8 @@ type UserEmailLogin struct {
//
// NB! App is required struct member.
type UserEmailLoginConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserEmailLogin creates a new [UserEmailLogin] form with
@@ -29,7 +29,7 @@ type UserEmailLoginConfig struct {
//
// This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use
// [NewUserEmailLoginWithConfig] with explicitly set TxDao.
// [NewUserEmailLoginWithConfig] with explicitly set Dao.
func NewUserEmailLogin(app core.App) *UserEmailLogin {
return NewUserEmailLoginWithConfig(UserEmailLoginConfig{
App: app,
@@ -45,8 +45,8 @@ func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin {
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -67,7 +67,7 @@ func (form *UserEmailLogin) Submit() (*models.User, error) {
return nil, err
}
user, err := form.config.TxDao.FindUserByEmail(form.Email)
user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil {
return nil, err
}
+71 -44
View File
@@ -35,15 +35,15 @@ type UserOauth2Login struct {
//
// NB! App is required struct member.
type UserOauth2LoginConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserOauth2Login creates a new [UserOauth2Login] form with
// initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserOauth2LoginWithConfig] with explicitly set TxDao.
// [NewUserOauth2LoginWithConfig] with explicitly set Dao.
func NewUserOauth2Login(app core.App) *UserOauth2Login {
return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{
App: app,
@@ -59,8 +59,8 @@ func NewUserOauth2LoginWithConfig(config UserOauth2LoginConfig) *UserOauth2Login
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -99,8 +99,11 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, nil, err
}
// load provider configuration
config := form.config.App.Settings().NamedAuthProviderConfigs()[form.Provider]
config.SetupProvider(provider)
if err := config.SetupProvider(provider); err != nil {
return nil, nil, err
}
provider.SetRedirectUrl(form.RedirectUrl)
@@ -113,55 +116,79 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, nil, err
}
// fetch auth user
// fetch external auth user
authData, err := provider.FetchAuthUser(token)
if err != nil {
return nil, nil, err
}
// login/register the auth user
user, _ := form.config.TxDao.FindUserByEmail(authData.Email)
if user != nil {
// update the existing user's verified state
if !user.Verified {
var user *models.User
// check for existing relation with the external auth user
rel, _ := form.config.Dao.FindExternalAuthByProvider(form.Provider, authData.Id)
if rel != nil {
user, err = form.config.Dao.FindUserById(rel.UserId)
if err != nil {
return nil, authData, err
}
} else if authData.Email != "" {
// look for an existing user by the external user's email
user, _ = form.config.Dao.FindUserByEmail(authData.Email)
}
if user == nil && !config.AllowRegistrations {
return nil, authData, errors.New("New users registration is not allowed for the authorized provider.")
}
saveErr := form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
if user == nil {
user = &models.User{}
user.Verified = true
if err := form.config.TxDao.SaveUser(user); err != nil {
return nil, authData, err
user.Email = authData.Email
user.SetPassword(security.RandomString(30))
// create the new user
if err := txDao.SaveUser(user); err != nil {
return err
}
} else {
// update the existing user empty email if the authData has one
// (this in case previously the user was created with
// an OAuth2 provider that didn't return an email address)
if user.Email == "" && authData.Email != "" {
user.Email = authData.Email
if err := txDao.SaveUser(user); err != nil {
return err
}
}
// update the existing user verified state
// (only if the user doesn't have an email or the user email match with the one in authData)
if !user.Verified && (user.Email == "" || user.Email == authData.Email) {
user.Verified = true
if err := txDao.SaveUser(user); err != nil {
return err
}
}
}
return user, authData, nil
}
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.")
}
// create ExternalAuth relation if missing
if rel == nil {
rel = &models.ExternalAuth{
UserId: user.Id,
Provider: form.Provider,
ProviderId: authData.Id,
}
if err := txDao.SaveExternalAuth(rel); err != nil {
return err
}
}
// create new user
user = &models.User{Verified: true}
upsertForm := NewUserUpsertWithConfig(UserUpsertConfig{
App: form.config.App,
TxDao: form.config.TxDao,
}, user)
upsertForm.Email = authData.Email
upsertForm.Password = security.RandomString(30)
upsertForm.PasswordConfirm = upsertForm.Password
return nil
})
event := &core.UserOauth2RegisterEvent{
User: user,
AuthData: authData,
}
if err := form.config.App.OnUserBeforeOauth2Register().Trigger(event); err != nil {
return nil, authData, err
}
if err := upsertForm.Submit(); err != nil {
return nil, authData, err
}
if err := form.config.App.OnUserAfterOauth2Register().Trigger(event); err != nil {
return nil, authData, err
if saveErr != nil {
return nil, authData, saveErr
}
return user, authData, nil
+8 -8
View File
@@ -22,15 +22,15 @@ type UserPasswordResetConfirm struct {
//
// NB! App is required struct member.
type UserPasswordResetConfirmConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserPasswordResetConfirm creates a new [UserPasswordResetConfirm]
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserPasswordResetConfirmWithConfig] with explicitly set TxDao.
// [NewUserPasswordResetConfirmWithConfig] with explicitly set Dao.
func NewUserPasswordResetConfirm(app core.App) *UserPasswordResetConfirm {
return NewUserPasswordResetConfirmWithConfig(UserPasswordResetConfirmConfig{
App: app,
@@ -46,8 +46,8 @@ func NewUserPasswordResetConfirmWithConfig(config UserPasswordResetConfirmConfig
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -70,7 +70,7 @@ func (form *UserPasswordResetConfirm) checkToken(value any) error {
return nil // nothing to check
}
user, err := form.config.TxDao.FindUserByToken(
user, err := form.config.Dao.FindUserByToken(
v,
form.config.App.Settings().UserPasswordResetToken.Secret,
)
@@ -88,7 +88,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
return nil, err
}
user, err := form.config.TxDao.FindUserByToken(
user, err := form.config.Dao.FindUserByToken(
form.Token,
form.config.App.Settings().UserPasswordResetToken.Secret,
)
@@ -100,7 +100,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
return nil, err
}
if err := form.config.TxDao.SaveUser(user); err != nil {
if err := form.config.Dao.SaveUser(user); err != nil {
return nil, err
}
+7 -7
View File
@@ -25,7 +25,7 @@ type UserPasswordResetRequest struct {
// NB! App is required struct member.
type UserPasswordResetRequestConfig struct {
App core.App
TxDao *daos.Dao
Dao *daos.Dao
ResendThreshold float64 // in seconds
}
@@ -33,7 +33,7 @@ type UserPasswordResetRequestConfig struct {
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserPasswordResetRequestWithConfig] with explicitly set TxDao.
// [NewUserPasswordResetRequestWithConfig] with explicitly set Dao.
func NewUserPasswordResetRequest(app core.App) *UserPasswordResetRequest {
return NewUserPasswordResetRequestWithConfig(UserPasswordResetRequestConfig{
App: app,
@@ -50,8 +50,8 @@ func NewUserPasswordResetRequestWithConfig(config UserPasswordResetRequestConfig
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -66,7 +66,7 @@ func (form *UserPasswordResetRequest) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
),
)
}
@@ -78,7 +78,7 @@ func (form *UserPasswordResetRequest) Submit() error {
return err
}
user, err := form.config.TxDao.FindUserByEmail(form.Email)
user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil {
return err
}
@@ -96,5 +96,5 @@ func (form *UserPasswordResetRequest) Submit() error {
// update last sent timestamp
user.LastResetSentAt = types.NowDateTime()
return form.config.TxDao.SaveUser(user)
return form.config.Dao.SaveUser(user)
}
+8 -8
View File
@@ -28,8 +28,8 @@ type UserUpsert struct {
//
// NB! App is required struct member.
type UserUpsertConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserUpsert creates a new [UserUpsert] form with initializer
@@ -37,7 +37,7 @@ type UserUpsertConfig struct {
// (for create you could pass a pointer to an empty User - `&models.User{}`).
//
// If you want to submit the form as part of another transaction, use
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
func NewUserUpsert(app core.App, user *models.User) *UserUpsert {
return NewUserUpsertWithConfig(UserUpsertConfig{
App: app,
@@ -57,8 +57,8 @@ func NewUserUpsertWithConfig(config UserUpsertConfig, user *models.User) *UserUp
panic("Invalid initializer config or nil upsert model.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
// load defaults
@@ -83,7 +83,7 @@ func (form *UserUpsert) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
validation.By(form.checkEmailDomain),
validation.By(form.checkUniqueEmail),
),
@@ -103,7 +103,7 @@ func (form *UserUpsert) Validate() error {
func (form *UserUpsert) checkUniqueEmail(value any) error {
v, _ := value.(string)
if v == "" || form.config.TxDao.IsUserEmailUnique(v, form.user.Id) {
if v == "" || form.config.Dao.IsUserEmailUnique(v, form.user.Id) {
return nil
}
@@ -160,6 +160,6 @@ func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error {
form.user.Email = form.Email
return runInterceptors(func() error {
return form.config.TxDao.SaveUser(form.user)
return form.config.Dao.SaveUser(form.user)
}, interceptors...)
}
+8 -8
View File
@@ -19,15 +19,15 @@ type UserVerificationConfirm struct {
//
// NB! App is required struct member.
type UserVerificationConfirmConfig struct {
App core.App
TxDao *daos.Dao
App core.App
Dao *daos.Dao
}
// NewUserVerificationConfirm creates a new [UserVerificationConfirm]
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserVerificationConfirmWithConfig] with explicitly set TxDao.
// [NewUserVerificationConfirmWithConfig] with explicitly set Dao.
func NewUserVerificationConfirm(app core.App) *UserVerificationConfirm {
return NewUserVerificationConfirmWithConfig(UserVerificationConfirmConfig{
App: app,
@@ -43,8 +43,8 @@ func NewUserVerificationConfirmWithConfig(config UserVerificationConfirmConfig)
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -63,7 +63,7 @@ func (form *UserVerificationConfirm) checkToken(value any) error {
return nil // nothing to check
}
user, err := form.config.TxDao.FindUserByToken(
user, err := form.config.Dao.FindUserByToken(
v,
form.config.App.Settings().UserVerificationToken.Secret,
)
@@ -81,7 +81,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
return nil, err
}
user, err := form.config.TxDao.FindUserByToken(
user, err := form.config.Dao.FindUserByToken(
form.Token,
form.config.App.Settings().UserVerificationToken.Secret,
)
@@ -95,7 +95,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
user.Verified = true
if err := form.config.TxDao.SaveUser(user); err != nil {
if err := form.config.Dao.SaveUser(user); err != nil {
return nil, err
}
+7 -7
View File
@@ -25,7 +25,7 @@ type UserVerificationRequest struct {
// NB! App is required struct member.
type UserVerificationRequestConfig struct {
App core.App
TxDao *daos.Dao
Dao *daos.Dao
ResendThreshold float64 // in seconds
}
@@ -33,7 +33,7 @@ type UserVerificationRequestConfig struct {
// form with initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserVerificationRequestWithConfig] with explicitly set TxDao.
// [NewUserVerificationRequestWithConfig] with explicitly set Dao.
func NewUserVerificationRequest(app core.App) *UserVerificationRequest {
return NewUserVerificationRequestWithConfig(UserVerificationRequestConfig{
App: app,
@@ -50,8 +50,8 @@ func NewUserVerificationRequestWithConfig(config UserVerificationRequestConfig)
panic("Missing required config.App instance.")
}
if form.config.TxDao == nil {
form.config.TxDao = form.config.App.Dao()
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
}
return form
@@ -66,7 +66,7 @@ func (form *UserVerificationRequest) Validate() error {
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
is.EmailFormat,
),
)
}
@@ -78,7 +78,7 @@ func (form *UserVerificationRequest) Submit() error {
return err
}
user, err := form.config.TxDao.FindUserByEmail(form.Email)
user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil {
return err
}
@@ -100,5 +100,5 @@ func (form *UserVerificationRequest) Submit() error {
// update last sent timestamp
user.LastVerificationSentAt = types.NowDateTime()
return form.config.TxDao.SaveUser(user)
return form.config.Dao.SaveUser(user)
}
+3 -3
View File
@@ -3,9 +3,9 @@ package validators
import (
"encoding/binary"
"fmt"
"net/http"
"strings"
"github.com/gabriel-vasile/mimetype"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/rest"
)
@@ -47,10 +47,10 @@ func UploadedFileMimeType(validTypes []string) validation.RuleFunc {
return validation.NewError("validation_invalid_mime_type", "Unsupported file type.")
}
filetype := http.DetectContentType(v.Bytes())
filetype := mimetype.Detect(v.Bytes())
for _, t := range validTypes {
if t == filetype {
if filetype.Is(t) {
return nil // valid
}
}
+1 -1
View File
@@ -189,7 +189,7 @@ func (validator *RecordDataValidator) checkEmailValue(field *schema.SchemaField,
return nil // nothing to check
}
if is.Email.Validate(val) != nil {
if is.EmailFormat.Validate(val) != nil {
return validation.NewError("validation_invalid_email", "Must be a valid email")
}
+41 -39
View File
@@ -3,11 +3,12 @@ module github.com/pocketbase/pocketbase
go 1.18
require (
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/aws/aws-sdk-go v1.44.81
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/aws/aws-sdk-go v1.44.102
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.3.4
github.com/fatih/color v1.13.0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/ganigeorgiev/fexpr v0.1.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.4.2
@@ -17,31 +18,33 @@ require (
github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.5.0
gocloud.dev v0.26.0
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
modernc.org/sqlite v1.18.1
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0
golang.org/x/net v0.0.0-20220921155015-db77216a4ee9
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
modernc.org/sqlite v1.19.1
)
require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
github.com/aws/smithy-go v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect
github.com/aws/smithy-go v1.13.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
@@ -58,27 +61,26 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/api v0.93.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.96.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.36.1 // indirect
modernc.org/ccgo/v3 v3.16.8 // indirect
modernc.org/libc v1.16.19 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/cc/v3 v3.39.0 // indirect
modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.19.0 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.2 // indirect
modernc.org/token v1.0.0 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)
+105 -34
View File
@@ -78,6 +78,8 @@ contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0Wk
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
github.com/Azure/azure-amqp-common-go/v3 v3.2.2/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
@@ -122,53 +124,85 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:W
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.81 h1:C8oBZ+a+ka0qk3Q24MohQIFq0tkbO8IAu5tfpAMKVWE=
github.com/aws/aws-sdk-go v1.44.81/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.85 h1:JM2rkKY/GtTDCQXW0StkImbLn6n4Q/Dm2bj+u1rm7Kw=
github.com/aws/aws-sdk-go v1.44.85/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.102 h1:6tUCTGL2UDbFZae1TLGk8vTgeXuzkb8KbAe2FiAeKHc=
github.com/aws/aws-sdk-go v1.44.102/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ=
github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=
github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk=
github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 h1:zfT11pa7ifu/VlLDpmc5OY2W4nYmnKkFDGeMVnmqAI0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4/go.mod h1:ES0I1GBs+YYgcDS1ek47Erbn4TOL811JKqBXtgzqyZ8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 h1:tcFliCWne+zOuUfKNRn8JdFBuWPDuISDH08wD2ULkhk=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU=
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA=
github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0=
github.com/aws/aws-sdk-go-v2/config v1.17.7 h1:odVM52tFHhpqZBKNjVW5h+Zt1tKHbhdTQRb+0WHrNtw=
github.com/aws/aws-sdk-go-v2/config v1.17.7/go.mod h1:dN2gja/QXxFF15hQreyrqYhLBaQo1d9ZKe/v/uplQoI=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q=
github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY=
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 h1:9+ZhlDY7N9dPnUmf7CDfW9In4sW5Ff3bh7oy4DzS1IE=
github.com/aws/aws-sdk-go-v2/credentials v1.12.20/go.mod h1:UKY5HyIux08bbNA7Blv4PcXQ8cTkGh7ghHMFklaviR4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 h1:r08j4sbZu/RVi+BNxkBJwPMUYY3P8mgSDuKkZ/ZN1lE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17/go.mod h1:yIkQcCDYNsZfXpd5UX2Cy+sWA1jPgIhGTw9cOBzfVnQ=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 h1:xFXIMBci0UXStoOHq/8w0XIZPB2hgb9CD7uATJhqt10=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27/go.mod h1:+tj2cHQkChanggNZn1J2fJ1Cv6RO1TV0AA3472do31I=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 h1:fAoVmNGhir6BR+RU0/EI+6+D7abM+MCwWf8v4ip5jNI=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33/go.mod h1:84XgODVR8uRhmOnUkKGUZKqIMxmjmLOR8Uyp7G/TPwc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 h1:5mvQDtNWtI6H56+E4LUnLWEmATMB7oEh+Z9RurtIuC0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 h1:g5qq9sgtEzt2szMaDqQO6fqKe026T6dHTFJp5NsPzkQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 h1:wj5Rwc05hvUSvKuOF29IYb9QrCLjU+rHAy/x/o0DK2c=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24/go.mod h1:jULHjqqjDlbyTa7pfM7WICATnOv+iOhjletM3N0Xbu8=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 h1:agLpf3vtYX1rtKTrOGpevdP3iC2W0hKDmzmhhxJzL+A=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9/go.mod h1:cv+n1mdyh+0B8tAtlEBzTYFA2Uv15SISEn6kabYhIgE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 h1:ZSIPAkAsCCjYrhqfw2+lNzWDzxzHXEckFkTePL5RSWQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 h1:g1ITJ9i9ixa+/WVggLNK20KyliAA8ltnuxfZEDfo2hM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5/go.mod h1:oehQLbMQkppKLXvpx/1Eo0X47Fe+0971DXC9UjGnKcI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9 h1:Lh1AShsuIJTwMkoxVCAYPJgNG5H+eN6SmoUn8nOZ5wE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 h1:3GamN8jcdz/a3nvL/ZVtoH/6xxeshfsiXj5O+6GW4Rg=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13/go.mod h1:89CSPn69UECDLVn0H6FwKNgbtirksl8C8i3aBeeeihw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 h1:BBYoNQt2kUZUUK4bIPsKrCcjVPUMNsgQpNAwhznK/zo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 h1:QFjSOmHSb77qRTv7KI9UFon9X5wLWY5/M+6la3dTcZc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12/go.mod h1:MADjAN0GHFDuc5lRa5Y5ki+oIO/w7X4qczHy+OUx0IA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 h1:HfVVR1vItaG6le+Bpw6P4midjBDMKnjMyZnw9MXYUcE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI=
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 h1:h9qqTedYnA9JcWjKyLV6UYIMSdp91ExLCUbjbpDLH7A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5/go.mod h1:J8SS5Tp/zeLxaubB0xGfKnVrvssNBNLwTipreTKLhjQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 h1:3/gm/JTX9bX8CpzTgIlrtYpB3EVBDxyg/GY/QdcIEZw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
@@ -176,12 +210,20 @@ github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 h1:pwvCchFUEnlceKIgPUouBJwK81aCkQ8UDMORfeFtW10=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23/go.mod h1:/w0eg9IhFGjGyyncHIQrXtU8wvNsTJOP0R6PPj0wf80=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.5 h1:GUnZ62TevLqIoDyHeiWj2P7EqaosgakBKVvWriIdLQY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.5/go.mod h1:csZuQY65DAdFBt1oIjO5hhBR49kQqop4+lcuCjf2arA=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 h1:9pPi0PsFNAGILFfPCk8Y0iyEBGc6lu6OQ97U7hmdesg=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19/go.mod h1:h4J3oPZQbxLhzGnk+j9dfYHi5qIOVJ5kczZd658/ydM=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=
github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA=
github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -238,6 +280,8 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/ganigeorgiev/fexpr v0.1.1 h1:La9kYEgTcIutvOnqNZ8pOUD0O0Q/Gn15sTVEX+IeBE8=
github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -577,8 +621,10 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 h1:vJ2V3lFLg+bBhgroYuRfyN583UzVveQmIXjc8T/y3to=
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -594,6 +640,8 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -668,8 +716,11 @@ golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes=
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220921155015-db77216a4ee9 h1:SdDGdqRuKrF2R4XGcnPzcvZ63c/55GvhoHUus0o+BNI=
golang.org/x/net v0.0.0-20220921155015-db77216a4ee9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -692,9 +743,10 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 h1:dtndE8FcEta75/4kHF3AbpuWzV6f1LjnLrM4pe2SZrw=
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -783,15 +835,20 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -809,6 +866,8 @@ golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 h1:yuLAip3bfURHClMG9VBdzPrQvCWjWiWUTBGV+/fCbUs=
golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -879,6 +938,8 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -925,8 +986,10 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.93.0 h1:T2xt9gi0gHdxdnRkVQhT8mIvPaXKNsDNWz+L696M66M=
google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.94.0 h1:KtKM9ru3nzQioV1HLlUf1cR7vMYJIpgls5VhAYQXIwA=
google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM=
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1029,8 +1092,10 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa h1:Ux9yJCyf598uEniFPSyp8g1jtGTt77m+lzYyVgrWQaQ=
google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI=
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 h1:mmbq5q8M1t7dhkLw320YK4PsOXm6jdnUAkErImaIqOg=
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1062,8 +1127,8 @@ google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1104,40 +1169,46 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.1 h1:CICrjwr/1M4+6OQ4HJZ/AHxjcwe67r5vPUF518MkO8A=
modernc.org/cc/v3 v3.36.1/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.8 h1:G0QNlTqI5uVgczBWfGKs7B++EPwCfXPWGD2MdeKloDs=
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg=
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.39.0 h1:sd+UyMj63acEV1jaFqxFGPQfllSncJgL+roJjFlo6lI=
modernc.org/cc/v3 v3.39.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.19 h1:S8flPn5ZeXx6iw/8yNa986hwTQDrY8RXU7tObZuAozo=
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.17.0 h1:nbL2Lv0I323wLc1GmTh/AqVtI9JeBVc7Nhapdg9EONs=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.19.0 h1:bXyVhGQg6KIClTr8FMVIDPl7jtbcs7aS5WP7vLDaxPs=
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.2.0 h1:zXehBrt9n+Pjn+4RoRCZ0KqRA/0ePFqcecxZ/hXCIVw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8=
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
modernc.org/sqlite v1.19.1 h1:8xmS5oLnZtAK//vnd4aTVj8VOeTAccEFOtUnIzfSw+4=
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.2 h1:iFBDH6j1Z0bN/Q9udJnnFoFpENA4252qe/7/5woE5MI=
modernc.org/strutil v1.1.2/go.mod h1:OYajnUAcI/MX+XD/Wx7v1bbdvcQSvxgtb0gC+u3d3eg=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@@ -0,0 +1,76 @@
package migrations
import "github.com/pocketbase/dbx"
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, createErr := db.NewQuery(`
CREATE TABLE {{_externalAuths}} (
[[id]] TEXT PRIMARY KEY,
[[userId]] TEXT NOT NULL,
[[provider]] TEXT NOT NULL,
[[providerId]] TEXT NOT NULL,
[[created]] TEXT DEFAULT "" NOT NULL,
[[updated]] TEXT DEFAULT "" NOT NULL,
---
FOREIGN KEY ([[userId]]) REFERENCES {{_users}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE UNIQUE INDEX _externalAuths_userId_provider_idx on {{_externalAuths}} ([[userId]], [[provider]]);
CREATE UNIQUE INDEX _externalAuths_provider_providerId_idx on {{_externalAuths}} ([[provider]], [[providerId]]);
`).Execute()
if createErr != nil {
return createErr
}
// remove the unique email index from the _users table and
// replace it with partial index
_, alterErr := db.NewQuery(`
-- crate new users table
CREATE TABLE {{_newUsers}} (
[[id]] TEXT PRIMARY KEY,
[[verified]] BOOLEAN DEFAULT FALSE NOT NULL,
[[email]] TEXT DEFAULT "" NOT NULL,
[[tokenKey]] TEXT NOT NULL,
[[passwordHash]] TEXT NOT NULL,
[[lastResetSentAt]] TEXT DEFAULT "" NOT NULL,
[[lastVerificationSentAt]] TEXT DEFAULT "" NOT NULL,
[[created]] TEXT DEFAULT "" NOT NULL,
[[updated]] TEXT DEFAULT "" NOT NULL
);
-- copy all data from the old users table to the new one
INSERT INTO {{_newUsers}} SELECT * FROM {{_users}};
-- drop old table
DROP TABLE {{_users}};
-- rename new table
ALTER TABLE {{_newUsers}} RENAME TO {{_users}};
-- create named indexes
CREATE UNIQUE INDEX _users_email_idx ON {{_users}} ([[email]]) WHERE [[email]] != "";
CREATE UNIQUE INDEX _users_tokenKey_idx ON {{_users}} ([[tokenKey]]);
`).Execute()
if alterErr != nil {
return alterErr
}
return nil
}, func(db dbx.Builder) error {
if _, err := db.DropTable("_externalAuths").Execute(); err != nil {
return err
}
// drop the partial email unique index and replace it with normal unique index
_, indexErr := db.NewQuery(`
DROP INDEX IF EXISTS _users_email_idx;
CREATE UNIQUE INDEX _users_email_idx on {{_users}} ([[email]]);
`).Execute()
if indexErr != nil {
return indexErr
}
return nil
})
}
+15
View File
@@ -0,0 +1,15 @@
package models
var _ Model = (*ExternalAuth)(nil)
type ExternalAuth struct {
BaseModel
UserId string `db:"userId" json:"userId"`
Provider string `db:"provider" json:"provider"`
ProviderId string `db:"providerId" json:"providerId"`
}
func (m *ExternalAuth) TableName() string {
return "_externalAuths"
}
+14
View File
@@ -0,0 +1,14 @@
package models_test
import (
"testing"
"github.com/pocketbase/pocketbase/models"
)
func TestExternalAuthTableName(t *testing.T) {
m := models.ExternalAuth{}
if m.TableName() != "_externalAuths" {
t.Fatalf("Unexpected table name, got %q", m.TableName())
}
}
+76 -71
View File
@@ -26,50 +26,60 @@ type appWrapper struct {
// PocketBase defines a PocketBase app launcher.
//
// It implements [core.App] via embedding and all of interface methods
// It implements [core.App] via embedding and all of the app interface methods
// could be accessed directly through the instance (eg. PocketBase.DataDir()).
type PocketBase struct {
*appWrapper
// RootCmd is the main cli command
debugFlag bool
dataDirFlag string
encryptionEnvFlag string
hideStartBanner bool
// RootCmd is the main console command
RootCmd *cobra.Command
// console flags
debugFlag bool
dataDirFlag string
encryptionEnv string
// console flag fallback values
defaultDebug bool
defaultDataDir string
defaultEncryptionEnv string
// serve start banner
showStartBanner bool
}
// New creates a new PocketBase instance.
// Config is the PocketBase initialization config struct.
type Config struct {
// optional default values for the console flags
DefaultDebug bool
DefaultDataDir string // if not set, it will fallback to "./pb_data"
DefaultEncryptionEnv string
// hide the default console server info on app startup
HideStartBanner bool
}
// New creates a new PocketBase instance with the default configuration.
// Use [NewWithConfig()] if you want to provide a custom configuration.
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when Start() is executed.
// If you want to initialize the application before calling Start(),
// then you'll have to manually call Bootstrap().
// Everything will be initialized when [Start()] is executed.
// 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
var withGoRun bool
var baseDir string
if strings.HasPrefix(os.Args[0], os.TempDir()) {
// probably ran with go run...
withGoRun = true
baseDir, _ = os.Getwd()
} else {
// probably ran with go build...
withGoRun = false
baseDir = filepath.Dir(os.Args[0])
}
_, isUsingGoRun := inspectRuntime()
defaultDir := filepath.Join(baseDir, "pb_data")
return NewWithConfig(Config{
DefaultDebug: isUsingGoRun,
})
}
// NewWithConfig creates a new PocketBase instance with the provided config.
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when [Start()] is executed.
// If you want to initialize the application before calling [Start()],
// then you'll have to manually call [Bootstrap()].
func NewWithConfig(config Config) *PocketBase {
// initialize a default data directory based on the executable baseDir
if config.DefaultDataDir == "" {
baseDir, _ := inspectRuntime()
config.DefaultDataDir = filepath.Join(baseDir, "pb_data")
}
pb := &PocketBase{
RootCmd: &cobra.Command{
@@ -84,46 +94,31 @@ func New() *PocketBase {
DisableDefaultCmd: true,
},
},
defaultDebug: withGoRun,
defaultDataDir: defaultDir,
defaultEncryptionEnv: "",
showStartBanner: true,
debugFlag: config.DefaultDebug,
dataDirFlag: config.DefaultDataDir,
encryptionEnvFlag: config.DefaultEncryptionEnv,
hideStartBanner: config.HideStartBanner,
}
// parse base flags
// (errors are ignored, since the full flags parsing happens on Execute())
pb.eagerParseFlags()
pb.eagerParseFlags(config)
// initialize the app instance
pb.appWrapper = &appWrapper{core.NewBaseApp(
pb.dataDirFlag,
pb.encryptionEnv,
pb.encryptionEnvFlag,
pb.debugFlag,
)}
return pb
}
// hide the default help command (allow only `--help` flag)
pb.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
// DefaultDebug sets the default --debug flag value.
func (pb *PocketBase) DefaultDebug(val bool) *PocketBase {
pb.defaultDebug = val
return pb
}
// hook the bootstrap process
pb.RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
return pb.Bootstrap()
}
// DefaultDataDir sets the default --dir flag value.
func (pb *PocketBase) DefaultDataDir(val string) *PocketBase {
pb.defaultDataDir = val
return pb
}
// DefaultEncryptionEnv sets the default --encryptionEnv flag value.
func (pb *PocketBase) DefaultEncryptionEnv(val string) *PocketBase {
pb.defaultEncryptionEnv = val
return pb
}
// ShowStartBanner shows/hides the web server start banner.
func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
pb.showStartBanner = val
return pb
}
@@ -131,7 +126,7 @@ func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
// commands (serve, migrate, version) and executes pb.RootCmd.
func (pb *PocketBase) Start() error {
// register system commands
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, pb.showStartBanner))
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner))
pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb))
return pb.Execute()
@@ -143,10 +138,6 @@ func (pb *PocketBase) Start() error {
// This method differs from pb.Start() by not registering the default
// system commands!
func (pb *PocketBase) Execute() error {
if err := pb.Bootstrap(); err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(1)
@@ -180,27 +171,41 @@ func (pb *PocketBase) onTerminate() error {
// eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute().
// so we can have all PocketBase flags ready for use on initialization.
func (pb *PocketBase) eagerParseFlags() error {
func (pb *PocketBase) eagerParseFlags(config Config) error {
pb.RootCmd.PersistentFlags().StringVar(
&pb.dataDirFlag,
"dir",
pb.defaultDataDir,
config.DefaultDataDir,
"the PocketBase data directory",
)
pb.RootCmd.PersistentFlags().StringVar(
&pb.encryptionEnv,
&pb.encryptionEnvFlag,
"encryptionEnv",
pb.defaultEncryptionEnv,
"the env variable whose value of 32 chars will be used \nas encryption key for the app settings (default none)",
config.DefaultEncryptionEnv,
"the env variable whose value of 32 characters will be used \nas encryption key for the app settings (default none)",
)
pb.RootCmd.PersistentFlags().BoolVar(
&pb.debugFlag,
"debug",
pb.defaultDebug,
config.DefaultDebug,
"enable debug mode, aka. showing more detailed logs",
)
return pb.RootCmd.ParseFlags(os.Args[1:])
}
// tries to find the base executable directory and how it was run
func inspectRuntime() (baseDir string, withGoRun bool) {
if strings.HasPrefix(os.Args[0], os.TempDir()) {
// probably ran with go run
withGoRun = true
baseDir, _ = os.Getwd()
} else {
// probably ran with go build
withGoRun = false
baseDir = filepath.Dir(os.Args[0])
}
return
}
+82 -36
View File
@@ -6,14 +6,19 @@ import (
)
func TestNew(t *testing.T) {
testDir := "./pb_test_data_dir"
defer os.RemoveAll(testDir)
// copy os.Args
originalArgs := []string{}
copy(originalArgs, os.Args)
defer func() {
// restore os.Args
copy(os.Args, originalArgs)
}()
// reset os.Args
// change os.Args
os.Args = os.Args[0:1]
os.Args = append(
os.Args,
"--dir="+testDir,
"--dir=test_dir",
"--encryptionEnv=test_encryption_env",
"--debug=true",
)
@@ -32,8 +37,8 @@ func TestNew(t *testing.T) {
t.Fatal("Expected appWrapper to be initialized, got nil")
}
if app.DataDir() != testDir {
t.Fatalf("Expected app.DataDir() %q, got %q", testDir, app.DataDir())
if app.DataDir() != "test_dir" {
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir", app.DataDir())
}
if app.EncryptionEnv() != "test_encryption_env" {
@@ -45,52 +50,93 @@ func TestNew(t *testing.T) {
}
}
func TestDefaultDebug(t *testing.T) {
app := New()
func TestNewWithConfig(t *testing.T) {
app := NewWithConfig(Config{
DefaultDebug: true,
DefaultDataDir: "test_dir",
DefaultEncryptionEnv: "test_encryption_env",
HideStartBanner: true,
})
app.DefaultDebug(true)
if app.defaultDebug != true {
t.Fatalf("Expected defaultDebug %v, got %v", true, app.defaultDebug)
if app == nil {
t.Fatal("Expected initialized PocketBase instance, got nil")
}
app.DefaultDebug(false)
if app.defaultDebug != false {
t.Fatalf("Expected defaultDebug %v, got %v", false, app.defaultDebug)
if app.RootCmd == nil {
t.Fatal("Expected RootCmd to be initialized, got nil")
}
if app.appWrapper == nil {
t.Fatal("Expected appWrapper to be initialized, got nil")
}
if app.hideStartBanner != true {
t.Fatal("Expected app.hideStartBanner to be true, got false")
}
if app.DataDir() != "test_dir" {
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir", app.DataDir())
}
if app.EncryptionEnv() != "test_encryption_env" {
t.Fatalf("Expected app.DataDir() %q, got %q", "test_encryption_env", app.EncryptionEnv())
}
if app.IsDebug() != true {
t.Fatal("Expected app.IsDebug() true, got false")
}
}
func TestDefaultDataDir(t *testing.T) {
app := New()
func TestNewWithConfigAndFlags(t *testing.T) {
// copy os.Args
originalArgs := []string{}
copy(originalArgs, os.Args)
defer func() {
// restore os.Args
copy(os.Args, originalArgs)
}()
expected := "test_default"
// change os.Args
os.Args = os.Args[0:1]
os.Args = append(
os.Args,
"--dir=test_dir_flag",
"--encryptionEnv=test_encryption_env_flag",
"--debug=false",
)
app.DefaultDataDir(expected)
if app.defaultDataDir != expected {
t.Fatalf("Expected defaultDataDir %v, got %v", expected, app.defaultDataDir)
app := NewWithConfig(Config{
DefaultDebug: true,
DefaultDataDir: "test_dir",
DefaultEncryptionEnv: "test_encryption_env",
HideStartBanner: true,
})
if app == nil {
t.Fatal("Expected initialized PocketBase instance, got nil")
}
}
func TestDefaultEncryptionEnv(t *testing.T) {
app := New()
if app.RootCmd == nil {
t.Fatal("Expected RootCmd to be initialized, got nil")
}
expected := "test_env"
if app.appWrapper == nil {
t.Fatal("Expected appWrapper to be initialized, got nil")
}
app.DefaultEncryptionEnv(expected)
if app.defaultEncryptionEnv != expected {
t.Fatalf("Expected defaultEncryptionEnv %v, got %v", expected, app.defaultEncryptionEnv)
if app.hideStartBanner != true {
t.Fatal("Expected app.hideStartBanner to be true, got false")
}
}
func TestShowStartBanner(t *testing.T) {
app := New()
if app.DataDir() != "test_dir_flag" {
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir_flag", app.DataDir())
}
app.ShowStartBanner(true)
if app.showStartBanner != true {
t.Fatalf("Expected showStartBanner %v, got %v", true, app.showStartBanner)
if app.EncryptionEnv() != "test_encryption_env_flag" {
t.Fatalf("Expected app.DataDir() %q, got %q", "test_encryption_env_flag", app.EncryptionEnv())
}
app.ShowStartBanner(false)
if app.showStartBanner != false {
t.Fatalf("Expected showStartBanner %v, got %v", false, app.showStartBanner)
if app.IsDebug() != false {
t.Fatal("Expected app.IsDebug() false, got true")
}
}
+35 -28
View File
@@ -96,7 +96,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
props := strings.Split(fieldName, ".")
currentCollectionName := r.baseCollection.Name
currentTableAlias := currentCollectionName
currentTableAlias := inflector.Columnify(currentCollectionName)
// check for @collection field (aka. non-relational join)
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
@@ -106,14 +106,14 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
}
currentCollectionName = props[1]
currentTableAlias = "__collection_" + currentCollectionName
currentTableAlias = inflector.Columnify("__collection_" + currentCollectionName)
collection, err := r.loadCollection(currentCollectionName)
if err != nil {
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
}
r.addJoin(collection.Name, currentTableAlias, nil)
r.addJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)
props = props[2:] // leave only the collection fields
} else if props[0] == "@request" {
@@ -123,13 +123,13 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
}
// not a profile relational field
if len(props) <= 4 || !strings.HasPrefix(fieldName, "@request.user.profile.") {
if !strings.HasPrefix(fieldName, "@request.user.profile.") {
return r.resolveStaticRequestField(props[1:]...)
}
// resolve the profile collection fields
currentCollectionName = models.ProfileCollectionName
currentTableAlias = "__user_" + currentCollectionName
currentTableAlias = inflector.Columnify("__user_" + currentCollectionName)
collection, err := r.loadCollection(currentCollectionName)
if err != nil {
@@ -146,12 +146,16 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
}
// join the profile collection
r.addJoin(collection.Name, currentTableAlias, dbx.NewExp(fmt.Sprintf(
// aka. profiles.id = profileId
"[[%s.id]] = %s",
inflector.Columnify(currentTableAlias),
profileIdPlaceholder,
), profileIdPlaceholderParam))
r.addJoin(
inflector.Columnify(collection.Name),
currentTableAlias,
dbx.NewExp(fmt.Sprintf(
// aka. profiles.id = profileId
"[[%s.id]] = %s",
currentTableAlias,
profileIdPlaceholder,
), profileIdPlaceholderParam),
)
props = props[3:] // leave only the profile fields
}
@@ -168,7 +172,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
// base model prop (always available but not part of the collection schema)
if list.ExistInSlice(prop, baseModelFields) {
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
}
field := collection.Schema.GetFieldByName(prop)
@@ -178,7 +182,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
// last prop
if i == totalProps-1 {
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
}
// check if it is a relation field
@@ -198,20 +202,27 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
if relErr != nil {
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
}
cleanFieldName := inflector.Columnify(field.Name)
newCollectionName := relCollection.Name
newTableAlias := currentTableAlias + "_" + field.Name
newTableAlias := currentTableAlias + "_" + cleanFieldName
jeTable := currentTableAlias + "_" + cleanFieldName + "_je"
jePair := currentTableAlias + "." + cleanFieldName
r.addJoin(
newCollectionName,
fmt.Sprintf(
// note: the case is used to normalize value access for single and multiple relations.
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
jePair, jePair, jePair,
),
jeTable,
nil,
)
r.addJoin(
inflector.Columnify(newCollectionName),
newTableAlias,
dbx.NewExp(fmt.Sprintf(
// 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array)
"[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')",
inflector.Columnify(currentTableAlias),
inflector.Columnify(field.Name),
inflector.Columnify(newTableAlias),
inflector.Columnify("id"),
)),
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeTable)),
)
currentCollectionName = newCollectionName
@@ -296,11 +307,7 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
}
func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) {
tableExpr := fmt.Sprintf(
"%s %s",
inflector.Columnify(tableName),
inflector.Columnify(tableAlias),
)
tableExpr := fmt.Sprintf("%s %s", tableName, tableAlias)
join := join{
id: tableAlias,
+14 -12
View File
@@ -52,32 +52,32 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
{
"single rel",
[]string{"onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"non-relation field + single rel",
[]string{"title", "onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"nested incomplete rels",
[]string{"manyrels.onerel"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]]",
},
{
"nested complete rels",
[]string{"manyrels.onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]]",
},
{
"repeated nested rels",
[]string{"manyrels.onerel.manyrels.onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel.manyrels]]) THEN [[demo4_manyrels_onerel.manyrels]] ELSE json_array([[demo4_manyrels_onerel.manyrels]]) END) `demo4_manyrels_onerel_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel_manyrels.id]] = [[demo4_manyrels_onerel_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel_manyrels.onerel]]) THEN [[demo4_manyrels_onerel_manyrels.onerel]] ELSE json_array([[demo4_manyrels_onerel_manyrels.onerel]]) END) `demo4_manyrels_onerel_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels_onerel.id]] = [[demo4_manyrels_onerel_manyrels_onerel_je.value]]",
},
{
"multiple rels",
[]string{"manyrels.title", "onerel.onefile"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"@collection join",
@@ -87,7 +87,9 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
{
"static @request.user.profile fields",
[]string{"@request.user.id", "@request.user.profile.id", "@request.data.demo"},
"SELECT `demo4`.* FROM `demo4`",
"^" +
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
" {:.*}$",
},
{
"relational @request.user.profile fields",
@@ -95,12 +97,12 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
"^" +
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
" {:.*} " +
regexp.QuoteMeta("LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles.rel]] LIKE ('%' || [[__user_profiles_rel.id]] || '%')") +
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__user_profiles.rel]]) THEN [[__user_profiles.rel]] ELSE json_array([[__user_profiles.rel]]) END) `__user_profiles_rel_je` LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles_rel.id]] = [[__user_profiles_rel_je.value]]") +
"$",
},
}
for i, s := range scenarios {
for _, s := range scenarios {
query := app.Dao().RecordQuery(collection)
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
@@ -109,14 +111,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
}
if err := r.UpdateQuery(query); err != nil {
t.Errorf("(%d) UpdateQuery failed with error %v", i, err)
t.Errorf("(%s) UpdateQuery failed with error %v", s.name, err)
continue
}
rawQuery := query.Build().SQL()
if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
t.Errorf("(%d) Expected query\n %v \ngot:\n %v", i, s.expectQuery, rawQuery)
t.Errorf("(%s) Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
}
}
}
@@ -161,6 +163,7 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
{"manyrels.title", false, "[[demo4_manyrels.title]]"},
{"manyrels.onerel.manyrels.onefile", false, "[[demo4_manyrels_onerel_manyrels.onefile]]"},
// @request.user.profile relation join:
{"@request.user.profile.rel", false, "[[__user_profiles.rel]]"},
{"@request.user.profile.rel.name", false, "[[__user_profiles_rel.name]]"},
// @collection fieds:
{"@collect", true, ""},
@@ -247,7 +250,6 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
{"@request.user", true, ""},
{"@request.user.id", false, `"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`},
{"@request.user.profile", false, `"{\"id\":\"d13f60a4-5765-48c7-9e1d-3e782340f833\",\"name\":\"test\"}"`},
{"@request.user.profile.name", false, `"test"`},
}
for i, s := range scenarios {
+17 -7
View File
@@ -36,13 +36,23 @@ type ApiScenario struct {
// test hooks
// ---
BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo)
AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo)
TestAppFactory func() (*TestApp, error)
BeforeTestFunc func(t *testing.T, app *TestApp, e *echo.Echo)
AfterTestFunc func(t *testing.T, app *TestApp, e *echo.Echo)
}
// Test executes the test case/scenario.
func (scenario *ApiScenario) Test(t *testing.T) {
testApp, _ := NewTestApp()
var testApp *TestApp
var testAppErr error
if scenario.TestAppFactory != nil {
testApp, testAppErr = scenario.TestAppFactory()
} else {
testApp, testAppErr = NewTestApp()
}
if testAppErr != nil {
t.Fatalf("Failed to initialize the test app instance: %v", testAppErr)
}
defer testApp.Cleanup()
e, err := apis.InitApi(testApp)
@@ -50,8 +60,8 @@ func (scenario *ApiScenario) Test(t *testing.T) {
t.Fatal(err)
}
if scenario.BeforeFunc != nil {
scenario.BeforeFunc(t, testApp, e)
if scenario.BeforeTestFunc != nil {
scenario.BeforeTestFunc(t, testApp, e)
}
recorder := httptest.NewRecorder()
@@ -135,7 +145,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
}
}
if scenario.AfterFunc != nil {
scenario.AfterFunc(t, testApp, e)
if scenario.AfterTestFunc != nil {
scenario.AfterTestFunc(t, testApp, e)
}
}
+33 -21
View File
@@ -45,12 +45,21 @@ func (t *TestApp) ResetEventCalls() {
t.EventCalls = make(map[string]int)
}
// NewTestApp creates and initializes a full application instance for testing.
// NewTestApp creates and initializes a test application instance.
//
// It is the caller's responsibility to call `app.Cleanup()`
// when the app is no longer needed.
func NewTestApp() (*TestApp, error) {
tempDir, err := NewTempDataDir()
func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
var testDataDir string
if len(optTestDataDir) == 0 || optTestDataDir[0] == "" {
// fallback to the default test data directory
_, currentFile, _, _ := runtime.Caller(0)
testDataDir = filepath.Join(path.Dir(currentFile), "data")
} else {
testDataDir = optTestDataDir[0]
}
tempDir, err := TempDirClone(testDataDir)
if err != nil {
return nil, err
}
@@ -188,21 +197,26 @@ func NewTestApp() (*TestApp, error) {
return nil
})
t.OnUserBeforeOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error {
t.EventCalls["OnUserBeforeOauth2Register"]++
return nil
})
t.OnUserAfterOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error {
t.EventCalls["OnUserAfterOauth2Register"]++
return nil
})
t.OnUserAuthRequest().Add(func(e *core.UserAuthEvent) error {
t.EventCalls["OnUserAuthRequest"]++
return nil
})
t.OnUserListExternalAuths().Add(func(e *core.UserListExternalAuthsEvent) error {
t.EventCalls["OnUserListExternalAuths"]++
return nil
})
t.OnUserBeforeUnlinkExternalAuthRequest().Add(func(e *core.UserUnlinkExternalAuthEvent) error {
t.EventCalls["OnUserBeforeUnlinkExternalAuthRequest"]++
return nil
})
t.OnUserAfterUnlinkExternalAuthRequest().Add(func(e *core.UserUnlinkExternalAuthEvent) error {
t.EventCalls["OnUserAfterUnlinkExternalAuthRequest"]++
return nil
})
t.OnMailerBeforeAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error {
t.EventCalls["OnMailerBeforeAdminResetPasswordSend"]++
return nil
@@ -376,21 +390,19 @@ func NewTestApp() (*TestApp, error) {
return t, nil
}
// NewTempDataDir creates a new temporary directory copy of the test data.
// TempDirClone creates a new temporary directory copy from the
// provided directory path.
//
// It is the caller's responsibility to call `os.RemoveAll(dir)`
// when the directory is no longer needed.
func NewTempDataDir() (string, error) {
// It is the caller's responsibility to call `os.RemoveAll(tempDir)`
// when the directory is no longer needed!
func TempDirClone(dirToClone string) (string, error) {
tempDir, err := os.MkdirTemp("", "pb_test_*")
if err != nil {
return "", err
}
_, currentFile, _, _ := runtime.Caller(0)
testDataDir := filepath.Join(path.Dir(currentFile), "data")
// copy everything from testDataDir to tempDir
if err := copyDir(testDataDir, tempDir); err != nil {
if err := copyDir(dirToClone, tempDir); err != nil {
return "", err
}
Binary file not shown.
+5
View File
@@ -11,6 +11,7 @@ import (
type AuthUser struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
}
@@ -90,6 +91,10 @@ func NewProviderByName(name string) (Provider, error) {
return NewGithubProvider(), nil
case NameGitlab:
return NewGitlabProvider(), nil
case NameDiscord:
return NewDiscordProvider(), nil
case NameTwitter:
return NewTwitterProvider(), nil
default:
return nil, errors.New("Missing provider " + name)
}
+9
View File
@@ -54,4 +54,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Gitlab); !ok {
t.Error("Expected to be instance of *auth.Gitlab")
}
// discord
p, err = auth.NewProviderByName(auth.NameDiscord)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Discord); !ok {
t.Error("Expected to be instance of *auth.Discord")
}
}
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"fmt"
"golang.org/x/oauth2"
)
var _ Provider = (*Discord)(nil)
// NameDiscord is the unique name of the Discord provider.
const NameDiscord string = "discord"
// Discord allows authentication via Discord OAuth2.
type Discord struct {
*baseProvider
}
// NewDiscordProvider creates a new Discord provider instance with some defaults.
func NewDiscordProvider() *Discord {
// https://discord.com/developers/docs/topics/oauth2
// https://discord.com/developers/docs/resources/user#get-current-user
return &Discord{&baseProvider{
scopes: []string{"identify", "email"},
authUrl: "https://discord.com/api/oauth2/authorize",
tokenUrl: "https://discord.com/api/oauth2/token",
userApiUrl: "https://discord.com/api/users/@me",
}}
}
// FetchAuthUser returns an AuthUser instance from Discord's user api.
func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://discord.com/developers/docs/resources/user#user-object
rawData := struct {
Id string `json:"id"`
Username string `json:"username"`
Discriminator string `json:"discriminator"`
Email string `json:"email"`
Avatar string `json:"avatar"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}
// Build a full avatar URL using the avatar hash provided in the API response
// https://discord.com/developers/docs/reference#image-formatting
avatarUrl := fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png", rawData.Id, rawData.Avatar)
// Concatenate the user's username and discriminator into a single username string
username := fmt.Sprintf("%s#%s", rawData.Username, rawData.Discriminator)
user := &AuthUser{
Id: rawData.Id,
Name: username,
Username: rawData.Username,
Email: rawData.Email,
AvatarUrl: avatarUrl,
}
return user, nil
}
+2
View File
@@ -32,6 +32,7 @@ func NewGithubProvider() *Github {
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
rawData := struct {
Login string `json:"login"`
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
@@ -45,6 +46,7 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Username: rawData.Login,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
}
+2
View File
@@ -32,6 +32,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
rawData := struct {
Id int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarUrl string `json:"avatar_url"`
}{}
@@ -43,6 +44,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Username: rawData.Username,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
}
+7 -8
View File
@@ -29,12 +29,11 @@ func NewGoogleProvider() *Google {
// FetchAuthUser returns an AuthUser instance based the Google's user api.
func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://cloud.google.com/identity-platform/docs/reference/rest/v1/UserInfo
rawData := struct {
LocalId string `json:"localId"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
PhotoUrl string `json:"photoUrl"`
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
@@ -42,10 +41,10 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
user := &AuthUser{
Id: rawData.LocalId,
Name: rawData.DisplayName,
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.PhotoUrl,
AvatarUrl: rawData.Picture,
}
return user, nil
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"golang.org/x/oauth2"
)
var _ Provider = (*Twitter)(nil)
// NameTwitter is the unique name of the Twitter provider.
const NameTwitter string = "twitter"
// Twitter allows authentication via Twitter OAuth2.
type Twitter struct {
*baseProvider
}
// NewTwitterProvider creates new Twitter provider instance with some defaults.
func NewTwitterProvider() *Twitter {
return &Twitter{&baseProvider{
scopes: []string{
"users.read",
// we don't actually use this scope, but for some reason it is required by the `/2/users/me` endpoint
// (see https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
"tweet.read",
},
authUrl: "https://twitter.com/i/oauth2/authorize",
tokenUrl: "https://api.twitter.com/2/oauth2/token",
userApiUrl: "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Twitter's user api.
func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me
rawData := struct {
Data struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
ProfileImageUrl string `json:"profile_image_url"`
// NB! At the time of writing, Twitter OAuth2 doesn't support returning the user email address
// (see https://twittercommunity.com/t/which-api-to-get-user-after-oauth2-authorization/162417/33)
Email string `json:"email"`
} `json:"data"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Data.Id,
Name: rawData.Data.Name,
Username: rawData.Data.Username,
Email: rawData.Data.Email,
AvatarUrl: rawData.Data.ProfileImageUrl,
}
return user, nil
}
+29 -11
View File
@@ -18,6 +18,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/disintegration/imaging"
"github.com/gabriel-vasile/mimetype"
"github.com/pocketbase/pocketbase/tools/list"
"gocloud.dev/blob"
"gocloud.dev/blob/fileblob"
@@ -98,7 +99,11 @@ func (s *System) Attributes(fileKey string) (*blob.Attributes, error) {
// Upload writes content into the fileKey location.
func (s *System) Upload(content []byte, fileKey string) error {
w, writerErr := s.bucket.NewWriter(s.ctx, fileKey, nil)
opts := &blob.WriterOptions{
ContentType: mimetype.Detect(content).String(),
}
w, writerErr := s.bucket.NewWriter(s.ctx, fileKey, opts)
if writerErr != nil {
return writerErr
}
@@ -185,9 +190,16 @@ var inlineServeContentTypes = []string{
// video
"video/webm", "video/mp4", "video/3gpp", "video/quicktime", "video/x-ms-wmv",
// audio
"audio/basic", "audio/aiff", "audio/mpeg", "audio/midi", "audio/wave",
"audio/basic", "audio/aiff", "audio/mpeg", "audio/midi", "audio/mp3", "audio/wave",
"audio/wav", "audio/x-wav", "audio/x-mpeg", "audio/x-m4a", "audio/aac",
// document
"application/pdf",
"application/pdf", "application/x-pdf",
}
// manualExtensionContentTypes is a map of file extensions to content types.
var manualExtensionContentTypes = map[string]string{
".svg": "image/svg+xml", // (see https://github.com/whatwg/mimesniff/issues/7)
".css": "text/css", // (see https://github.com/gabriel-vasile/mimetype/pull/113)
}
// Serve serves the file at fileKey location to an HTTP response.
@@ -204,12 +216,11 @@ func (s *System) Serve(response http.ResponseWriter, fileKey string, name string
disposition = "inline"
}
// make an exception for svg and use a custom content type
// to send in the response so that it can be loaded in a img tag
// (see https://github.com/whatwg/mimesniff/issues/7)
// make an exception for specific content types and force a
// custom content type to send in the response so that it can be loaded directly.
extContentType := realContentType
if filepath.Ext(name) == ".svg" {
extContentType = "image/svg+xml"
if ct, found := manualExtensionContentTypes[filepath.Ext(name)]; found && extContentType != ct {
extContentType = ct
}
response.Header().Set("Content-Disposition", disposition+"; filename="+name)
@@ -265,8 +276,8 @@ func (s *System) CreateThumb(originalKey string, thumbKey, thumbSize string) err
}
defer r.Close()
// create imaging object from the origial reader
img, decodeErr := imaging.Decode(r)
// create imaging object from the original reader
img, decodeErr := imaging.Decode(r, imaging.AutoOrientation(true))
if decodeErr != nil {
return decodeErr
}
@@ -299,8 +310,15 @@ func (s *System) CreateThumb(originalKey string, thumbKey, thumbSize string) err
return writerErr
}
// try to detect the thumb format based on the original file name
// (fallbacks to png on error)
format, err := imaging.FormatFromFilename(thumbKey)
if err != nil {
format = imaging.PNG
}
// thumb encode (aka. upload)
if err := imaging.Encode(w, thumbImg, imaging.PNG); err != nil {
if err := imaging.Encode(w, thumbImg, format); err != nil {
w.Close()
return err
}
+19 -1
View File
@@ -207,6 +207,18 @@ func TestFileSystemServe(t *testing.T) {
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
},
},
{
// css exception
"style.css",
"test_name.css",
false,
map[string]string{
"Content-Disposition": "attachment; filename=test_name.css",
"Content-Type": "text/css",
"Content-Length": "0",
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
},
},
}
for _, scenario := range scenarios {
@@ -216,7 +228,7 @@ func TestFileSystemServe(t *testing.T) {
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("(%s) Expected hasError %v, got %v", scenario.path, scenario.expectError, hasErr)
t.Errorf("(%s) Expected hasError %v, got %v (%v)", scenario.path, scenario.expectError, hasErr, err)
continue
}
@@ -321,5 +333,11 @@ func createTestDir(t *testing.T) string {
}
file4.Close()
file5, err := os.OpenFile(filepath.Join(dir, "style.css"), os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
t.Fatal(err)
}
file5.Close()
return dir
}
+12 -1
View File
@@ -17,7 +17,18 @@ type Hook[T any] struct {
handlers []Handler[T]
}
// Add registers a new handler to the hook.
// PreAdd registers a new handler to the hook by prepending it to the existing queue.
func (h *Hook[T]) PreAdd(fn Handler[T]) {
h.mux.Lock()
defer h.mux.Unlock()
// minimize allocations by shifting the slice
h.handlers = append(h.handlers, nil)
copy(h.handlers[1:], h.handlers)
h.handlers[0] = fn
}
// Add registers a new handler to the hook by appending it to the existing queue.
func (h *Hook[T]) Add(fn Handler[T]) {
h.mux.Lock()
defer h.mux.Unlock()
+21 -5
View File
@@ -5,18 +5,34 @@ import (
"testing"
)
func TestAdd(t *testing.T) {
func TestAddAndPreAdd(t *testing.T) {
h := Hook[int]{}
if total := len(h.handlers); total != 0 {
t.Fatalf("Expected no handlers, found %d", total)
}
h.Add(func(data int) error { return nil })
h.Add(func(data int) error { return nil })
triggerSequence := ""
if total := len(h.handlers); total != 2 {
t.Fatalf("Expected 2 handlers, found %d", total)
f1 := func(data int) error { triggerSequence += "f1"; return nil }
f2 := func(data int) error { triggerSequence += "f2"; return nil }
f3 := func(data int) error { triggerSequence += "f3"; return nil }
f4 := func(data int) error { triggerSequence += "f4"; return nil }
h.Add(f1)
h.Add(f2)
h.PreAdd(f3)
h.PreAdd(f4)
h.Trigger(1)
if total := len(h.handlers); total != 4 {
t.Fatalf("Expected %d handlers, found %d", 4, total)
}
expectedTriggerSequence := "f4f3f1f2"
if triggerSequence != expectedTriggerSequence {
t.Fatalf("Expected trigger sequence %s, got %s", expectedTriggerSequence, triggerSequence)
}
}
+105
View File
@@ -0,0 +1,105 @@
package mailer
import (
"regexp"
"strings"
"github.com/pocketbase/pocketbase/tools/list"
"golang.org/x/net/html"
)
var whitespaceRegex = regexp.MustCompile(`\s+`)
// Very rudimentary auto HTML to Text mail body converter.
//
// Caveats:
// - This method doesn't check for correctness of the HTML document.
// - Links will be converted to "[text](url)" format.
// - List items (<li>) are prefixed with "- ".
// - Indentation is stripped (both tabs and spaces).
// - Trailing spaces are preserved.
// - Multiple consequence newlines are collapsed as one unless multiple <br> tags are used.
func html2Text(htmlDocument string) (string, error) {
var builder strings.Builder
doc, err := html.Parse(strings.NewReader(htmlDocument))
if err != nil {
return "", err
}
tagsToSkip := []string{
"style", "script", "iframe", "applet", "object", "svg", "img",
"button", "form", "textarea", "input", "select", "option", "template",
}
inlineTags := []string{
"a", "span", "small", "strike", "strong",
"sub", "sup", "em", "b", "u", "i",
}
var canAddNewLine bool
// see https://pkg.go.dev/golang.org/x/net/html#Parse
var f func(*html.Node)
f = func(n *html.Node) {
// start link wrapping for producing "[text](link)" formatted string
isLink := n.Type == html.ElementNode && n.Data == "a"
if isLink {
builder.WriteString("[")
}
switch n.Type {
case html.TextNode:
txt := whitespaceRegex.ReplaceAllString(n.Data, " ")
// the prev node has new line so it is safe to trim the indentation
if !canAddNewLine {
txt = strings.TrimLeft(txt, " ")
}
if txt != "" {
builder.WriteString(txt)
canAddNewLine = true
}
case html.ElementNode:
if n.Data == "br" {
// always write new lines when <br> tag is used
builder.WriteString("\r\n")
canAddNewLine = false
} else if canAddNewLine && !list.ExistInSlice(n.Data, inlineTags) {
builder.WriteString("\r\n")
canAddNewLine = false
}
// prefix list items with dash
if n.Data == "li" {
builder.WriteString("- ")
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type != html.ElementNode || !list.ExistInSlice(c.Data, tagsToSkip) {
f(c)
}
}
// end link wrapping
if isLink {
builder.WriteString("]")
for _, a := range n.Attr {
if a.Key == "href" {
if a.Val != "" {
builder.WriteString("(")
builder.WriteString(a.Val)
builder.WriteString(")")
}
break
}
}
}
}
f(doc)
return strings.TrimSpace(builder.String()), nil
}
+131
View File
@@ -0,0 +1,131 @@
package mailer
import (
"testing"
)
func TestHtml2Text(t *testing.T) {
scenarios := []struct {
html string
expected string
}{
{
"",
"",
},
{
"ab c",
"ab c",
},
{
"<!-- test html comment -->",
"",
},
{
"<!-- test html comment --> a ",
"a",
},
{
"<span>a</span>b<span>c</span>",
"abc",
},
{
`<a href="a/b/c">test</span>`,
"[test](a/b/c)",
},
{
`<a href="">test</span>`,
"[test]",
},
{
"<span>a</span> <span>b</span>",
"a b",
},
{
"<span>a</span> b <span>c</span>",
"a b c",
},
{
"<span>a</span> b <div>c</div>",
"a b \r\nc",
},
{
`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body {
padding: 0;
}
</style>
</head>
<body>
<!-- test html comment -->
<style>
body {
padding: 0;
}
</style>
<div class="wrapper">
<div class="content">
<p>Lorem ipsum</p>
<p>Dolor sit amet</p>
<p>
<a href="a/b/c">Verify</a>
</p>
<br>
<p>
<a href="a/b/c"><strong>Verify2.1</strong> <strong>Verify2.2</strong></a>
</p>
<br>
<br>
<div>
<div>
<div>
<ul>
<li>ul.test1</li>
<li>ul.test2</li>
<li>ul.test3</li>
</ul>
<ol>
<li>ol.test1</li>
<li>ol.test2</li>
<li>ol.test3</li>
</ol>
</div>
</div>
</div>
<select>
<option>Option 1</option>
<option>Option 2</option>
</select>
<textarea>test</textarea>
<input type="text" value="test" />
<button>test</button>
<p>
Thanks,<br/>
PocketBase team
</p>
</div>
</div>
</body>
</html>
`,
"Lorem ipsum \r\nDolor sit amet \r\n[Verify](a/b/c) \r\n[Verify2.1 Verify2.2](a/b/c) \r\n\r\n- ul.test1 \r\n- ul.test2 \r\n- ul.test3 \r\n- ol.test1 \r\n- ol.test2 \r\n- ol.test3 \r\nThanks,\r\nPocketBase team",
},
}
for i, s := range scenarios {
result, err := html2Text(s.html)
if err != nil {
t.Errorf("(%d) Unexpected error %v", i, err)
}
if result != s.expected {
t.Errorf("(%d) Expected \n(%q)\n%v,\n\ngot:\n\n(%q)\n%v", i, s.expected, s.expected, result, result)
}
}
}
+5
View File
@@ -68,6 +68,11 @@ func (m *SmtpClient) Send(
yak.Subject(subject)
yak.HTML().Set(htmlContent)
// try to generate a plain text version of the HTML
if plain, err := html2Text(htmlContent); err == nil {
yak.Plain().Set(plain)
}
for name, data := range attachments {
yak.Attach(name, data)
}
+12
View File
@@ -103,16 +103,28 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
case fexpr.SignNeq:
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", lName, rName), params), nil
case fexpr.SignLike:
// both sides are columns and therefore wrap the right side with "%" for contains like behavior
if len(params) == 0 {
return dbx.NewExp(fmt.Sprintf("%s LIKE ('%%' || %s || '%%')", lName, rName), params), nil
}
// normalize operands and switch sides if the left operand is a number or text
if len(lParams) > 0 {
return dbx.NewExp(fmt.Sprintf("%s LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
}
return dbx.NewExp(fmt.Sprintf("%s LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
case fexpr.SignNlike:
// both sides are columns and therefore wrap the right side with "%" for not-contains like behavior
if len(params) == 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE ('%%' || %s || '%%')", lName, rName), params), nil
}
// normalize operands and switch sides if the left operand is a number or text
if len(lParams) > 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
}
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
case fexpr.SignLt:
return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), params), nil
+28
View File
@@ -32,6 +32,34 @@ func TestFilterDataBuildExpr(t *testing.T) {
regexp.QuoteMeta("}") +
"$",
},
// like with 2 columns
{"test1 ~ test2", false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE ('%' || [[test2]] || '%')") +
"$",
},
// reversed like with text
{"'lorem' ~ test1", false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
},
// not like with 2 columns
{"test1 !~ test2", false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE ('%' || [[test2]] || '%')") +
"$",
},
// reversed not like with text
{"'lorem' !~ test1", false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
},
// current datetime constant
{"test1 > @now", false,
"^" +
+2 -2
View File
@@ -13,7 +13,7 @@ import (
const DefaultPerPage int = 30
// MaxPerPage specifies the maximum allowed search result items returned in a single page.
const MaxPerPage int = 200
const MaxPerPage int = 400
// url search query params
const (
@@ -199,7 +199,7 @@ func (s *Provider) Exec(items any) (*Result, error) {
// count
var totalCount int64
countQuery := modelsQuery
countQuery.Distinct(false).Select("COUNT(*)")
countQuery.Distinct(false).Select("COUNT(*)").OrderBy() // unset ORDER BY statements
if s.countColumn != "" {
countQuery.Select("COUNT(DISTINCT(" + s.countColumn + "))")
}
+9 -9
View File
@@ -238,7 +238,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":10,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10",
},
},
@@ -252,7 +252,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":30,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30",
},
},
@@ -288,8 +288,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 200",
"SELECT COUNT(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2)",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 400",
},
},
// valid sort and filter fields (zero results)
@@ -302,7 +302,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC",
"SELECT COUNT(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', ''))",
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
},
},
@@ -316,7 +316,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
},
},
@@ -330,7 +330,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(DISTINCT(test.test1)) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
"SELECT COUNT(DISTINCT(test.test1)) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
},
},
@@ -437,9 +437,9 @@ func TestProviderParseAndExec(t *testing.T) {
},
// valid query params
{
"page=3&perPage=555&filter=test1>1&sort=-test2,test3",
"page=3&perPage=9999&filter=test1>1&sort=-test2,test3",
false,
`{"page":1,"perPage":200,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
`{"page":1,"perPage":400,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
},
}
+1 -1
View File
@@ -4,4 +4,4 @@ PB_PROFILE_COLLECTION = "profiles"
PB_INSTALLER_PARAM = "installer"
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/manage-collections#rules-filters-syntax"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.5.2"
PB_VERSION = "v0.7.7"
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
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{S as E,i as G,s as I,F as K,c as A,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as k,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.c92519c5.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&&Z(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,F,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(),A(r.$$.fragment),p=k(),A(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,F||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],F=!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),F=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){A(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),X("Successfully set a new admin password."),Y("/")}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};
import{S as E,i as G,s as I,F as K,c as A,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as C,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.7e58656e.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&&Z(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=C(),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=C(),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,P,v,k,F,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=C(),A(r.$$.fragment),p=C(),A(d.$$.fragment),n=C(),i=c("button"),g=c("span"),g.textContent="Set new password",R=C(),P=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(P,"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,P,$),_(P,v),k=!0,F||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],F=!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),(!k||$&4)&&(i.disabled=a[2]),(!k||$&4)&&L(i,"btn-loading",a[2])},i(a){k||(H(r.$$.fragment,a),H(d.$$.fragment,a),k=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),k=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(P),F=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){A(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),X("Successfully set a new admin password."),Y("/")}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};
@@ -1,2 +0,0 @@
import{S as M,i as T,s as j,F as z,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 A,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as E,v as I,w as h,x as J,y as P,z as R}from"./index.c92519c5.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=E(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=E(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=A(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 z({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 M{constructor(e){super(),T(this,e,W,V,j,{})}}export{Y as default};
@@ -0,0 +1,2 @@
import{S as M,i as T,s as j,F as z,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 A,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as E,v as I,w as h,x as J,y as P,z as R}from"./index.7e58656e.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=E(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]),(!a||$&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=E(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=A(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 z({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 M{constructor(e){super(),T(this,e,W,V,j,{})}}export{Y as default};
@@ -1,4 +1,4 @@
import{S as J,i as M,s as N,F as R,c as T,m as L,t as v,a as y,d as z,C as U,E as W,g as _,k as Y,n as j,o as b,_ as A,p as B,q as D,e as m,w as C,b as h,f as d,r as F,h as k,u as q,v as G,y as E,x as I,z as H}from"./index.c92519c5.js";function K(r){let e,t,s,l,n,o,c,a,i,u,g,$,p=r[3]&&S(r);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[Q,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),t=m("div"),s=m("h5"),l=C(`Type your password to confirm changing your email address
`),p&&p.c(),n=h(),T(o.$$.fragment),c=h(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","content txt-center m-b-base"),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),k(e,t),k(t,s),k(s,l),p&&p.m(s,null),k(e,n),L(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||($=q(e,"submit",G(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=S(f),p.c(),p.m(s,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&&F(a,"btn-loading",f[1])},i(f){u||(v(o.$$.fragment,f),u=!0)},o(f){y(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),z(o),g=!1,$()}}}function O(r){let e,t,s,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
import{S as G,i as J,s as M,F as N,c as T,m as L,t as v,a as y,d as z,C as R,E as U,g as _,k as W,n as Y,o as b,G as j,p as A,q as B,e as m,w as C,b as h,f as d,r as F,h as k,u as q,v as D,y as E,x as I,z as H}from"./index.7e58656e.js";function K(r){let e,t,s,l,n,o,c,a,i,u,g,$,p=r[3]&&S(r);return o=new B({props:{class:"form-field required",name:"password",$$slots:{default:[Q,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),t=m("div"),s=m("h5"),l=C(`Type your password to confirm changing your email address
`),p&&p.c(),n=h(),T(o.$$.fragment),c=h(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","content txt-center m-b-base"),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),k(e,t),k(t,s),k(s,l),p&&p.m(s,null),k(e,n),L(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||($=q(e,"submit",D(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=S(f),p.c(),p.m(s,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]),(!u||w&2)&&F(a,"btn-loading",f[1])},i(f){u||(v(o.$$.fragment,f),u=!0)},o(f){y(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),z(o),g=!1,$()}}}function O(r){let e,t,s,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>Successfully changed the user email address.</p>
<p>You can now sign in with your new email address.</p></div>`,t=h(),s=m("button"),s.textContent="Close",d(e,"class","alert alert-success"),d(s,"type","button"),d(s,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,t,c),_(o,s,c),l||(n=q(s,"click",r[6]),l=!0)},p:E,i:E,o:E,d(o){o&&b(e),o&&b(t),o&&b(s),l=!1,n()}}}function S(r){let e,t,s;return{c(){e=C("to "),t=m("strong"),s=C(r[3]),d(t,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,t,n),k(t,s)},p(l,n){n&8&&I(s,l[3])},d(l){l&&b(e),l&&b(t)}}}function Q(r){let e,t,s,l,n,o,c,a;return{c(){e=m("label"),t=C("Password"),l=h(),n=m("input"),d(e,"for",s=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,t),_(i,l,u),_(i,n,u),H(n,r[0]),n.focus(),c||(a=q(n,"input",r[7]),c=!0)},p(i,u){u&256&&s!==(s=i[8])&&d(e,"for",s),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&H(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function V(r){let e,t,s,l;const n=[O,K],o=[];function c(a,i){return a[2]?0:1}return e=c(r),t=o[e]=n[e](r),{c(){t.c(),s=W()},m(a,i){o[e].m(a,i),_(a,s,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(Y(),y(o[u],1,1,()=>{o[u]=null}),j(),t=o[e],t?t.p(a,i):(t=o[e]=n[e](a),t.c()),v(t,1),t.m(s.parentNode,s))},i(a){l||(v(t),l=!0)},o(a){y(t),l=!1},d(a){o[e].d(a),a&&b(s)}}}function X(r){let e,t;return e=new R({props:{nobranding:!0,$$slots:{default:[V]},$$scope:{ctx:r}}}),{c(){T(e.$$.fragment)},m(s,l){L(e,s,l),t=!0},p(s,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:s}),e.$set(n)},i(s){t||(v(e.$$.fragment,s),t=!0)},o(s){y(e.$$.fragment,s),t=!1},d(s){z(e,s)}}}function Z(r,e,t){let s,{params:l}=e,n="",o=!1,c=!1;async function a(){if(o)return;t(1,o=!0);const g=new A("../");try{await g.users.confirmEmailChange(l==null?void 0:l.token,n),t(2,c=!0)}catch($){B.errorResponseHandler($)}t(1,o=!1)}const i=()=>window.close();function u(){n=this.value,t(0,n)}return r.$$set=g=>{"params"in g&&t(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&t(3,s=U.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,s,a,l,i,u]}class ee extends J{constructor(e){super(),M(this,e,Z,X,N,{params:5})}}export{ee as default};
<p>You can now sign in with your new email address.</p></div>`,t=h(),s=m("button"),s.textContent="Close",d(e,"class","alert alert-success"),d(s,"type","button"),d(s,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,t,c),_(o,s,c),l||(n=q(s,"click",r[6]),l=!0)},p:E,i:E,o:E,d(o){o&&b(e),o&&b(t),o&&b(s),l=!1,n()}}}function S(r){let e,t,s;return{c(){e=C("to "),t=m("strong"),s=C(r[3]),d(t,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,t,n),k(t,s)},p(l,n){n&8&&I(s,l[3])},d(l){l&&b(e),l&&b(t)}}}function Q(r){let e,t,s,l,n,o,c,a;return{c(){e=m("label"),t=C("Password"),l=h(),n=m("input"),d(e,"for",s=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,t),_(i,l,u),_(i,n,u),H(n,r[0]),n.focus(),c||(a=q(n,"input",r[7]),c=!0)},p(i,u){u&256&&s!==(s=i[8])&&d(e,"for",s),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&H(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function V(r){let e,t,s,l;const n=[O,K],o=[];function c(a,i){return a[2]?0:1}return e=c(r),t=o[e]=n[e](r),{c(){t.c(),s=U()},m(a,i){o[e].m(a,i),_(a,s,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(W(),y(o[u],1,1,()=>{o[u]=null}),Y(),t=o[e],t?t.p(a,i):(t=o[e]=n[e](a),t.c()),v(t,1),t.m(s.parentNode,s))},i(a){l||(v(t),l=!0)},o(a){y(t),l=!1},d(a){o[e].d(a),a&&b(s)}}}function X(r){let e,t;return e=new N({props:{nobranding:!0,$$slots:{default:[V]},$$scope:{ctx:r}}}),{c(){T(e.$$.fragment)},m(s,l){L(e,s,l),t=!0},p(s,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:s}),e.$set(n)},i(s){t||(v(e.$$.fragment,s),t=!0)},o(s){y(e.$$.fragment,s),t=!1},d(s){z(e,s)}}}function Z(r,e,t){let s,{params:l}=e,n="",o=!1,c=!1;async function a(){if(o)return;t(1,o=!0);const g=new j("../");try{await g.users.confirmEmailChange(l==null?void 0:l.token,n),t(2,c=!0)}catch($){A.errorResponseHandler($)}t(1,o=!1)}const i=()=>window.close();function u(){n=this.value,t(0,n)}return r.$$set=g=>{"params"in g&&t(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&t(3,s=R.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,s,a,l,i,u]}class ee extends G{constructor(e){super(),J(this,e,Z,X,M,{params:5})}}export{ee as default};
@@ -1,4 +1,4 @@
import{S as U,i as W,s as Y,F as j,c as H,m as N,t as P,a as q,d as L,C as A,E as B,g as _,k as D,n as G,o as m,_ as I,p as K,q as E,e as b,w as y,b as C,f as c,r as J,h as w,u as S,v as O,y as F,x as Q,z as R}from"./index.c92519c5.js";function V(i){let e,l,s,n,t,o,p,u,r,a,v,g,k,h,d=i[4]&&M(i);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),u=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=y(`Reset your user password
`),d&&d.c(),t=C(),H(o.$$.fragment),p=C(),H(u.$$.fragment),r=C(),a=b("button"),v=b("span"),v.textContent="Set new password",c(l,"class","content txt-center m-b-base"),c(v,"class","txt"),c(a,"type","submit"),c(a,"class","btn btn-lg btn-block"),a.disabled=i[2],J(a,"btn-loading",i[2])},m(f,$){_(f,e,$),w(e,l),w(l,s),w(s,n),d&&d.m(s,null),w(e,t),N(o,e,null),w(e,p),N(u,e,null),w(e,r),w(e,a),w(a,v),g=!0,k||(h=S(e,"submit",O(i[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=M(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const z={};$&3074&&(z.$$scope={dirty:$,ctx:f}),u.$set(z),(!g||$&4)&&(a.disabled=f[2]),$&4&&J(a,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(u.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(u.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),L(o),L(u),k=!1,h()}}}function X(i){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
import{S as M,i as U,s as W,F as Y,c as H,m as N,t as P,a as q,d as L,C as j,E as A,g as _,k as B,n as D,o as m,G as I,p as K,q as E,e as b,w as y,b as C,f as c,r as G,h as w,u as S,v as O,y as F,x as Q,z as R}from"./index.7e58656e.js";function V(i){let e,l,s,n,t,o,p,u,r,a,v,k,g,h,d=i[4]&&J(i);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),u=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=y(`Reset your user password
`),d&&d.c(),t=C(),H(o.$$.fragment),p=C(),H(u.$$.fragment),r=C(),a=b("button"),v=b("span"),v.textContent="Set new password",c(l,"class","content txt-center m-b-base"),c(v,"class","txt"),c(a,"type","submit"),c(a,"class","btn btn-lg btn-block"),a.disabled=i[2],G(a,"btn-loading",i[2])},m(f,$){_(f,e,$),w(e,l),w(l,s),w(s,n),d&&d.m(s,null),w(e,t),N(o,e,null),w(e,p),N(u,e,null),w(e,r),w(e,a),w(a,v),k=!0,g||(h=S(e,"submit",O(i[5])),g=!0)},p(f,$){f[4]?d?d.p(f,$):(d=J(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const z={};$&3074&&(z.$$scope={dirty:$,ctx:f}),u.$set(z),(!k||$&4)&&(a.disabled=f[2]),(!k||$&4)&&G(a,"btn-loading",f[2])},i(f){k||(P(o.$$.fragment,f),P(u.$$.fragment,f),k=!0)},o(f){q(o.$$.fragment,f),q(u.$$.fragment,f),k=!1},d(f){f&&m(e),d&&d.d(),L(o),L(u),g=!1,h()}}}function X(i){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Successfully changed the user password.</p>
<p>You can now sign in with your new password.</p></div>`,l=C(),s=b("button"),s.textContent="Close",c(e,"class","alert alert-success"),c(s,"type","button"),c(s,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,s,p),n||(t=S(s,"click",i[7]),n=!0)},p:F,i:F,o:F,d(o){o&&m(e),o&&m(l),o&&m(s),n=!1,t()}}}function M(i){let e,l,s;return{c(){e=y("for "),l=b("strong"),s=y(i[4])},m(n,t){_(n,e,t),_(n,l,t),w(l,s)},p(n,t){t&16&&Q(s,n[4])},d(n){n&&m(e),n&&m(l)}}}function Z(i){let e,l,s,n,t,o,p,u;return{c(){e=b("label"),l=y("New password"),n=C(),t=b("input"),c(e,"for",s=i[10]),c(t,"type","password"),c(t,"id",o=i[10]),t.required=!0,t.autofocus=!0},m(r,a){_(r,e,a),w(e,l),_(r,n,a),_(r,t,a),R(t,i[0]),t.focus(),p||(u=S(t,"input",i[8]),p=!0)},p(r,a){a&1024&&s!==(s=r[10])&&c(e,"for",s),a&1024&&o!==(o=r[10])&&c(t,"id",o),a&1&&t.value!==r[0]&&R(t,r[0])},d(r){r&&m(e),r&&m(n),r&&m(t),p=!1,u()}}}function x(i){let e,l,s,n,t,o,p,u;return{c(){e=b("label"),l=y("New password confirm"),n=C(),t=b("input"),c(e,"for",s=i[10]),c(t,"type","password"),c(t,"id",o=i[10]),t.required=!0},m(r,a){_(r,e,a),w(e,l),_(r,n,a),_(r,t,a),R(t,i[1]),p||(u=S(t,"input",i[9]),p=!0)},p(r,a){a&1024&&s!==(s=r[10])&&c(e,"for",s),a&1024&&o!==(o=r[10])&&c(t,"id",o),a&2&&t.value!==r[1]&&R(t,r[1])},d(r){r&&m(e),r&&m(n),r&&m(t),p=!1,u()}}}function ee(i){let e,l,s,n;const t=[X,V],o=[];function p(u,r){return u[3]?0:1}return e=p(i),l=o[e]=t[e](i),{c(){l.c(),s=B()},m(u,r){o[e].m(u,r),_(u,s,r),n=!0},p(u,r){let a=e;e=p(u),e===a?o[e].p(u,r):(D(),q(o[a],1,1,()=>{o[a]=null}),G(),l=o[e],l?l.p(u,r):(l=o[e]=t[e](u),l.c()),P(l,1),l.m(s.parentNode,s))},i(u){n||(P(l),n=!0)},o(u){q(l),n=!1},d(u){o[e].d(u),u&&m(s)}}}function te(i){let e,l;return e=new j({props:{nobranding:!0,$$slots:{default:[ee]},$$scope:{ctx:i}}}),{c(){H(e.$$.fragment)},m(s,n){N(e,s,n),l=!0},p(s,[n]){const t={};n&2079&&(t.$$scope={dirty:n,ctx:s}),e.$set(t)},i(s){l||(P(e.$$.fragment,s),l=!0)},o(s){q(e.$$.fragment,s),l=!1},d(s){L(e,s)}}}function se(i,e,l){let s,{params:n}=e,t="",o="",p=!1,u=!1;async function r(){if(p)return;l(2,p=!0);const k=new I("../");try{await k.users.confirmPasswordReset(n==null?void 0:n.token,t,o),l(3,u=!0)}catch(h){K.errorResponseHandler(h)}l(2,p=!1)}const a=()=>window.close();function v(){t=this.value,l(0,t)}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,s=A.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,p,u,s,r,n,a,v,g]}class ne extends U{constructor(e){super(),W(this,e,se,te,Y,{params:6})}}export{ne as default};
<p>You can now sign in with your new password.</p></div>`,l=C(),s=b("button"),s.textContent="Close",c(e,"class","alert alert-success"),c(s,"type","button"),c(s,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,s,p),n||(t=S(s,"click",i[7]),n=!0)},p:F,i:F,o:F,d(o){o&&m(e),o&&m(l),o&&m(s),n=!1,t()}}}function J(i){let e,l,s;return{c(){e=y("for "),l=b("strong"),s=y(i[4])},m(n,t){_(n,e,t),_(n,l,t),w(l,s)},p(n,t){t&16&&Q(s,n[4])},d(n){n&&m(e),n&&m(l)}}}function Z(i){let e,l,s,n,t,o,p,u;return{c(){e=b("label"),l=y("New password"),n=C(),t=b("input"),c(e,"for",s=i[10]),c(t,"type","password"),c(t,"id",o=i[10]),t.required=!0,t.autofocus=!0},m(r,a){_(r,e,a),w(e,l),_(r,n,a),_(r,t,a),R(t,i[0]),t.focus(),p||(u=S(t,"input",i[8]),p=!0)},p(r,a){a&1024&&s!==(s=r[10])&&c(e,"for",s),a&1024&&o!==(o=r[10])&&c(t,"id",o),a&1&&t.value!==r[0]&&R(t,r[0])},d(r){r&&m(e),r&&m(n),r&&m(t),p=!1,u()}}}function x(i){let e,l,s,n,t,o,p,u;return{c(){e=b("label"),l=y("New password confirm"),n=C(),t=b("input"),c(e,"for",s=i[10]),c(t,"type","password"),c(t,"id",o=i[10]),t.required=!0},m(r,a){_(r,e,a),w(e,l),_(r,n,a),_(r,t,a),R(t,i[1]),p||(u=S(t,"input",i[9]),p=!0)},p(r,a){a&1024&&s!==(s=r[10])&&c(e,"for",s),a&1024&&o!==(o=r[10])&&c(t,"id",o),a&2&&t.value!==r[1]&&R(t,r[1])},d(r){r&&m(e),r&&m(n),r&&m(t),p=!1,u()}}}function ee(i){let e,l,s,n;const t=[X,V],o=[];function p(u,r){return u[3]?0:1}return e=p(i),l=o[e]=t[e](i),{c(){l.c(),s=A()},m(u,r){o[e].m(u,r),_(u,s,r),n=!0},p(u,r){let a=e;e=p(u),e===a?o[e].p(u,r):(B(),q(o[a],1,1,()=>{o[a]=null}),D(),l=o[e],l?l.p(u,r):(l=o[e]=t[e](u),l.c()),P(l,1),l.m(s.parentNode,s))},i(u){n||(P(l),n=!0)},o(u){q(l),n=!1},d(u){o[e].d(u),u&&m(s)}}}function te(i){let e,l;return e=new Y({props:{nobranding:!0,$$slots:{default:[ee]},$$scope:{ctx:i}}}),{c(){H(e.$$.fragment)},m(s,n){N(e,s,n),l=!0},p(s,[n]){const t={};n&2079&&(t.$$scope={dirty:n,ctx:s}),e.$set(t)},i(s){l||(P(e.$$.fragment,s),l=!0)},o(s){q(e.$$.fragment,s),l=!1},d(s){L(e,s)}}}function se(i,e,l){let s,{params:n}=e,t="",o="",p=!1,u=!1;async function r(){if(p)return;l(2,p=!0);const g=new I("../");try{await g.users.confirmPasswordReset(n==null?void 0:n.token,t,o),l(3,u=!0)}catch(h){K.errorResponseHandler(h)}l(2,p=!1)}const a=()=>window.close();function v(){t=this.value,l(0,t)}function k(){o=this.value,l(1,o)}return i.$$set=g=>{"params"in g&&l(6,n=g.params)},i.$$.update=()=>{i.$$.dirty&64&&l(4,s=j.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,p,u,s,r,n,a,v,k]}class ne extends M{constructor(e){super(),U(this,e,se,te,W,{params:6})}}export{ne as default};
@@ -1,3 +1,3 @@
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,_ 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.c92519c5.js";function P(c){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,G 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.7e58656e.js";function P(c){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,o){r(l,t,o),r(l,s,o),r(l,e,o),n||(i=_(e,"click",c[4]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function S(c){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,o){r(l,t,o),r(l,s,o),r(l,e,o),n||(i=_(e,"click",c[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(c){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(c){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(c),n=e(c);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 V(c){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[F]},$$scope:{ctx:c}}}),{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 q(c,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);const d=new H("../");try{await d.users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch{s(0,n=!1)}s(1,i=!1)}const o=()=>window.close(),b=()=>window.close();return c.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,o,b]}class N extends k{constructor(t){super(),v(this,t,q,V,y,{params:2})}}export{N 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,o){r(l,t,o),r(l,s,o),r(l,e,o),n||(i=_(e,"click",c[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(c){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(c){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(c),n=e(c);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 V(c){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[F]},$$scope:{ctx:c}}}),{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 q(c,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);const d=new H("../");try{await d.users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch{s(0,n=!1)}s(1,i=!1)}const o=()=>window.close(),b=()=>window.close();return c.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,o,b]}class I extends k{constructor(t){super(),v(this,t,q,V,y,{params:2})}}export{I as default};
File diff suppressed because one or more lines are too long
+661
View File
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
-661
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -24,8 +24,8 @@
window.Prism = window.Prism || {};
window.Prism.manual = true;
</script>
<script type="module" crossorigin src="./assets/index.c92519c5.js"></script>
<link rel="stylesheet" href="./assets/index.8992dfa3.css">
<script type="module" crossorigin src="./assets/index.7e58656e.js"></script>
<link rel="stylesheet" href="./assets/index.26987507.css">
</head>
<body>
<div id="app"></div>
+320 -293
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -25,7 +25,7 @@
"chart.js": "^3.7.1",
"chartjs-adapter-luxon": "^1.1.0",
"luxon": "^2.3.2",
"pocketbase": "^0.5.0",
"pocketbase": "^0.7.0",
"prismjs": "^1.28.0",
"sass": "^1.45.0",
"svelte": "^3.44.0",
+1 -1
View File
@@ -189,7 +189,7 @@
submitShortcut,
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
searchKeymap.find((item) => item.key === "Mod-d"),
...historyKeymap,
...completionKeymap,
]),
@@ -270,7 +270,6 @@
}
const skipFields = [
"@request.user.profile.id",
"@request.user.profile.userId",
"@request.user.profile.created",
"@request.user.profile.updated",
@@ -379,7 +378,7 @@
submitShortcut,
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
searchKeymap.find((item) => item.key === "Mod-d"),
...historyKeymap,
...completionKeymap,
]),
+5 -6
View File
@@ -1,14 +1,13 @@
<script>
import CommonHelper from "@/utils/CommonHelper";
import tooltip from "@/actions/tooltip";
export let date = "";
// note: manual trim the ms without converting to DateTime
// to help improving the rendering performance in large data sets
let shortDate = date.length > 19 ? date.substring(0, 19) : date;
</script>
{#if date}
<span class="txt" use:tooltip={CommonHelper.formatToLocalDate(date) + " Local"}>
{CommonHelper.formatToUTCDate(date)} UTC
</span>
<span class="txt">{shortDate} UTC</span>
{:else}
<span class="txt txt-hint">N/A</span>
{/if}
+3 -2
View File
@@ -25,8 +25,9 @@
let newSelected = [];
for (let item of items) {
if (CommonHelper.inArray(newKeyOfSelected, item[selectionKey])) {
for (let v of newKeyOfSelected) {
const item = CommonHelper.findByKey(items, selectionKey, v);
if (item) {
newSelected.push(item);
}
}

Some files were not shown because too many files have changed in this diff Show More