Compare commits

..
60 Commits
Author SHA1 Message Date
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 4e78f1e4ad updated ui/dist 2022-08-31 21:01:12 +03:00
Gani Georgiev 235670fae8 [#367] fixed wildcard multiple file upload deletion and added support to delete a single file by its name 2022-08-31 20:33:07 +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 3bfa0b6dd4 updated ui/dist 2022-08-25 11:13:52 +03:00
Gani Georgiev 9e3c59f966 reverted changes to the mailer To: address format 2022-08-25 10:59:55 +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
Gani Georgiev 0b8c7f6883 fixed typo and removed unusued import 2022-08-21 14:47:33 +03:00
Gani Georgiev 587cfc335c [#75] added option to test s3 connection and send test emails 2022-08-21 14:30:36 +03:00
Gani Georgiev 3f4f4cf031 [#282] reversed the X-Forwarded-For ips iteration 2022-08-20 08:01:54 +03:00
Gani Georgiev d4f160d959 updated the user and admin login email validator to check only the string format 2022-08-20 07:58:42 +03:00
Gani Georgiev 07cd758112 [#282] fixed "real" user ip extraction 2022-08-20 07:57:17 +03:00
Gani Georgiev beb8e7924d [#282] fixed X-Forward-For ip extraction 2022-08-20 05:56:56 +03:00
Gani Georgiev 72fdf0d116 updated test data.db 2022-08-18 21:05:17 +03:00
Gani Georgiev 7e14ea7cfb [#210] change the uploaded filename strategy to include the original filename 2022-08-18 20:44:29 +03:00
Gani Georgiev 25c4db7a30 updated ui/dist with older sdk version 2022-08-18 20:32:07 +03:00
Gani Georgiev e86b1f54a0 updated ui/dist 2022-08-18 20:28:20 +03:00
Gani Georgiev 7be389704d added hideControls setting 2022-08-18 18:45:27 +03:00
Gani Georgiev cfaff31d97 [#282] added the "real" user ip to the logs 2022-08-18 15:27:45 +03:00
Gani Georgiev 9bf66be870 fixed typos 2022-08-17 22:32:10 +03:00
Gani Georgiev efda3d5a0b [#87] added additional thumb resizers 2022-08-17 22:29:47 +03:00
Gani Georgiev a516435f2e fixed settings clone test 2022-08-16 13:37:59 +03:00
Gani Georgiev ccd010c490 simplified mail settings ui 2022-08-16 13:32:32 +03:00
Gani Georgiev 456ced75ce [#197] added now datetime filter constant 2022-08-15 22:38:17 +03:00
Gani Georgiev b7d32c23aa updated ui/dist 2022-08-15 21:42:01 +03:00
Gani Georgiev 04ddae46c9 updated SMTP recommendation 2022-08-15 21:31:25 +03:00
Gani Georgiev 7d10d20de1 [#275] added support to customize the default user email templates from the Admin UI 2022-08-14 19:30:45 +03:00
Gani Georgiev 1de56d3d9e [#302] fixed admin reset password email link 2022-08-12 17:08:36 +03:00
Gani Georgiev 80f710731c updated vite dependency 2022-08-12 12:30:11 +03:00
Gani Georgiev bd76130cd8 [#] added missing import in the api preview for deleting records 2022-08-12 12:20:56 +03:00
Gani Georgiev d60dd13581 [#294] added additional inline serving mime types 2022-08-11 20:09:26 +03:00
187 changed files with 5711 additions and 2331 deletions

No files matched your search

+1 -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 ## 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. your own custom app specific business logic and still have a single portable executable at the end.
### Installation ### Installation
+3 -3
View File
@@ -66,7 +66,7 @@ func (api *adminApi) refresh(c echo.Context) error {
func (api *adminApi) emailAuth(c echo.Context) error { func (api *adminApi) emailAuth(c echo.Context) error {
form := forms.NewAdminLogin(api.app) form := forms.NewAdminLogin(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
admin, submitErr := form.Submit() admin, submitErr := form.Submit()
@@ -80,7 +80,7 @@ func (api *adminApi) emailAuth(c echo.Context) error {
func (api *adminApi) requestPasswordReset(c echo.Context) error { func (api *adminApi) requestPasswordReset(c echo.Context) error {
form := forms.NewAdminPasswordResetRequest(api.app) form := forms.NewAdminPasswordResetRequest(api.app)
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
} }
if err := form.Validate(); err != nil { if err := form.Validate(); err != nil {
@@ -101,7 +101,7 @@ func (api *adminApi) requestPasswordReset(c echo.Context) error {
func (api *adminApi) confirmPasswordReset(c echo.Context) error { func (api *adminApi) confirmPasswordReset(c echo.Context) error {
form := forms.NewAdminPasswordResetConfirm(api.app) form := forms.NewAdminPasswordResetConfirm(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
admin, submitErr := form.Submit() admin, submitErr := form.Submit()
+3 -3
View File
@@ -119,7 +119,7 @@ func TestAdminRequestPasswordReset(t *testing.T) {
Body: strings.NewReader(`{"email":"test@example.com"}`), Body: strings.NewReader(`{"email":"test@example.com"}`),
Delay: 100 * time.Millisecond, Delay: 100 * time.Millisecond,
ExpectedStatus: 204, 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 // simulate recent password request
admin, err := app.Dao().FindAdminByEmail("test@example.com") admin, err := app.Dao().FindAdminByEmail("test@example.com")
if err != nil { if err != nil {
@@ -446,7 +446,7 @@ func TestAdminDelete(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 // delete all admins except the authorized one
adminModel := &models.Admin{} adminModel := &models.Admin{}
_, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{ _, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{
@@ -483,7 +483,7 @@ func TestAdminCreate(t *testing.T) {
Method: http.MethodPost, Method: http.MethodPost,
Url: "/api/admins", Url: "/api/admins",
Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":3}`), 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 // delete all admins
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute() _, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
if err != nil { if err != nil {
+4 -4
View File
@@ -54,7 +54,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "custom route", Name: "custom route",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/custom", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/custom", Path: "/custom",
@@ -70,7 +70,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with HTTPError", Name: "route with HTTPError",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/http-error", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/http-error", Path: "/http-error",
@@ -86,7 +86,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with api error", Name: "route with api error",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/api-error", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/api-error", Path: "/api-error",
@@ -102,7 +102,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
Name: "route with plain error", Name: "route with plain error",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/plain-error", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/plain-error", Path: "/plain-error",
+5 -5
View File
@@ -475,7 +475,7 @@ func TestCollectionImport(t *testing.T) {
`"data":{`, `"data":{`,
`"collections":{"code":"validation_required"`, `"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{} collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -501,7 +501,7 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnCollectionsBeforeImportRequest": 1, "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{} collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -542,7 +542,7 @@ func TestCollectionImport(t *testing.T) {
"OnCollectionsBeforeImportRequest": 1, "OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeCreate": 2, "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{} collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -590,7 +590,7 @@ func TestCollectionImport(t *testing.T) {
"OnModelBeforeCreate": 2, "OnModelBeforeCreate": 2,
"OnModelAfterCreate": 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{} collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -689,7 +689,7 @@ func TestCollectionImport(t *testing.T) {
"OnModelBeforeCreate": 1, "OnModelBeforeCreate": 1,
"OnModelAfterCreate": 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{} collections := []*models.Collection{}
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
+1 -1
View File
@@ -78,7 +78,7 @@ func (api *fileApi) download(c echo.Context) error {
// - if exists - compare last modified dates to determine whether the thumb should be recreated // - if exists - compare last modified dates to determine whether the thumb should be recreated
tAttrs, tAttrsErr := fs.Attributes(servedPath) tAttrs, tAttrsErr := fs.Attributes(servedPath)
if tAttrsErr != nil || oAttrs.ModTime.After(tAttrs.ModTime) { if tAttrsErr != nil || oAttrs.ModTime.After(tAttrs.ModTime) {
if err := fs.CreateThumb(originalPath, servedPath, thumbSize, false); err != nil { if err := fs.CreateThumb(originalPath, servedPath, thumbSize); err != nil {
servedPath = originalPath // fallback to the original servedPath = originalPath // fallback to the original
} }
} }
+85 -5
View File
@@ -16,7 +16,12 @@ func TestFileDownload(t *testing.T) {
dataDirRelPath := "../tests/data/" dataDirRelPath := "../tests/data/"
testFilePath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt") testFilePath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt")
testImgPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png") testImgPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png") testThumbCropCenterPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/70x50_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbCropTopPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/70x50t_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbCropBottomPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/70x50b_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbFitPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/70x50f_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbZeroWidthPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/0x50_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testThumbZeroHeightPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/70x0_4881bdef-06b4-4dea-8d97-6125ad242677.png")
testFile, fileErr := os.ReadFile(testFilePath) testFile, fileErr := os.ReadFile(testFilePath)
if fileErr != nil { if fileErr != nil {
@@ -28,7 +33,32 @@ func TestFileDownload(t *testing.T) {
t.Fatal(imgErr) t.Fatal(imgErr)
} }
testThumb, thumbErr := os.ReadFile(testThumbPath) testThumbCropCenter, thumbErr := os.ReadFile(testThumbCropCenterPath)
if thumbErr != nil {
t.Fatal(thumbErr)
}
testThumbCropTop, thumbErr := os.ReadFile(testThumbCropTopPath)
if thumbErr != nil {
t.Fatal(thumbErr)
}
testThumbCropBottom, thumbErr := os.ReadFile(testThumbCropBottomPath)
if thumbErr != nil {
t.Fatal(thumbErr)
}
testThumbFit, thumbErr := os.ReadFile(testThumbFitPath)
if thumbErr != nil {
t.Fatal(thumbErr)
}
testThumbZeroWidth, thumbErr := os.ReadFile(testThumbZeroWidthPath)
if thumbErr != nil {
t.Fatal(thumbErr)
}
testThumbZeroHeight, thumbErr := os.ReadFile(testThumbZeroHeightPath)
if thumbErr != nil { if thumbErr != nil {
t.Fatal(thumbErr) t.Fatal(thumbErr)
} }
@@ -76,11 +106,61 @@ func TestFileDownload(t *testing.T) {
}, },
}, },
{ {
Name: "existing image - existing thumb", Name: "existing image - existing thumb (crop center)",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=100x100", Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=70x50",
ExpectedStatus: 200, ExpectedStatus: 200,
ExpectedContent: []string{string(testThumb)}, ExpectedContent: []string{string(testThumbCropCenter)},
ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1,
},
},
{
Name: "existing image - existing thumb (crop top)",
Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=70x50t",
ExpectedStatus: 200,
ExpectedContent: []string{string(testThumbCropTop)},
ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1,
},
},
{
Name: "existing image - existing thumb (crop bottom)",
Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=70x50b",
ExpectedStatus: 200,
ExpectedContent: []string{string(testThumbCropBottom)},
ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1,
},
},
{
Name: "existing image - existing thumb (fit)",
Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=70x50f",
ExpectedStatus: 200,
ExpectedContent: []string{string(testThumbFit)},
ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1,
},
},
{
Name: "existing image - existing thumb (zero width)",
Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=0x50",
ExpectedStatus: 200,
ExpectedContent: []string{string(testThumbZeroWidth)},
ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1,
},
},
{
Name: "existing image - existing thumb (zero height)",
Method: http.MethodGet,
Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=70x0",
ExpectedStatus: 200,
ExpectedContent: []string{string(testThumbZeroHeight)},
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnFileDownloadRequest": 1, "OnFileDownloadRequest": 1,
}, },
+1 -1
View File
@@ -28,7 +28,7 @@ type logsApi struct {
var requestFilterFields = []string{ var requestFilterFields = []string{
"rowid", "id", "created", "updated", "rowid", "id", "created", "updated",
"url", "method", "status", "auth", "url", "method", "status", "auth",
"ip", "referer", "userAgent", "remoteIp", "userIp", "referer", "userAgent",
} }
func (api *logsApi) requestsList(c echo.Context) error { func (api *logsApi) requestsList(c echo.Context) error {
+6 -6
View File
@@ -34,7 +34,7 @@ func TestRequestsList(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -56,7 +56,7 @@ func TestRequestsList(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -103,7 +103,7 @@ func TestRequestView(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -118,7 +118,7 @@ func TestRequestView(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -161,7 +161,7 @@ func TestRequestsStats(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -178,7 +178,7 @@ func TestRequestsStats(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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 { if err := tests.MockRequestLogsData(app); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+31 -1
View File
@@ -3,6 +3,7 @@ package apis
import ( import (
"fmt" "fmt"
"log" "log"
"net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -247,12 +248,15 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
requestAuth = models.RequestAuthAdmin requestAuth = models.RequestAuthAdmin
} }
ip, _, _ := net.SplitHostPort(httpRequest.RemoteAddr)
model := &models.Request{ model := &models.Request{
Url: httpRequest.URL.RequestURI(), Url: httpRequest.URL.RequestURI(),
Method: strings.ToLower(httpRequest.Method), Method: strings.ToLower(httpRequest.Method),
Status: status, Status: status,
Auth: requestAuth, Auth: requestAuth,
Ip: httpRequest.RemoteAddr, UserIp: realUserIp(httpRequest, ip),
RemoteIp: ip,
Referer: httpRequest.Referer(), Referer: httpRequest.Referer(),
UserAgent: httpRequest.UserAgent(), UserAgent: httpRequest.UserAgent(),
Meta: meta, Meta: meta,
@@ -297,3 +301,29 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
} }
} }
} }
// Returns the "real" user IP from common proxy headers (or fallbackIp if none is found).
//
// The returned IP value shouldn't be trusted if not behind a trusted reverse proxy!
func realUserIp(r *http.Request, fallbackIp string) string {
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ipsList := r.Header.Get("X-Forwarded-For"); ipsList != "" {
ips := strings.Split(ipsList, ",")
// extract the rightmost ip
for i := len(ips) - 1; i >= 0; i-- {
ip := strings.TrimSpace(ips[i])
if ip != "" {
return ip
}
}
}
return fallbackIp
}
+26 -26
View File
@@ -18,7 +18,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -40,7 +40,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -62,7 +62,7 @@ func TestRequireGuestOnly(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -81,7 +81,7 @@ func TestRequireGuestOnly(t *testing.T) {
Name: "guest", Name: "guest",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -109,7 +109,7 @@ func TestRequireUserAuth(t *testing.T) {
Name: "guest", Name: "guest",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -131,7 +131,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -153,7 +153,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -175,7 +175,7 @@ func TestRequireUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -203,7 +203,7 @@ func TestRequireAdminAuth(t *testing.T) {
Name: "guest", Name: "guest",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -225,7 +225,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -247,7 +247,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -269,7 +269,7 @@ func TestRequireAdminAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -297,7 +297,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
Name: "guest (while having at least 1 existing admin)", Name: "guest (while having at least 1 existing admin)",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -316,7 +316,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
Name: "guest (while having 0 existing admins)", Name: "guest (while having 0 existing admins)",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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 // delete all admins
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute() _, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
if err != nil { if err != nil {
@@ -344,7 +344,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -366,7 +366,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -388,7 +388,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -416,7 +416,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
Name: "guest", Name: "guest",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -438,7 +438,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -460,7 +460,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -482,7 +482,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test", Path: "/my/test",
@@ -510,7 +510,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
Name: "guest", Name: "guest",
Method: http.MethodGet, Method: http.MethodGet,
Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", 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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test/:id", Path: "/my/test/:id",
@@ -532,7 +532,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test/:id", Path: "/my/test/:id",
@@ -555,7 +555,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
// test3@example.com // test3@example.com
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test/:id", Path: "/my/test/:id",
@@ -577,7 +577,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test/:id", Path: "/my/test/:id",
@@ -599,7 +599,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", "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{ e.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/my/test/:custom", Path: "/my/test/:custom",
+3
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("Content-Type", "text/event-stream; charset=UTF-8")
c.Response().Header().Set("Cache-Control", "no-store") c.Response().Header().Set("Cache-Control", "no-store")
c.Response().Header().Set("Connection", "keep-alive") 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{ event := &core.RealtimeConnectEvent{
HttpContext: c, HttpContext: c,
+11 -11
View File
@@ -27,7 +27,7 @@ func TestRealtimeConnect(t *testing.T) {
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnRealtimeConnectRequest": 1, "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 { if len(app.SubscriptionsBroker().Clients()) != 0 {
t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients())) 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, "OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 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") client.Subscribe("test0")
app.SubscriptionsBroker().Register(client) 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 { if len(client.Subscriptions()) != 0 {
t.Errorf("Expected no subscriptions, got %v", client.Subscriptions()) t.Errorf("Expected no subscriptions, got %v", client.Subscriptions())
} }
@@ -89,11 +89,11 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1, "OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 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") client.Subscribe("test0")
app.SubscriptionsBroker().Register(client) 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"} expectedSubs := []string{"test1", "test2"}
if len(expectedSubs) != len(client.Subscriptions()) { if len(expectedSubs) != len(client.Subscriptions()) {
t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions()) t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions())
@@ -120,10 +120,10 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1, "OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 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) 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) admin, _ := client.Get(apis.ContextAdminKey).(*models.Admin)
if admin == nil { if admin == nil {
t.Errorf("Expected admin auth model, got nil") t.Errorf("Expected admin auth model, got nil")
@@ -144,10 +144,10 @@ func TestRealtimeSubscribe(t *testing.T) {
"OnRealtimeBeforeSubscribeRequest": 1, "OnRealtimeBeforeSubscribeRequest": 1,
"OnRealtimeAfterSubscribeRequest": 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) 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) user, _ := client.Get(apis.ContextUserKey).(*models.User)
if user == nil { if user == nil {
t.Errorf("Expected user auth model, got nil") t.Errorf("Expected user auth model, got nil")
@@ -165,14 +165,14 @@ func TestRealtimeSubscribe(t *testing.T) {
}, },
ExpectedStatus: 403, ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`}, 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 := &models.User{}
initialAuth.RefreshId() initialAuth.RefreshId()
client.Set(apis.ContextUserKey, initialAuth) client.Set(apis.ContextUserKey, initialAuth)
app.SubscriptionsBroker().Register(client) 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) user, _ := client.Get(apis.ContextUserKey).(*models.User)
if user == nil { if user == nil {
t.Errorf("Expected user auth model, got 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 "OnModelBeforeDelete": 3, // +2 cascade delete related records
"OnModelAfterDelete": 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, "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", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f") 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 "OnModelBeforeDelete": 3, // +2 cascade delete related records
"OnModelAfterDelete": 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, "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", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f") ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
@@ -501,7 +501,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 1, "OnModelBeforeDelete": 1,
"OnModelAfterDelete": 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") ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
}, },
}, },
@@ -529,7 +529,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 1, "OnModelBeforeDelete": 1,
"OnModelAfterDelete": 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") ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
}, },
}, },
@@ -562,7 +562,7 @@ func TestRecordDelete(t *testing.T) {
"OnModelBeforeDelete": 3, "OnModelBeforeDelete": 3,
"OnModelAfterDelete": 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" recId := "63c2ab80-84ab-4057-a592-4604a731f78f"
col, _ := app.Dao().FindCollectionByNameOrId("demo2") col, _ := app.Dao().FindCollectionByNameOrId("demo2")
rec, _ := app.Dao().FindRecordById(col, recId, nil) rec, _ := app.Dao().FindRecordById(col, recId, nil)
+51 -1
View File
@@ -3,10 +3,12 @@ package apis
import ( import (
"net/http" "net/http"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tools/rest" "github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/tools/security"
) )
// BindSettingsApi registers the settings api endpoints. // BindSettingsApi registers the settings api endpoints.
@@ -16,6 +18,8 @@ func BindSettingsApi(app core.App, rg *echo.Group) {
subGroup := rg.Group("/settings", ActivityLogger(app), RequireAdminAuth()) subGroup := rg.Group("/settings", ActivityLogger(app), RequireAdminAuth())
subGroup.GET("", api.list) subGroup.GET("", api.list)
subGroup.PATCH("", api.set) subGroup.PATCH("", api.set)
subGroup.POST("/test/s3", api.testS3)
subGroup.POST("/test/email", api.testEmail)
} }
type settingsApi struct { type settingsApi struct {
@@ -43,7 +47,7 @@ func (api *settingsApi) set(c echo.Context) error {
// load request // load request
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
} }
event := &core.SettingsUpdateEvent{ event := &core.SettingsUpdateEvent{
@@ -76,3 +80,49 @@ func (api *settingsApi) set(c echo.Context) error {
return submitErr return submitErr
} }
func (api *settingsApi) testS3(c echo.Context) error {
if !api.app.Settings().S3.Enabled {
return rest.NewBadRequestError("S3 storage is not enabled.", nil)
}
fs, err := api.app.NewFilesystem()
if err != nil {
return rest.NewBadRequestError("Failed to initialize the S3 storage. Raw error: \n"+err.Error(), nil)
}
defer fs.Close()
testFileKey := "pb_test_" + security.RandomString(5) + "/test.txt"
if err := fs.Upload([]byte("test"), testFileKey); err != nil {
return rest.NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil)
}
if err := fs.Delete(testFileKey); err != nil {
return rest.NewBadRequestError("Failed to delete a test file. Raw error: \n"+err.Error(), nil)
}
return c.NoContent(http.StatusNoContent)
}
func (api *settingsApi) testEmail(c echo.Context) error {
form := forms.NewTestEmailSend(api.app)
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
}
// send
if err := form.Submit(); err != nil {
if fErr, ok := err.(validation.Errors); ok {
// form error
return rest.NewBadRequestError("Failed to send the test email.", fErr)
}
// mailer error
return rest.NewBadRequestError("Failed to send the test email. Raw error: \n"+err.Error(), nil)
}
return c.NoContent(http.StatusNoContent)
}
+193
View File
@@ -5,6 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tests"
) )
@@ -51,6 +52,7 @@ func TestSettingsList(t *testing.T) {
`"facebookAuth":{`, `"facebookAuth":{`,
`"githubAuth":{`, `"githubAuth":{`,
`"gitlabAuth":{`, `"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
}, },
@@ -113,6 +115,7 @@ func TestSettingsSet(t *testing.T) {
`"facebookAuth":{`, `"facebookAuth":{`,
`"githubAuth":{`, `"githubAuth":{`,
`"gitlabAuth":{`, `"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
`"appName":"Acme"`, `"appName":"Acme"`,
@@ -165,6 +168,7 @@ func TestSettingsSet(t *testing.T) {
`"facebookAuth":{`, `"facebookAuth":{`,
`"githubAuth":{`, `"githubAuth":{`,
`"gitlabAuth":{`, `"gitlabAuth":{`,
`"discordAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
`"appName":"update_test"`, `"appName":"update_test"`,
@@ -183,3 +187,192 @@ func TestSettingsSet(t *testing.T) {
scenario.Test(t) scenario.Test(t)
} }
} }
func TestSettingsTestS3(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (no s3)",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
// @todo consider creating a test S3 filesystem
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestSettingsTestEmail(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "test@example.com"
}`),
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "test@example.com"
}`),
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (invalid body)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (empty json)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"email":{"code":"validation_required"`,
`"template":{"code":"validation_required"`,
},
},
{
Name: "authorized as admin (verifiation template)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "test@example.com"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
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)
}
if app.TestMailer.LastToAddress.Address != "test@example.com" {
t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastToAddress.Address)
}
if !strings.Contains(app.TestMailer.LastHtmlBody, "Verify") {
t.Fatalf("[verification] Expected to sent a verification email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserVerificationSend": 1,
"OnMailerAfterUserVerificationSend": 1,
},
},
{
Name: "authorized as admin (password reset template)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "password-reset",
"email": "test@example.com"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
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)
}
if app.TestMailer.LastToAddress.Address != "test@example.com" {
t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastToAddress.Address)
}
if !strings.Contains(app.TestMailer.LastHtmlBody, "Reset password") {
t.Fatalf("[password-reset] Expected to sent a password-reset email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserResetPasswordSend": 1,
"OnMailerAfterUserResetPasswordSend": 1,
},
},
{
Name: "authorized as admin (email change)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "email-change",
"email": "test@example.com"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
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)
}
if app.TestMailer.LastToAddress.Address != "test@example.com" {
t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastToAddress.Address)
}
if !strings.Contains(app.TestMailer.LastHtmlBody, "Confirm new email") {
t.Fatalf("[email-change] Expected to sent a confirm new email email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserChangeEmailSend": 1,
"OnMailerAfterUserChangeEmailSend": 1,
},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
+76 -9
View File
@@ -38,6 +38,8 @@ func BindUserApi(app core.App, rg *echo.Group) {
subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id")) subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id"))
subGroup.PATCH("/:id", api.update, RequireAdminAuth()) subGroup.PATCH("/:id", api.update, RequireAdminAuth())
subGroup.DELETE("/:id", api.delete, RequireAdminOrOwnerAuth("id")) 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 { type userApi struct {
@@ -129,7 +131,7 @@ func (api *userApi) authMethods(c echo.Context) error {
} }
state := security.RandomString(30) state := security.RandomString(30)
codeVerifier := security.RandomString(30) codeVerifier := security.RandomString(43)
codeChallenge := security.S256Challenge(codeVerifier) codeChallenge := security.S256Challenge(codeVerifier)
codeChallengeMethod := "S256" codeChallengeMethod := "S256"
result.AuthProviders = append(result.AuthProviders, providerInfo{ result.AuthProviders = append(result.AuthProviders, providerInfo{
@@ -152,7 +154,7 @@ func (api *userApi) authMethods(c echo.Context) error {
func (api *userApi) oauth2Auth(c echo.Context) error { func (api *userApi) oauth2Auth(c echo.Context) error {
form := forms.NewUserOauth2Login(api.app) form := forms.NewUserOauth2Login(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
user, authData, submitErr := form.Submit() user, authData, submitErr := form.Submit()
@@ -170,7 +172,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
form := forms.NewUserEmailLogin(api.app) form := forms.NewUserEmailLogin(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
user, submitErr := form.Submit() user, submitErr := form.Submit()
@@ -184,7 +186,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
func (api *userApi) requestPasswordReset(c echo.Context) error { func (api *userApi) requestPasswordReset(c echo.Context) error {
form := forms.NewUserPasswordResetRequest(api.app) form := forms.NewUserPasswordResetRequest(api.app)
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
} }
if err := form.Validate(); err != nil { if err := form.Validate(); err != nil {
@@ -205,7 +207,7 @@ func (api *userApi) requestPasswordReset(c echo.Context) error {
func (api *userApi) confirmPasswordReset(c echo.Context) error { func (api *userApi) confirmPasswordReset(c echo.Context) error {
form := forms.NewUserPasswordResetConfirm(api.app) form := forms.NewUserPasswordResetConfirm(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
user, submitErr := form.Submit() user, submitErr := form.Submit()
@@ -224,7 +226,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
form := forms.NewUserEmailChangeRequest(api.app, loggedUser) form := forms.NewUserEmailChangeRequest(api.app, loggedUser)
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
} }
if err := form.Submit(); err != nil { if err := form.Submit(); err != nil {
@@ -237,7 +239,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
func (api *userApi) confirmEmailChange(c echo.Context) error { func (api *userApi) confirmEmailChange(c echo.Context) error {
form := forms.NewUserEmailChangeConfirm(api.app) form := forms.NewUserEmailChangeConfirm(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
user, submitErr := form.Submit() user, submitErr := form.Submit()
@@ -251,7 +253,7 @@ func (api *userApi) confirmEmailChange(c echo.Context) error {
func (api *userApi) requestVerification(c echo.Context) error { func (api *userApi) requestVerification(c echo.Context) error {
form := forms.NewUserVerificationRequest(api.app) form := forms.NewUserVerificationRequest(api.app)
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
} }
if err := form.Validate(); err != nil { if err := form.Validate(); err != nil {
@@ -272,7 +274,7 @@ func (api *userApi) requestVerification(c echo.Context) error {
func (api *userApi) confirmVerification(c echo.Context) error { func (api *userApi) confirmVerification(c echo.Context) error {
form := forms.NewUserVerificationConfirm(api.app) form := forms.NewUserVerificationConfirm(api.app)
if readErr := c.Bind(form); readErr != nil { if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
} }
user, submitErr := form.Submit() user, submitErr := form.Submit()
@@ -450,3 +452,68 @@ func (api *userApi) delete(c echo.Context) error {
return handlerErr 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, Method: http.MethodPost,
Url: "/api/users/auth-via-email", Url: "/api/users/auth-via-email",
Body: strings.NewReader(`{"email":"test@example.com","password":"123456"}`), 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 app.Settings().EmailAuth.Enabled = false
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
@@ -159,7 +159,7 @@ func TestUserRequestPasswordReset(t *testing.T) {
Body: strings.NewReader(`{"email":"test@example.com"}`), Body: strings.NewReader(`{"email":"test@example.com"}`),
Delay: 100 * time.Millisecond, Delay: 100 * time.Millisecond,
ExpectedStatus: 204, 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 // simulate recent password request
user, err := app.Dao().FindUserByEmail("test@example.com") user, err := app.Dao().FindUserByEmail("test@example.com")
if err != nil { if err != nil {
@@ -285,7 +285,7 @@ func TestUserRequestVerification(t *testing.T) {
Body: strings.NewReader(`{"email":"test2@example.com"}`), Body: strings.NewReader(`{"email":"test2@example.com"}`),
Delay: 100 * time.Millisecond, Delay: 100 * time.Millisecond,
ExpectedStatus: 204, 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 // simulate recent verification sent
user, err := app.Dao().FindUserByEmail("test2@example.com") user, err := app.Dao().FindUserByEmail("test2@example.com")
if err != nil { if err != nil {
@@ -584,11 +584,12 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":1`, `"page":1`,
`"perPage":30`, `"perPage":30`,
`"totalItems":3`, `"totalItems":4`,
`"items":[{`, `"items":[{`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
`"id":"cx9u0dh2udo8xol"`,
}, },
ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
}, },
@@ -603,8 +604,9 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":2`, `"page":2`,
`"perPage":2`, `"perPage":2`,
`"totalItems":3`, `"totalItems":4`,
`"items":[{`, `"items":[{`,
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
}, },
ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
@@ -630,10 +632,11 @@ func TestUsersList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":1`, `"page":1`,
`"perPage":30`, `"perPage":30`,
`"totalItems":2`, `"totalItems":3`,
`"items":[{`, `"items":[{`,
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
`"id":"cx9u0dh2udo8xol"`,
}, },
ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
}, },
@@ -804,7 +807,7 @@ func TestUserCreate(t *testing.T) {
Method: http.MethodPost, Method: http.MethodPost,
Url: "/api/users", Url: "/api/users",
Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`), 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 app.Settings().EmailAuth.Enabled = false
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
@@ -926,3 +929,185 @@ func TestUserUpdate(t *testing.T) {
scenario.Test(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 ( import (
"crypto/tls" "crypto/tls"
"fmt"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -87,10 +86,11 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
if httpsAddr != "" { if httpsAddr != "" {
schema = "https" schema = "https"
} }
regular := color.New()
bold := color.New(color.Bold).Add(color.FgGreen) bold := color.New(color.Bold).Add(color.FgGreen)
bold.Printf("> Server started at: %s\n", color.CyanString("%s://%s", schema, serverConfig.Addr)) 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)) regular.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(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
} }
var serveErr error var serveErr error
+13 -8
View File
@@ -317,16 +317,21 @@ type App interface {
// authenticated user data and token. // authenticated user data and token.
OnUserAuthRequest() *hook.Hook[*UserAuthEvent] OnUserAuthRequest() *hook.Hook[*UserAuthEvent]
// OnUserBeforeOauth2Register hook is triggered before each User OAuth2 // OnUserListExternalAuths hook is triggered on each API user's external auths list request.
// authentication request (when the client config has enabled new users registration).
// //
// Could be used to additionally validate or modify the new user // Could be used to validate or modify the response before returning it to the client.
// before persisting in the DB. OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent]
OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent]
// OnUserAfterOauth2Register hook is triggered after each successful User // OnUserBeforeUnlinkExternalAuthRequest hook is triggered before each API user's
// OAuth2 authentication sign-up request (right after the new user persistence). // external auth unlink request (after models load and before the actual relation deletion).
OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] //
// 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 // Record API event hooks
+33 -27
View File
@@ -85,18 +85,19 @@ type BaseApp struct {
onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent] onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent]
onAdminAuthRequest *hook.Hook[*AdminAuthEvent] onAdminAuthRequest *hook.Hook[*AdminAuthEvent]
// user api event hooks // user api event hooks
onUsersListRequest *hook.Hook[*UsersListEvent] onUsersListRequest *hook.Hook[*UsersListEvent]
onUserViewRequest *hook.Hook[*UserViewEvent] onUserViewRequest *hook.Hook[*UserViewEvent]
onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent] onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent]
onUserAfterCreateRequest *hook.Hook[*UserCreateEvent] onUserAfterCreateRequest *hook.Hook[*UserCreateEvent]
onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent] onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent] onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent]
onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent] onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent] onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent]
onUserAuthRequest *hook.Hook[*UserAuthEvent] onUserAuthRequest *hook.Hook[*UserAuthEvent]
onUserBeforeOauth2Register *hook.Hook[*UserOauth2RegisterEvent] onUserListExternalAuths *hook.Hook[*UserListExternalAuthsEvent]
onUserAfterOauth2Register *hook.Hook[*UserOauth2RegisterEvent] onUserBeforeUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
onUserAfterUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
// record api event hooks // record api event hooks
onRecordsListRequest *hook.Hook[*RecordsListEvent] onRecordsListRequest *hook.Hook[*RecordsListEvent]
@@ -180,17 +181,18 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{}, onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{},
// user API event hooks // user API event hooks
onUsersListRequest: &hook.Hook[*UsersListEvent]{}, onUsersListRequest: &hook.Hook[*UsersListEvent]{},
onUserViewRequest: &hook.Hook[*UserViewEvent]{}, onUserViewRequest: &hook.Hook[*UserViewEvent]{},
onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{}, onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{}, onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{},
onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{}, onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{}, onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{}, onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{}, onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
onUserAuthRequest: &hook.Hook[*UserAuthEvent]{}, onUserAuthRequest: &hook.Hook[*UserAuthEvent]{},
onUserBeforeOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{}, onUserListExternalAuths: &hook.Hook[*UserListExternalAuthsEvent]{},
onUserAfterOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{}, onUserBeforeUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
onUserAfterUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
// record API event hooks // record API event hooks
onRecordsListRequest: &hook.Hook[*RecordsListEvent]{}, onRecordsListRequest: &hook.Hook[*RecordsListEvent]{},
@@ -611,12 +613,16 @@ func (app *BaseApp) OnUserAuthRequest() *hook.Hook[*UserAuthEvent] {
return app.onUserAuthRequest return app.onUserAuthRequest
} }
func (app *BaseApp) OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] { func (app *BaseApp) OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent] {
return app.onUserBeforeOauth2Register return app.onUserListExternalAuths
} }
func (app *BaseApp) OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] { func (app *BaseApp) OnUserBeforeUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
return app.onUserAfterOauth2Register return app.onUserBeforeUnlinkExternalAuthRequest
}
func (app *BaseApp) OnUserAfterUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
return app.onUserAfterUnlinkExternalAuthRequest
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
+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) 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 { if app.onUserListExternalAuths != app.OnUserListExternalAuths() || app.OnUserListExternalAuths() == nil {
t.Fatalf("Getter app.OnUserBeforeOauth2Register does not match or nil (%v vs %v)", app.OnUserBeforeOauth2Register(), app.onUserBeforeOauth2Register) 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 { if app.onUserBeforeUnlinkExternalAuthRequest != app.OnUserBeforeUnlinkExternalAuthRequest() || app.OnUserBeforeUnlinkExternalAuthRequest() == nil {
t.Fatalf("Getter app.OnUserAfterOauth2Register does not match or nil (%v vs %v)", app.OnUserAfterOauth2Register(), app.onUserAfterOauth2Register) 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 { 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/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/subscriptions" "github.com/pocketbase/pocketbase/tools/subscriptions"
@@ -180,10 +179,16 @@ type UserAuthEvent struct {
Meta any Meta any
} }
type UserOauth2RegisterEvent struct { type UserListExternalAuthsEvent struct {
HttpContext echo.Context HttpContext echo.Context
User *models.User User *models.User
AuthData *auth.AuthUser ExternalAuths []*models.ExternalAuth
}
type UserUnlinkExternalAuthEvent struct {
HttpContext echo.Context
User *models.User
ExternalAuth *models.ExternalAuth
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
+154 -80
View File
@@ -10,48 +10,49 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is" "github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/tools/auth" "github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/tools/security" "github.com/pocketbase/pocketbase/tools/security"
) )
// Common settings placeholder tokens
const (
EmailPlaceholderAppUrl string = "%APP_URL%"
EmailPlaceholderToken string = "%TOKEN%"
)
// Settings defines common app configuration options. // Settings defines common app configuration options.
type Settings struct { type Settings struct {
mux sync.RWMutex mux sync.RWMutex
Meta MetaConfig `form:"meta" json:"meta"` Meta MetaConfig `form:"meta" json:"meta"`
Logs LogsConfig `form:"logs" json:"logs"` Logs LogsConfig `form:"logs" json:"logs"`
Smtp SmtpConfig `form:"smtp" json:"smtp"` Smtp SmtpConfig `form:"smtp" json:"smtp"`
S3 S3Config `form:"s3" json:"s3"` S3 S3Config `form:"s3" json:"s3"`
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"` AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"` AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"` UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"`
UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"` UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"`
UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"` UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"`
EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"` UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"`
GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"` EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"` GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"` 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. // NewSettings creates and returns a new default Settings instance.
func NewSettings() *Settings { func NewSettings() *Settings {
return &Settings{ return &Settings{
Meta: MetaConfig{ Meta: MetaConfig{
AppName: "Acme", AppName: "Acme",
AppUrl: "http://localhost:8090", AppUrl: "http://localhost:8090",
SenderName: "Support", HideControls: false,
SenderAddress: "support@example.com", SenderName: "Support",
UserVerificationUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-verification/" + EmailPlaceholderToken, SenderAddress: "support@example.com",
UserResetPasswordUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-password-reset/" + EmailPlaceholderToken, VerificationTemplate: defaultVerificationTemplate,
UserConfirmEmailChangeUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-email-change/" + EmailPlaceholderToken, ResetPasswordTemplate: defaultResetPasswordTemplate,
ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
}, },
Logs: LogsConfig{ Logs: LogsConfig{
MaxDays: 7, MaxDays: 7,
}, },
@@ -107,6 +108,14 @@ func NewSettings() *Settings {
Enabled: false, Enabled: false,
AllowRegistrations: true, AllowRegistrations: true,
}, },
DiscordAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
TwitterAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
} }
} }
@@ -131,6 +140,8 @@ func (s *Settings) Validate() error {
validation.Field(&s.FacebookAuth), validation.Field(&s.FacebookAuth),
validation.Field(&s.GithubAuth), validation.Field(&s.GithubAuth),
validation.Field(&s.GitlabAuth), validation.Field(&s.GitlabAuth),
validation.Field(&s.DiscordAuth),
validation.Field(&s.TwitterAuth),
) )
} }
@@ -179,6 +190,8 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.FacebookAuth.ClientSecret, &clone.FacebookAuth.ClientSecret,
&clone.GithubAuth.ClientSecret, &clone.GithubAuth.ClientSecret,
&clone.GitlabAuth.ClientSecret, &clone.GitlabAuth.ClientSecret,
&clone.DiscordAuth.ClientSecret,
&clone.TwitterAuth.ClientSecret,
} }
// mask all sensitive fields // mask all sensitive fields
@@ -194,11 +207,16 @@ func (s *Settings) RedactClone() (*Settings, error) {
// NamedAuthProviderConfigs returns a map with all registered OAuth2 // NamedAuthProviderConfigs returns a map with all registered OAuth2
// provider configurations (indexed by their name identifier). // provider configurations (indexed by their name identifier).
func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig { func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
s.mux.RLock()
defer s.mux.RUnlock()
return map[string]AuthProviderConfig{ return map[string]AuthProviderConfig{
auth.NameGoogle: s.GoogleAuth, auth.NameGoogle: s.GoogleAuth,
auth.NameFacebook: s.FacebookAuth, auth.NameFacebook: s.FacebookAuth,
auth.NameGithub: s.GithubAuth, auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth, auth.NameGitlab: s.GitlabAuth,
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
} }
} }
@@ -213,7 +231,7 @@ type TokenConfig struct {
func (c TokenConfig) Validate() error { func (c TokenConfig) Validate() error {
return validation.ValidateStruct(&c, return validation.ValidateStruct(&c,
validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)), 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)),
) )
} }
@@ -267,13 +285,14 @@ func (c S3Config) Validate() error {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
type MetaConfig struct { type MetaConfig struct {
AppName string `form:"appName" json:"appName"` AppName string `form:"appName" json:"appName"`
AppUrl string `form:"appUrl" json:"appUrl"` AppUrl string `form:"appUrl" json:"appUrl"`
SenderName string `form:"senderName" json:"senderName"` HideControls bool `form:"hideControls" json:"hideControls"`
SenderAddress string `form:"senderAddress" json:"senderAddress"` SenderName string `form:"senderName" json:"senderName"`
UserVerificationUrl string `form:"userVerificationUrl" json:"userVerificationUrl"` SenderAddress string `form:"senderAddress" json:"senderAddress"`
UserResetPasswordUrl string `form:"userResetPasswordUrl" json:"userResetPasswordUrl"` VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
UserConfirmEmailChangeUrl string `form:"userConfirmEmailChangeUrl" json:"userConfirmEmailChangeUrl"` ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
} }
// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. // Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
@@ -282,35 +301,46 @@ func (c MetaConfig) Validate() error {
validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)), validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
validation.Field(&c.AppUrl, validation.Required, is.URL), validation.Field(&c.AppUrl, validation.Required, is.URL),
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)), 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),
)
}
type EmailTemplate struct {
Body string `form:"body" json:"body"`
Subject string `form:"subject" json:"subject"`
ActionUrl string `form:"actionUrl" json:"actionUrl"`
}
// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface.
func (t EmailTemplate) Validate() error {
return validation.ValidateStruct(&t,
validation.Field(&t.Subject, validation.Required),
validation.Field( validation.Field(
&c.UserVerificationUrl, &t.Body,
validation.Required, validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)), validation.By(checkPlaceholderParams(EmailPlaceholderActionUrl)),
), ),
validation.Field( validation.Field(
&c.UserResetPasswordUrl, &t.ActionUrl,
validation.Required, validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)), validation.By(checkPlaceholderParams(EmailPlaceholderToken)),
),
validation.Field(
&c.UserConfirmEmailChangeUrl,
validation.Required,
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
), ),
) )
} }
func (c *MetaConfig) checkPlaceholders(params ...string) validation.RuleFunc { func checkPlaceholderParams(params ...string) validation.RuleFunc {
return func(value any) error { return func(value any) error {
v, _ := value.(string) v, _ := value.(string)
if v == "" {
return nil // nothing to check
}
for _, param := range params { for _, param := range params {
if !strings.Contains(v, param) { if !strings.Contains(v, param) {
return validation.NewError("validation_missing_required_param", fmt.Sprintf("Missing required parameter %q", param)) return validation.NewError(
"validation_missing_required_param",
fmt.Sprintf("Missing required parameter %q", param),
)
} }
} }
@@ -318,6 +348,50 @@ func (c *MetaConfig) checkPlaceholders(params ...string) validation.RuleFunc {
} }
} }
// Resolve replaces the placeholder parameters in the current email
// template and returns its components as ready-to-use strings.
func (t EmailTemplate) Resolve(
appName string,
appUrl,
token string,
) (subject, body, actionUrl string) {
// replace action url placeholder params (if any)
actionUrlParams := map[string]string{
EmailPlaceholderAppName: appName,
EmailPlaceholderAppUrl: appUrl,
EmailPlaceholderToken: token,
}
actionUrl = t.ActionUrl
for k, v := range actionUrlParams {
actionUrl = strings.ReplaceAll(actionUrl, k, v)
}
actionUrl, _ = rest.NormalizeUrl(actionUrl)
// replace body placeholder params (if any)
bodyParams := map[string]string{
EmailPlaceholderAppName: appName,
EmailPlaceholderAppUrl: appUrl,
EmailPlaceholderToken: token,
EmailPlaceholderActionUrl: actionUrl,
}
body = t.Body
for k, v := range bodyParams {
body = strings.ReplaceAll(body, k, v)
}
// replace subject placeholder params (if any)
subjectParams := map[string]string{
EmailPlaceholderAppName: appName,
EmailPlaceholderAppUrl: appUrl,
}
subject = t.Subject
for k, v := range subjectParams {
subject = strings.ReplaceAll(subject, k, v)
}
return subject, body, actionUrl
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
type LogsConfig struct { type LogsConfig struct {
@@ -333,6 +407,35 @@ func (c LogsConfig) Validate() error {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
type EmailAuthConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
}
// Validate makes `EmailAuthConfig` validatable by implementing [validation.Validatable] interface.
func (c EmailAuthConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(
&c.ExceptDomains,
validation.When(len(c.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.OnlyDomains,
validation.When(len(c.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.MinPasswordLength,
validation.When(c.Enabled, validation.Required),
validation.Min(5),
validation.Max(100),
),
)
}
// -------------------------------------------------------------------
type AuthProviderConfig struct { type AuthProviderConfig struct {
Enabled bool `form:"enabled" json:"enabled"` Enabled bool `form:"enabled" json:"enabled"`
AllowRegistrations bool `form:"allowRegistrations" json:"allowRegistrations"` AllowRegistrations bool `form:"allowRegistrations" json:"allowRegistrations"`
@@ -382,32 +485,3 @@ func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
return nil return nil
} }
// -------------------------------------------------------------------
type EmailAuthConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
}
// Validate makes `EmailAuthConfig` validatable by implementing [validation.Validatable] interface.
func (c EmailAuthConfig) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(
&c.ExceptDomains,
validation.When(len(c.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.OnlyDomains,
validation.When(len(c.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
),
validation.Field(
&c.MinPasswordLength,
validation.When(c.Enabled, validation.Required),
validation.Min(5),
validation.Max(100),
),
)
}
+54
View File
@@ -0,0 +1,54 @@
package core
// Common settings placeholder tokens
const (
EmailPlaceholderAppName string = "{APP_NAME}"
EmailPlaceholderAppUrl string = "{APP_URL}"
EmailPlaceholderToken string = "{TOKEN}"
EmailPlaceholderActionUrl string = "{ACTION_URL}"
)
var defaultVerificationTemplate = EmailTemplate{
Subject: "Verify your " + EmailPlaceholderAppName + " email",
Body: `<p>Hello,</p>
<p>Thank you for joining us at ` + EmailPlaceholderAppName + `.</p>
<p>Click on the button below to verify your email address.</p>
<p>
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Verify</a>
</p>
<p>
Thanks,<br/>
` + EmailPlaceholderAppName + ` team
</p>`,
ActionUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-verification/" + EmailPlaceholderToken,
}
var defaultResetPasswordTemplate = EmailTemplate{
Subject: "Reset your " + EmailPlaceholderAppName + " password",
Body: `<p>Hello,</p>
<p>Click on the button below to reset your password.</p>
<p>
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Reset password</a>
</p>
<p><i>If you didn't ask to reset your password, you can ignore this email.</i></p>
<p>
Thanks,<br/>
` + EmailPlaceholderAppName + ` team
</p>`,
ActionUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-password-reset/" + EmailPlaceholderToken,
}
var defaultConfirmEmailChangeTemplate = EmailTemplate{
Subject: "Confirm your " + EmailPlaceholderAppName + " new email address",
Body: `<p>Hello,</p>
<p>Click on the button below to confirm your new email address.</p>
<p>
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Confirm new email</a>
</p>
<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>
<p>
Thanks,<br/>
` + EmailPlaceholderAppName + ` team
</p>`,
ActionUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-email-change/" + EmailPlaceholderToken,
}
+190 -25
View File
@@ -2,9 +2,11 @@ package core_test
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"testing" "testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/auth" "github.com/pocketbase/pocketbase/tools/auth"
) )
@@ -35,6 +37,10 @@ func TestSettingsValidate(t *testing.T) {
s.GithubAuth.ClientId = "" s.GithubAuth.ClientId = ""
s.GitlabAuth.Enabled = true s.GitlabAuth.Enabled = true
s.GitlabAuth.ClientId = "" 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. // check if Validate() is triggering the members validate methods.
err := s.Validate() err := s.Validate()
@@ -58,6 +64,7 @@ func TestSettingsValidate(t *testing.T) {
`"facebookAuth":{`, `"facebookAuth":{`,
`"githubAuth":{`, `"githubAuth":{`,
`"gitlabAuth":{`, `"gitlabAuth":{`,
`"discordAuth":{`,
} }
errBytes, _ := json.Marshal(err) errBytes, _ := json.Marshal(err)
@@ -96,6 +103,10 @@ func TestSettingsMerge(t *testing.T) {
s2.GithubAuth.ClientId = "github_test" s2.GithubAuth.ClientId = "github_test"
s2.GitlabAuth.Enabled = true s2.GitlabAuth.Enabled = true
s2.GitlabAuth.ClientId = "gitlab_test" 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 { if err := s1.Merge(s2); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -161,6 +172,8 @@ func TestSettingsRedactClone(t *testing.T) {
s1.FacebookAuth.ClientSecret = "test123" s1.FacebookAuth.ClientSecret = "test123"
s1.GithubAuth.ClientSecret = "test123" s1.GithubAuth.ClientSecret = "test123"
s1.GitlabAuth.ClientSecret = "test123" s1.GitlabAuth.ClientSecret = "test123"
s1.DiscordAuth.ClientSecret = "test123"
s1.TwitterAuth.ClientSecret = "test123"
s2, err := s1.RedactClone() s2, err := s1.RedactClone()
if err != nil { if err != nil {
@@ -172,7 +185,7 @@ func TestSettingsRedactClone(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","senderName":"Support","senderAddress":"support@example.com","userVerificationUrl":"%APP_URL%/_/#/users/confirm-verification/%TOKEN%","userResetPasswordUrl":"%APP_URL%/_/#/users/confirm-password-reset/%TOKEN%","userConfirmEmailChangeUrl":"%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 { if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected %v, got \n%v", expected, encodedStr) t.Fatalf("Expected %v, got \n%v", expected, encodedStr)
@@ -187,6 +200,8 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.GithubAuth.ClientId = "github_test" s.GithubAuth.ClientId = "github_test"
s.GitlabAuth.ClientId = "gitlab_test" s.GitlabAuth.ClientId = "gitlab_test"
s.GitlabAuth.Enabled = true s.GitlabAuth.Enabled = true
s.DiscordAuth.ClientId = "discord_test"
s.TwitterAuth.ClientId = "twitter_test"
result := s.NamedAuthProviderConfigs() result := s.NamedAuthProviderConfigs()
@@ -195,7 +210,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
t.Fatal(err) 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 { if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected the same serialization, got %v", encodedStr) t.Fatalf("Expected the same serialization, got %v", encodedStr)
@@ -215,15 +230,23 @@ func TestTokenConfigValidate(t *testing.T) {
// invalid data // invalid data
{ {
core.TokenConfig{ core.TokenConfig{
Secret: "test", Secret: strings.Repeat("a", 5),
Duration: 4, Duration: 4,
}, },
true, true,
}, },
// valid secret but invalid duration
{
core.TokenConfig{
Secret: strings.Repeat("a", 30),
Duration: 63072000 + 1,
},
true,
},
// valid data // valid data
{ {
core.TokenConfig{ core.TokenConfig{
Secret: "testtesttesttesttesttesttestte", Secret: strings.Repeat("a", 30),
Duration: 100, Duration: 100,
}, },
false, false,
@@ -355,6 +378,24 @@ func TestS3ConfigValidate(t *testing.T) {
} }
func TestMetaConfigValidate(t *testing.T) { func TestMetaConfigValidate(t *testing.T) {
invalidTemplate := core.EmailTemplate{
Subject: "test",
ActionUrl: "test",
Body: "test",
}
noPlaceholdersTemplate := core.EmailTemplate{
Subject: "test",
ActionUrl: "http://example.com",
Body: "test",
}
withPlaceholdersTemplate := core.EmailTemplate{
Subject: "test",
ActionUrl: "http://example.com" + core.EmailPlaceholderToken,
Body: "test" + core.EmailPlaceholderActionUrl,
}
scenarios := []struct { scenarios := []struct {
config core.MetaConfig config core.MetaConfig
expectError bool expectError bool
@@ -367,39 +408,39 @@ func TestMetaConfigValidate(t *testing.T) {
// invalid data // invalid data
{ {
core.MetaConfig{ core.MetaConfig{
AppName: strings.Repeat("a", 300), AppName: strings.Repeat("a", 300),
AppUrl: "test", AppUrl: "test",
SenderName: strings.Repeat("a", 300), SenderName: strings.Repeat("a", 300),
SenderAddress: "invalid_email", SenderAddress: "invalid_email",
UserVerificationUrl: "test", VerificationTemplate: invalidTemplate,
UserResetPasswordUrl: "test", ResetPasswordTemplate: invalidTemplate,
UserConfirmEmailChangeUrl: "test", ConfirmEmailChangeTemplate: invalidTemplate,
}, },
true, true,
}, },
// invalid data (missing required placeholders) // invalid data (missing required placeholders)
{ {
core.MetaConfig{ core.MetaConfig{
AppName: "test", AppName: "test",
AppUrl: "https://example.com", AppUrl: "https://example.com",
SenderName: "test", SenderName: "test",
SenderAddress: "test@example.com", SenderAddress: "test@example.com",
UserVerificationUrl: "https://example.com", VerificationTemplate: noPlaceholdersTemplate,
UserResetPasswordUrl: "https://example.com", ResetPasswordTemplate: noPlaceholdersTemplate,
UserConfirmEmailChangeUrl: "https://example.com", ConfirmEmailChangeTemplate: noPlaceholdersTemplate,
}, },
true, true,
}, },
// valid data // valid data
{ {
core.MetaConfig{ core.MetaConfig{
AppName: "test", AppName: "test",
AppUrl: "https://example.com", AppUrl: "https://example.com",
SenderName: "test", SenderName: "test",
SenderAddress: "test@example.com", SenderAddress: "test@example.com",
UserVerificationUrl: "https://example.com/" + core.EmailPlaceholderToken, VerificationTemplate: withPlaceholdersTemplate,
UserResetPasswordUrl: "https://example.com/" + core.EmailPlaceholderToken, ResetPasswordTemplate: withPlaceholdersTemplate,
UserConfirmEmailChangeUrl: "https://example.com/" + core.EmailPlaceholderToken, ConfirmEmailChangeTemplate: withPlaceholdersTemplate,
}, },
false, false,
}, },
@@ -418,6 +459,130 @@ func TestMetaConfigValidate(t *testing.T) {
} }
} }
func TestEmailTemplateValidate(t *testing.T) {
scenarios := []struct {
emailTemplate core.EmailTemplate
expectedErrors []string
}{
// require values
{
core.EmailTemplate{},
[]string{"subject", "actionUrl", "body"},
},
// missing placeholders
{
core.EmailTemplate{
Subject: "test",
ActionUrl: "test",
Body: "test",
},
[]string{"actionUrl", "body"},
},
// valid data
{
core.EmailTemplate{
Subject: "test",
ActionUrl: "test" + core.EmailPlaceholderToken,
Body: "test" + core.EmailPlaceholderActionUrl,
},
[]string{},
},
}
for i, s := range scenarios {
result := s.emailTemplate.Validate()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Errorf("(%d) Failed to parse errors %v", i, result)
continue
}
// check errors
if len(errs) > len(s.expectedErrors) {
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
}
for _, k := range s.expectedErrors {
if _, ok := errs[k]; !ok {
t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs)
}
}
}
}
func TestEmailTemplateResolve(t *testing.T) {
allPlaceholders := core.EmailPlaceholderActionUrl + core.EmailPlaceholderToken + core.EmailPlaceholderAppName + core.EmailPlaceholderAppUrl
scenarios := []struct {
emailTemplate core.EmailTemplate
expectedSubject string
expectedBody string
expectedActionUrl string
}{
// no placeholders
{
emailTemplate: core.EmailTemplate{
Subject: "subject:",
Body: "body:",
ActionUrl: "/actionUrl////",
},
expectedSubject: "subject:",
expectedActionUrl: "/actionUrl/",
expectedBody: "body:",
},
// with placeholders
{
emailTemplate: core.EmailTemplate{
ActionUrl: "/actionUrl////" + allPlaceholders,
Subject: "subject:" + allPlaceholders,
Body: "body:" + allPlaceholders,
},
expectedActionUrl: fmt.Sprintf(
"/actionUrl/%%7BACTION_URL%%7D%s%s%s",
"token_test",
"name_test",
"url_test",
),
expectedSubject: fmt.Sprintf(
"subject:%s%s%s%s",
core.EmailPlaceholderActionUrl,
core.EmailPlaceholderToken,
"name_test",
"url_test",
),
expectedBody: fmt.Sprintf(
"body:%s%s%s%s",
fmt.Sprintf(
"/actionUrl/%%7BACTION_URL%%7D%s%s%s",
"token_test",
"name_test",
"url_test",
),
"token_test",
"name_test",
"url_test",
),
},
}
for i, s := range scenarios {
subject, body, actionUrl := s.emailTemplate.Resolve("name_test", "url_test", "token_test")
if s.expectedSubject != subject {
t.Errorf("(%d) Expected subject %q got %q", i, s.expectedSubject, subject)
}
if s.expectedBody != body {
t.Errorf("(%d) Expected body \n%v got \n%v", i, s.expectedBody, body)
}
if s.expectedActionUrl != actionUrl {
t.Errorf("(%d) Expected actionUrl \n%v got \n%v", i, s.expectedActionUrl, actionUrl)
}
}
}
func TestLogsConfigValidate(t *testing.T) { func TestLogsConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.LogsConfig config core.LogsConfig
+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)
}
}
+2 -1
View File
@@ -94,7 +94,7 @@ func (dao *Dao) FindUserById(id string) (*models.User, error) {
return model, nil 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 // This method also auto loads the related user profile record
// into the found model. // into the found model.
@@ -102,6 +102,7 @@ func (dao *Dao) FindUserByEmail(email string) (*models.User, error) {
model := &models.User{} model := &models.User{}
err := dao.UserQuery(). err := dao.UserQuery().
AndWhere(dbx.Not(dbx.HashExp{"email": ""})).
AndWhere(dbx.HashExp{"email": email}). AndWhere(dbx.HashExp{"email": email}).
Limit(1). Limit(1).
One(model) One(model)
+1
View File
@@ -110,6 +110,7 @@ func TestFindUserByEmail(t *testing.T) {
email string email string
expectError bool expectError bool
}{ }{
{"", true},
{"invalid", true}, {"invalid", true},
{"missing@example.com", true}, {"missing@example.com", true},
{"test@example.com", false}, {"test@example.com", false},
+23 -3
View File
@@ -2,20 +2,40 @@ package main
import ( import (
"log" "log"
"os"
"path/filepath"
"strings"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core" "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() { func main() {
app := pocketbase.New() 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 { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// serves static files from the provided public dir (if exists) // serves static files from the provided public dir (if exists)
subFs := echo.MustSubFS(e.Router.Filesystem, "pb_public") e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDirFlag), false))
e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false))
return nil return nil
}) })
+7 -7
View File
@@ -22,15 +22,15 @@ type AdminLogin struct {
// //
// NB! App is a required struct member. // NB! App is a required struct member.
type AdminLoginConfig struct { type AdminLoginConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewAdminLogin creates a new [AdminLogin] form with initializer // NewAdminLogin creates a new [AdminLogin] form with initializer
// config created from the provided [core.App] instance. // config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewAdminLogin(app core.App) *AdminLogin {
return NewAdminLoginWithConfig(AdminLoginConfig{ return NewAdminLoginWithConfig(AdminLoginConfig{
App: app, App: app,
@@ -46,8 +46,8 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin {
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -56,7 +56,7 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin {
// Validate makes the form validatable by implementing [validation.Validatable] interface. // Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *AdminLogin) Validate() error { func (form *AdminLogin) Validate() error {
return validation.ValidateStruct(form, return validation.ValidateStruct(form,
validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.Email), validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.EmailFormat),
validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), validation.Field(&form.Password, validation.Required, validation.Length(1, 255)),
) )
} }
@@ -68,7 +68,7 @@ func (form *AdminLogin) Submit() (*models.Admin, error) {
return nil, err return nil, err
} }
admin, err := form.config.TxDao.FindAdminByEmail(form.Email) admin, err := form.config.Dao.FindAdminByEmail(form.Email)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+8 -8
View File
@@ -21,15 +21,15 @@ type AdminPasswordResetConfirm struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type AdminPasswordResetConfirmConfig struct { type AdminPasswordResetConfirmConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm] // NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm]
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm {
return NewAdminPasswordResetConfirmWithConfig(AdminPasswordResetConfirmConfig{ return NewAdminPasswordResetConfirmWithConfig(AdminPasswordResetConfirmConfig{
App: app, App: app,
@@ -45,8 +45,8 @@ func NewAdminPasswordResetConfirmWithConfig(config AdminPasswordResetConfirmConf
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -67,7 +67,7 @@ func (form *AdminPasswordResetConfirm) checkToken(value any) error {
return nil // nothing to check return nil // nothing to check
} }
admin, err := form.config.TxDao.FindAdminByToken( admin, err := form.config.Dao.FindAdminByToken(
v, v,
form.config.App.Settings().AdminPasswordResetToken.Secret, form.config.App.Settings().AdminPasswordResetToken.Secret,
) )
@@ -85,7 +85,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
return nil, err return nil, err
} }
admin, err := form.config.TxDao.FindAdminByToken( admin, err := form.config.Dao.FindAdminByToken(
form.Token, form.Token,
form.config.App.Settings().AdminPasswordResetToken.Secret, form.config.App.Settings().AdminPasswordResetToken.Secret,
) )
@@ -97,7 +97,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
return nil, err return nil, err
} }
if err := form.config.TxDao.SaveAdmin(admin); err != nil { if err := form.config.Dao.SaveAdmin(admin); err != nil {
return nil, err return nil, err
} }
+7 -7
View File
@@ -24,7 +24,7 @@ type AdminPasswordResetRequest struct {
// NB! App is required struct member. // NB! App is required struct member.
type AdminPasswordResetRequestConfig struct { type AdminPasswordResetRequestConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
ResendThreshold float64 // in seconds ResendThreshold float64 // in seconds
} }
@@ -32,7 +32,7 @@ type AdminPasswordResetRequestConfig struct {
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest {
return NewAdminPasswordResetRequestWithConfig(AdminPasswordResetRequestConfig{ return NewAdminPasswordResetRequestWithConfig(AdminPasswordResetRequestConfig{
App: app, App: app,
@@ -49,8 +49,8 @@ func NewAdminPasswordResetRequestWithConfig(config AdminPasswordResetRequestConf
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -65,7 +65,7 @@ func (form *AdminPasswordResetRequest) Validate() error {
&form.Email, &form.Email,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
), ),
) )
} }
@@ -77,7 +77,7 @@ func (form *AdminPasswordResetRequest) Submit() error {
return err return err
} }
admin, err := form.config.TxDao.FindAdminByEmail(form.Email) admin, err := form.config.Dao.FindAdminByEmail(form.Email)
if err != nil { if err != nil {
return err return err
} }
@@ -95,5 +95,5 @@ func (form *AdminPasswordResetRequest) Submit() error {
// update last sent timestamp // update last sent timestamp
admin.LastResetSentAt = types.NowDateTime() 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. // NB! App is a required struct member.
type AdminUpsertConfig struct { type AdminUpsertConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewAdminUpsert creates a new [AdminUpsert] form with initializer // 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{}`). // (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 // 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 { func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert {
return NewAdminUpsertWithConfig(AdminUpsertConfig{ return NewAdminUpsertWithConfig(AdminUpsertConfig{
App: app, App: app,
@@ -54,8 +54,8 @@ func NewAdminUpsertWithConfig(config AdminUpsertConfig, admin *models.Admin) *Ad
panic("Invalid initializer config or nil upsert model.") panic("Invalid initializer config or nil upsert model.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
// load defaults // load defaults
@@ -86,7 +86,7 @@ func (form *AdminUpsert) Validate() error {
&form.Email, &form.Email,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
validation.By(form.checkUniqueEmail), validation.By(form.checkUniqueEmail),
), ),
validation.Field( validation.Field(
@@ -105,7 +105,7 @@ func (form *AdminUpsert) Validate() error {
func (form *AdminUpsert) checkUniqueEmail(value any) error { func (form *AdminUpsert) checkUniqueEmail(value any) error {
v, _ := value.(string) v, _ := value.(string)
if form.config.TxDao.IsAdminEmailUnique(v, form.admin.Id) { if form.config.Dao.IsAdminEmailUnique(v, form.admin.Id) {
return nil return nil
} }
@@ -135,6 +135,6 @@ func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error {
} }
return runInterceptors(func() error { return runInterceptors(func() error {
return form.config.TxDao.SaveAdmin(form.admin) return form.config.Dao.SaveAdmin(form.admin)
}, interceptors...) }, interceptors...)
} }
+10 -10
View File
@@ -36,8 +36,8 @@ type CollectionUpsert struct {
// //
// NB! App is a required struct member. // NB! App is a required struct member.
type CollectionUpsertConfig struct { type CollectionUpsertConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer // 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{}`). // (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 // 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 { func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert {
return NewCollectionUpsertWithConfig(CollectionUpsertConfig{ return NewCollectionUpsertWithConfig(CollectionUpsertConfig{
App: app, App: app,
@@ -65,8 +65,8 @@ func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *mo
panic("Invalid initializer config or nil upsert model.") panic("Invalid initializer config or nil upsert model.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
// load defaults // load defaults
@@ -130,11 +130,11 @@ func (form *CollectionUpsert) Validate() error {
func (form *CollectionUpsert) checkUniqueName(value any) error { func (form *CollectionUpsert) checkUniqueName(value any) error {
v, _ := value.(string) 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).") 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.") 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 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( return validation.Errors{fmt.Sprint(i): validation.NewError(
"validation_field_invalid_relation", "validation_field_invalid_relation",
"The relation collection doesn't exist.", "The relation collection doesn't exist.",
@@ -228,7 +228,7 @@ func (form *CollectionUpsert) checkRule(value any) error {
} }
dummy := &models.Collection{Schema: form.Schema} 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) _, err := search.FilterData(*v).BuildExpr(r)
if err != nil { if err != nil {
@@ -273,6 +273,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
form.collection.DeleteRule = form.DeleteRule form.collection.DeleteRule = form.DeleteRule
return runInterceptors(func() error { return runInterceptors(func() error {
return form.config.TxDao.SaveCollection(form.collection) return form.config.Dao.SaveCollection(form.collection)
}, interceptors...) }, interceptors...)
} }
+8 -8
View File
@@ -24,15 +24,15 @@ type CollectionsImport struct {
// //
// NB! App is a required struct member. // NB! App is a required struct member.
type CollectionsImportConfig struct { type CollectionsImportConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewCollectionsImport creates a new [CollectionsImport] form with // NewCollectionsImport creates a new [CollectionsImport] form with
// initializer config created from the provided [core.App] instance. // initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewCollectionsImport(app core.App) *CollectionsImport {
return NewCollectionsImportWithConfig(CollectionsImportConfig{ return NewCollectionsImportWithConfig(CollectionsImportConfig{
App: app, App: app,
@@ -48,8 +48,8 @@ func NewCollectionsImportWithConfig(config CollectionsImportConfig) *Collections
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -79,7 +79,7 @@ func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc) error {
} }
return runInterceptors(func() 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( importErr := txDao.ImportCollections(
form.Collections, form.Collections,
form.DeleteMissing, form.DeleteMissing,
@@ -122,8 +122,8 @@ func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, map
} }
upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{ upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{
App: form.config.App, App: form.config.App,
TxDao: txDao, Dao: txDao,
}, upsertModel) }, upsertModel)
// load form fields with the refreshed collection state // load form fields with the refreshed collection state
+1 -1
View File
@@ -4,7 +4,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
) )
// RealtimeSubscribe specifies a RealtimeSubscribe request form. // RealtimeSubscribe is a realtime subscriptions request form.
type RealtimeSubscribe struct { type RealtimeSubscribe struct {
ClientId string `form:"clientId" json:"clientId"` ClientId string `form:"clientId" json:"clientId"`
Subscriptions []string `form:"subscriptions" json:"subscriptions"` Subscriptions []string `form:"subscriptions" json:"subscriptions"`
+34 -22
View File
@@ -37,8 +37,8 @@ type RecordUpsert struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type RecordUpsertConfig struct { type RecordUpsertConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewRecordUpsert creates a new [RecordUpsert] form with initializer // 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)`). // (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 // 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 { func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
return NewRecordUpsertWithConfig(RecordUpsertConfig{ return NewRecordUpsertWithConfig(RecordUpsertConfig{
App: app, App: app,
@@ -68,8 +68,8 @@ func NewRecordUpsertWithConfig(config RecordUpsertConfig, record *models.Record)
panic("Invalid initializer config or nil upsert model.") panic("Invalid initializer config or nil upsert model.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
form.Id = record.Id form.Id = record.Id
@@ -194,21 +194,26 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
options, _ := field.Options.(*schema.FileOptions) options, _ := field.Options.(*schema.FileOptions)
oldNames := list.ToUniqueStringSlice(form.Data[key]) oldNames := list.ToUniqueStringSlice(form.Data[key])
// delete previously uploaded file(s) // -----------------------------------------------------------
if options.MaxSelect == 1 { // Delete previously uploaded file(s)
// search for unset zero indexed key as a fallback // -----------------------------------------------------------
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") { // if empty value was set, mark all previously uploaded files for deletion
if len(oldNames) > 0 { if len(list.ToUniqueStringSlice(value)) == 0 && len(oldNames) > 0 {
form.filesToDelete = append(form.filesToDelete, oldNames...) form.filesToDelete = append(form.filesToDelete, oldNames...)
} form.Data[key] = []string{}
form.Data[key] = "" } else if len(oldNames) > 0 {
}
} else if options.MaxSelect > 1 {
// search for individual file index to delete (eg. "file.0")
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
indexesToDelete := make([]int, 0, len(extendedData)) indexesToDelete := make([]int, 0, len(extendedData))
// search for individual file name to delete (eg. "file.test.png = null")
for i, name := range oldNames {
if v, ok := extendedData[key+"."+name]; ok && cast.ToString(v) == "" {
indexesToDelete = append(indexesToDelete, i)
}
}
// search for individual file index to delete (eg. "file.0 = null")
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
for indexedKey := range extendedData { for indexedKey := range extendedData {
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" { if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:]) index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
@@ -234,7 +239,14 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
form.Data[key] = nonDeleted form.Data[key] = nonDeleted
} }
// check if there are any new uploaded form files // -----------------------------------------------------------
// Check for new uploaded file
// -----------------------------------------------------------
if form.getContentType(r) != "multipart/form-data" {
continue // file upload is supported only via multipart/form-data
}
files, err := rest.FindUploadedFiles(r, key) files, err := rest.FindUploadedFiles(r, key)
if err != nil { if err != nil {
if form.config.App.IsDebug() { if form.config.App.IsDebug() {
@@ -286,7 +298,7 @@ func (form *RecordUpsert) Validate() error {
// record data validator // record data validator
dataValidator := validators.NewRecordDataValidator( dataValidator := validators.NewRecordDataValidator(
form.config.TxDao, form.config.Dao,
form.record, form.record,
form.filesToUpload, form.filesToUpload,
) )
@@ -316,7 +328,7 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
return err 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) tx, ok := txDao.DB().(*dbx.Tx)
if !ok { if !ok {
return errors.New("failed to get transaction db") return errors.New("failed to get transaction db")
@@ -366,7 +378,7 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
} }
return runInterceptors(func() 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 // persist record model
if err := txDao.SaveRecord(form.record); err != nil { if err := txDao.SaveRecord(form.record); err != nil {
return err return err
+20 -19
View File
@@ -98,7 +98,7 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
"onefile": nil, "onefile": nil,
"manyfiles.0": "", "manyfiles.0": "",
"manyfiles.1": "test.png", // should be ignored "manyfiles.1": "test.png", // should be ignored
"onlyimages": nil, // should be ignored "onlyimages": nil,
} }
form := forms.NewRecordUpsert(app, record) form := forms.NewRecordUpsert(app, record)
@@ -136,18 +136,12 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
} }
manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles)) manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles))
if manyfilesRemains != 1 { if manyfilesRemains != 1 {
t.Fatalf("Expect only 1 manyfiles to remain, got %v", manyfiles) t.Fatalf("Expect only 1 manyfiles to remain, got \n%v", manyfiles)
} }
// cannot reset multiple file upload field with just using the field name onlyimages := form.Data["onlyimages"]
onlyimages, ok := form.Data["onlyimages"] if len(list.ToUniqueStringSlice(onlyimages)) != 0 {
if !ok || onlyimages == nil { t.Fatalf("Expect onlyimages field to be deleted, got \n%v", onlyimages)
t.Fatal("Expect onlyimages field to be set and not be altered")
}
onlyimagesRemains := len(list.ToUniqueStringSlice(onlyimages))
expectedRemains := 2 // 2 existing
if onlyimagesRemains != expectedRemains {
t.Fatalf("Expect onlyimages to be %d, got %d (%v)", expectedRemains, onlyimagesRemains, onlyimages)
} }
} }
@@ -167,9 +161,10 @@ func TestRecordUpsertLoadDataMultipart(t *testing.T) {
"unknown": "test456", "unknown": "test456",
// file fields unset/delete // file fields unset/delete
"onefile": "", "onefile": "",
"manyfiles.0": "", "manyfiles.0": "", // delete by index
"manyfiles.b635c395-6837-49e5-8535-b0a6ebfbdbf3.png": "", // delete by name
"manyfiles.1": "test.png", // should be ignored "manyfiles.1": "test.png", // should be ignored
"onlyimages": "", // should be ignored "onlyimages": "",
}, "onlyimages") }, "onlyimages")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -208,16 +203,16 @@ func TestRecordUpsertLoadDataMultipart(t *testing.T) {
t.Fatal("Expect manyfiles field to be set") t.Fatal("Expect manyfiles field to be set")
} }
manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles)) manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles))
if manyfilesRemains != 1 { if manyfilesRemains != 0 {
t.Fatalf("Expect only 1 manyfiles to remain, got %v", manyfiles) t.Fatalf("Expect 0 manyfiles to remain, got %v", manyfiles)
} }
onlyimages, ok := form.Data["onlyimages"] onlyimages, ok := form.Data["onlyimages"]
if !ok || onlyimages == nil { if !ok || onlyimages == nil {
t.Fatal("Expect onlyimages field to be set and not be altered") t.Fatal("Expect onlyimages field to be set")
} }
onlyimagesRemains := len(list.ToUniqueStringSlice(onlyimages)) onlyimagesRemains := len(list.ToUniqueStringSlice(onlyimages))
expectedRemains := 3 // 2 existing + 1 new upload expectedRemains := 1 // -2 removed + 1 new upload
if onlyimagesRemains != expectedRemains { if onlyimagesRemains != expectedRemains {
t.Fatalf("Expect onlyimages to be %d, got %d (%v)", expectedRemains, onlyimagesRemains, onlyimages) t.Fatalf("Expect onlyimages to be %d, got %d (%v)", expectedRemains, onlyimagesRemains, onlyimages)
} }
@@ -489,8 +484,9 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
} }
formData, mp, err := tests.MockMultipartData(map[string]string{ formData, mp, err := tests.MockMultipartData(map[string]string{
"title": "test_save", "title": "test_save",
"onefile": "", "onefile": "",
"onlyimages": "",
}, "manyfiles.1", "manyfiles") // replace + new file }, "manyfiles.1", "manyfiles") // replace + new file
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -535,6 +531,11 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
t.Fatal("Expected record.onefile to be deleted") t.Fatal("Expected record.onefile to be deleted")
} }
onlyimages := (recordAfter.GetStringSliceDataValue("onlyimages"))
if len(onlyimages) != 0 {
t.Fatalf("Expected all onlyimages files to be deleted, got %d (%v)", len(onlyimages), onlyimages)
}
manyfiles := (recordAfter.GetStringSliceDataValue("manyfiles")) manyfiles := (recordAfter.GetStringSliceDataValue("manyfiles"))
if len(manyfiles) != 3 { if len(manyfiles) != 3 {
t.Fatalf("Expected 3 manyfiles, got %d (%v)", len(manyfiles), manyfiles) t.Fatalf("Expected 3 manyfiles, got %d (%v)", len(manyfiles), manyfiles)
+10 -10
View File
@@ -20,16 +20,16 @@ type SettingsUpsert struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type SettingsUpsertConfig struct { type SettingsUpsertConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
TxLogsDao *daos.Dao LogsDao *daos.Dao
} }
// NewSettingsUpsert creates a new [SettingsUpsert] form with initializer // NewSettingsUpsert creates a new [SettingsUpsert] form with initializer
// config created from the provided [core.App] instance. // config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewSettingsUpsert(app core.App) *SettingsUpsert {
return NewSettingsUpsertWithConfig(SettingsUpsertConfig{ return NewSettingsUpsertWithConfig(SettingsUpsertConfig{
App: app, App: app,
@@ -45,12 +45,12 @@ func NewSettingsUpsertWithConfig(config SettingsUpsertConfig) *SettingsUpsert {
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
if form.config.TxLogsDao == nil { if form.config.LogsDao == nil {
form.config.TxLogsDao = form.config.App.LogsDao() form.config.LogsDao = form.config.App.LogsDao()
} }
// load the application settings into the form // 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()) encryptionKey := os.Getenv(form.config.App.EncryptionEnv())
return runInterceptors(func() error { return runInterceptors(func() error {
saveErr := form.config.TxDao.SaveParam( saveErr := form.config.Dao.SaveParam(
models.ParamAppSettings, models.ParamAppSettings,
form.Settings, form.Settings,
encryptionKey, encryptionKey,
@@ -88,7 +88,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
} }
// explicitly trigger old logs deletion // explicitly trigger old logs deletion
form.config.TxLogsDao.DeleteOldRequests( form.config.LogsDao.DeleteOldRequests(
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays), time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
) )
+69
View File
@@ -0,0 +1,69 @@
package forms
import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails"
"github.com/pocketbase/pocketbase/models"
)
const (
templateVerification = "verification"
templatePasswordReset = "password-reset"
templateEmailChange = "email-change"
)
// TestEmailSend is a email template test request form.
type TestEmailSend struct {
app core.App
Template string `form:"template" json:"template"`
Email string `form:"email" json:"email"`
}
// NewTestEmailSend creates and initializes new TestEmailSend form.
func NewTestEmailSend(app core.App) *TestEmailSend {
return &TestEmailSend{app: app}
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *TestEmailSend) Validate() error {
return validation.ValidateStruct(form,
validation.Field(
&form.Email,
validation.Required,
validation.Length(1, 255),
is.EmailFormat,
),
validation.Field(
&form.Template,
validation.Required,
validation.In(templateVerification, templateEmailChange, templatePasswordReset),
),
)
}
// Submit validates and sends a test email to the form.Email address.
func (form *TestEmailSend) Submit() error {
if err := form.Validate(); err != nil {
return err
}
// create a test user
user := &models.User{}
user.Id = "__pb_test_id__"
user.Email = form.Email
user.RefreshTokenKey()
switch form.Template {
case templateVerification:
return mails.SendUserVerification(form.app, user)
case templatePasswordReset:
return mails.SendUserPasswordReset(form.app, user)
case templateEmailChange:
return mails.SendUserChangeEmail(form.app, user, form.Email)
}
return nil
}
+103
View File
@@ -0,0 +1,103 @@
package forms_test
import (
"strings"
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tests"
)
func TestEmailSendValidate(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
template string
email string
expectedErrors []string
}{
{"", "", []string{"template", "email"}},
{"invalid", "test@example.com", []string{"template"}},
{"verification", "invalid", []string{"email"}},
{"verification", "test@example.com", nil},
{"password-reset", "test@example.com", nil},
{"email-change", "test@example.com", nil},
}
for i, s := range scenarios {
form := forms.NewTestEmailSend(app)
form.Email = s.email
form.Template = s.template
result := form.Validate()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Errorf("(%d) Failed to parse errors %v", i, result)
continue
}
// check errors
if len(errs) > len(s.expectedErrors) {
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
}
for _, k := range s.expectedErrors {
if _, ok := errs[k]; !ok {
t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs)
}
}
}
}
func TestEmailSendSubmit(t *testing.T) {
scenarios := []struct {
template string
email string
expectError bool
}{
{"", "", true},
{"invalid", "test@example.com", true},
{"verification", "invalid", true},
{"verification", "test@example.com", false},
{"password-reset", "test@example.com", false},
{"email-change", "test@example.com", false},
}
for i, s := range scenarios {
app, _ := tests.NewTestApp()
defer app.Cleanup()
form := forms.NewTestEmailSend(app)
form.Email = s.email
form.Template = s.template
err := form.Submit()
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err)
}
if hasErr {
continue
}
if app.TestMailer.TotalSend != 1 {
t.Errorf("(%d) Expected one email to be sent, got %d", i, app.TestMailer.TotalSend)
}
expectedContent := "Verify"
if s.template == "password-reset" {
expectedContent = "Reset password"
} else if s.template == "email-change" {
expectedContent = "Confirm new email"
}
if !strings.Contains(app.TestMailer.LastHtmlBody, expectedContent) {
t.Errorf("(%d) Expected the email to contains %s, got \n%v", i, expectedContent, app.TestMailer.LastHtmlBody)
}
}
}
+8 -8
View File
@@ -20,8 +20,8 @@ type UserEmailChangeConfirm struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserEmailChangeConfirmConfig struct { type UserEmailChangeConfirmConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserEmailChangeConfirm creates a new [UserEmailChangeConfirm] // NewUserEmailChangeConfirm creates a new [UserEmailChangeConfirm]
@@ -29,7 +29,7 @@ type UserEmailChangeConfirmConfig struct {
// //
// This factory method is used primarily for convenience (and backward compatibility). // This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use // 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 { func NewUserEmailChangeConfirm(app core.App) *UserEmailChangeConfirm {
return NewUserEmailChangeConfirmWithConfig(UserEmailChangeConfirmConfig{ return NewUserEmailChangeConfirmWithConfig(UserEmailChangeConfirmConfig{
App: app, App: app,
@@ -45,8 +45,8 @@ func NewUserEmailChangeConfirmWithConfig(config UserEmailChangeConfirmConfig) *U
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form 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 // 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) 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 // 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, token,
form.config.App.Settings().UserEmailChangeToken.Secret, form.config.App.Settings().UserEmailChangeToken.Secret,
) )
@@ -135,7 +135,7 @@ func (form *UserEmailChangeConfirm) Submit() (*models.User, error) {
user.Verified = true user.Verified = true
user.RefreshTokenKey() // invalidate old tokens 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 return nil, err
} }
+7 -7
View File
@@ -21,15 +21,15 @@ type UserEmailChangeRequest struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserEmailChangeRequestConfig struct { type UserEmailChangeRequestConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserEmailChangeRequest creates a new [UserEmailChangeRequest] // NewUserEmailChangeRequest creates a new [UserEmailChangeRequest]
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserEmailChangeRequest(app core.App, user *models.User) *UserEmailChangeRequest {
return NewUserEmailChangeRequestWithConfig(UserEmailChangeRequestConfig{ return NewUserEmailChangeRequestWithConfig(UserEmailChangeRequestConfig{
App: app, App: app,
@@ -48,8 +48,8 @@ func NewUserEmailChangeRequestWithConfig(config UserEmailChangeRequestConfig, us
panic("Invalid initializer config or nil user model.") panic("Invalid initializer config or nil user model.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -62,7 +62,7 @@ func (form *UserEmailChangeRequest) Validate() error {
&form.NewEmail, &form.NewEmail,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
validation.By(form.checkUniqueEmail), validation.By(form.checkUniqueEmail),
), ),
) )
@@ -71,7 +71,7 @@ func (form *UserEmailChangeRequest) Validate() error {
func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error { func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error {
v, _ := value.(string) 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.") return validation.NewError("validation_user_email_exists", "User email already exists.")
} }
+7 -7
View File
@@ -20,8 +20,8 @@ type UserEmailLogin struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserEmailLoginConfig struct { type UserEmailLoginConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserEmailLogin creates a new [UserEmailLogin] form with // 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). // This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use // 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 { func NewUserEmailLogin(app core.App) *UserEmailLogin {
return NewUserEmailLoginWithConfig(UserEmailLoginConfig{ return NewUserEmailLoginWithConfig(UserEmailLoginConfig{
App: app, App: app,
@@ -45,8 +45,8 @@ func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin {
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -55,7 +55,7 @@ func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin {
// Validate makes the form validatable by implementing [validation.Validatable] interface. // Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *UserEmailLogin) Validate() error { func (form *UserEmailLogin) Validate() error {
return validation.ValidateStruct(form, return validation.ValidateStruct(form,
validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.Email), validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.EmailFormat),
validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), validation.Field(&form.Password, validation.Required, validation.Length(1, 255)),
) )
} }
@@ -67,7 +67,7 @@ func (form *UserEmailLogin) Submit() (*models.User, error) {
return nil, err return nil, err
} }
user, err := form.config.TxDao.FindUserByEmail(form.Email) user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+71 -44
View File
@@ -35,15 +35,15 @@ type UserOauth2Login struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserOauth2LoginConfig struct { type UserOauth2LoginConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserOauth2Login creates a new [UserOauth2Login] form with // NewUserOauth2Login creates a new [UserOauth2Login] form with
// initializer config created from the provided [core.App] instance. // initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserOauth2Login(app core.App) *UserOauth2Login {
return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{ return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{
App: app, App: app,
@@ -59,8 +59,8 @@ func NewUserOauth2LoginWithConfig(config UserOauth2LoginConfig) *UserOauth2Login
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -99,8 +99,11 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, nil, err return nil, nil, err
} }
// load provider configuration
config := form.config.App.Settings().NamedAuthProviderConfigs()[form.Provider] 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) provider.SetRedirectUrl(form.RedirectUrl)
@@ -113,55 +116,79 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, nil, err return nil, nil, err
} }
// fetch auth user // fetch external auth user
authData, err := provider.FetchAuthUser(token) authData, err := provider.FetchAuthUser(token)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// login/register the auth user var user *models.User
user, _ := form.config.TxDao.FindUserByEmail(authData.Email)
if user != nil { // check for existing relation with the external auth user
// update the existing user's verified state rel, _ := form.config.Dao.FindExternalAuthByProvider(form.Provider, authData.Id)
if !user.Verified { 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 user.Verified = true
if err := form.config.TxDao.SaveUser(user); err != nil { user.Email = authData.Email
return nil, authData, err 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 { // create ExternalAuth relation if missing
// registration of new users is not allowed via the Oauth2 provider if rel == nil {
return nil, authData, errors.New("Cannot find user with the authorized email.") rel = &models.ExternalAuth{
} UserId: user.Id,
Provider: form.Provider,
ProviderId: authData.Id,
}
if err := txDao.SaveExternalAuth(rel); err != nil {
return err
}
}
// create new user return nil
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
event := &core.UserOauth2RegisterEvent{ if saveErr != nil {
User: user, return nil, authData, saveErr
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
} }
return user, authData, nil return user, authData, nil
+8 -8
View File
@@ -22,15 +22,15 @@ type UserPasswordResetConfirm struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserPasswordResetConfirmConfig struct { type UserPasswordResetConfirmConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserPasswordResetConfirm creates a new [UserPasswordResetConfirm] // NewUserPasswordResetConfirm creates a new [UserPasswordResetConfirm]
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserPasswordResetConfirm(app core.App) *UserPasswordResetConfirm {
return NewUserPasswordResetConfirmWithConfig(UserPasswordResetConfirmConfig{ return NewUserPasswordResetConfirmWithConfig(UserPasswordResetConfirmConfig{
App: app, App: app,
@@ -46,8 +46,8 @@ func NewUserPasswordResetConfirmWithConfig(config UserPasswordResetConfirmConfig
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -70,7 +70,7 @@ func (form *UserPasswordResetConfirm) checkToken(value any) error {
return nil // nothing to check return nil // nothing to check
} }
user, err := form.config.TxDao.FindUserByToken( user, err := form.config.Dao.FindUserByToken(
v, v,
form.config.App.Settings().UserPasswordResetToken.Secret, form.config.App.Settings().UserPasswordResetToken.Secret,
) )
@@ -88,7 +88,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
return nil, err return nil, err
} }
user, err := form.config.TxDao.FindUserByToken( user, err := form.config.Dao.FindUserByToken(
form.Token, form.Token,
form.config.App.Settings().UserPasswordResetToken.Secret, form.config.App.Settings().UserPasswordResetToken.Secret,
) )
@@ -100,7 +100,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
return nil, err return nil, err
} }
if err := form.config.TxDao.SaveUser(user); err != nil { if err := form.config.Dao.SaveUser(user); err != nil {
return nil, err return nil, err
} }
+7 -7
View File
@@ -25,7 +25,7 @@ type UserPasswordResetRequest struct {
// NB! App is required struct member. // NB! App is required struct member.
type UserPasswordResetRequestConfig struct { type UserPasswordResetRequestConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
ResendThreshold float64 // in seconds ResendThreshold float64 // in seconds
} }
@@ -33,7 +33,7 @@ type UserPasswordResetRequestConfig struct {
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserPasswordResetRequest(app core.App) *UserPasswordResetRequest {
return NewUserPasswordResetRequestWithConfig(UserPasswordResetRequestConfig{ return NewUserPasswordResetRequestWithConfig(UserPasswordResetRequestConfig{
App: app, App: app,
@@ -50,8 +50,8 @@ func NewUserPasswordResetRequestWithConfig(config UserPasswordResetRequestConfig
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -66,7 +66,7 @@ func (form *UserPasswordResetRequest) Validate() error {
&form.Email, &form.Email,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
), ),
) )
} }
@@ -78,7 +78,7 @@ func (form *UserPasswordResetRequest) Submit() error {
return err return err
} }
user, err := form.config.TxDao.FindUserByEmail(form.Email) user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil { if err != nil {
return err return err
} }
@@ -96,5 +96,5 @@ func (form *UserPasswordResetRequest) Submit() error {
// update last sent timestamp // update last sent timestamp
user.LastResetSentAt = types.NowDateTime() 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. // NB! App is required struct member.
type UserUpsertConfig struct { type UserUpsertConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserUpsert creates a new [UserUpsert] form with initializer // 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{}`). // (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 // 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 { func NewUserUpsert(app core.App, user *models.User) *UserUpsert {
return NewUserUpsertWithConfig(UserUpsertConfig{ return NewUserUpsertWithConfig(UserUpsertConfig{
App: app, App: app,
@@ -57,8 +57,8 @@ func NewUserUpsertWithConfig(config UserUpsertConfig, user *models.User) *UserUp
panic("Invalid initializer config or nil upsert model.") panic("Invalid initializer config or nil upsert model.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
// load defaults // load defaults
@@ -83,7 +83,7 @@ func (form *UserUpsert) Validate() error {
&form.Email, &form.Email,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
validation.By(form.checkEmailDomain), validation.By(form.checkEmailDomain),
validation.By(form.checkUniqueEmail), validation.By(form.checkUniqueEmail),
), ),
@@ -103,7 +103,7 @@ func (form *UserUpsert) Validate() error {
func (form *UserUpsert) checkUniqueEmail(value any) error { func (form *UserUpsert) checkUniqueEmail(value any) error {
v, _ := value.(string) 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 return nil
} }
@@ -160,6 +160,6 @@ func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error {
form.user.Email = form.Email form.user.Email = form.Email
return runInterceptors(func() error { return runInterceptors(func() error {
return form.config.TxDao.SaveUser(form.user) return form.config.Dao.SaveUser(form.user)
}, interceptors...) }, interceptors...)
} }
+8 -8
View File
@@ -19,15 +19,15 @@ type UserVerificationConfirm struct {
// //
// NB! App is required struct member. // NB! App is required struct member.
type UserVerificationConfirmConfig struct { type UserVerificationConfirmConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
} }
// NewUserVerificationConfirm creates a new [UserVerificationConfirm] // NewUserVerificationConfirm creates a new [UserVerificationConfirm]
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserVerificationConfirm(app core.App) *UserVerificationConfirm {
return NewUserVerificationConfirmWithConfig(UserVerificationConfirmConfig{ return NewUserVerificationConfirmWithConfig(UserVerificationConfirmConfig{
App: app, App: app,
@@ -43,8 +43,8 @@ func NewUserVerificationConfirmWithConfig(config UserVerificationConfirmConfig)
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -63,7 +63,7 @@ func (form *UserVerificationConfirm) checkToken(value any) error {
return nil // nothing to check return nil // nothing to check
} }
user, err := form.config.TxDao.FindUserByToken( user, err := form.config.Dao.FindUserByToken(
v, v,
form.config.App.Settings().UserVerificationToken.Secret, form.config.App.Settings().UserVerificationToken.Secret,
) )
@@ -81,7 +81,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
return nil, err return nil, err
} }
user, err := form.config.TxDao.FindUserByToken( user, err := form.config.Dao.FindUserByToken(
form.Token, form.Token,
form.config.App.Settings().UserVerificationToken.Secret, form.config.App.Settings().UserVerificationToken.Secret,
) )
@@ -95,7 +95,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
user.Verified = true user.Verified = true
if err := form.config.TxDao.SaveUser(user); err != nil { if err := form.config.Dao.SaveUser(user); err != nil {
return nil, err return nil, err
} }
+7 -7
View File
@@ -25,7 +25,7 @@ type UserVerificationRequest struct {
// NB! App is required struct member. // NB! App is required struct member.
type UserVerificationRequestConfig struct { type UserVerificationRequestConfig struct {
App core.App App core.App
TxDao *daos.Dao Dao *daos.Dao
ResendThreshold float64 // in seconds ResendThreshold float64 // in seconds
} }
@@ -33,7 +33,7 @@ type UserVerificationRequestConfig struct {
// form with initializer config created from the provided [core.App] instance. // form with initializer config created from the provided [core.App] instance.
// //
// If you want to submit the form as part of another transaction, use // 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 { func NewUserVerificationRequest(app core.App) *UserVerificationRequest {
return NewUserVerificationRequestWithConfig(UserVerificationRequestConfig{ return NewUserVerificationRequestWithConfig(UserVerificationRequestConfig{
App: app, App: app,
@@ -50,8 +50,8 @@ func NewUserVerificationRequestWithConfig(config UserVerificationRequestConfig)
panic("Missing required config.App instance.") panic("Missing required config.App instance.")
} }
if form.config.TxDao == nil { if form.config.Dao == nil {
form.config.TxDao = form.config.App.Dao() form.config.Dao = form.config.App.Dao()
} }
return form return form
@@ -66,7 +66,7 @@ func (form *UserVerificationRequest) Validate() error {
&form.Email, &form.Email,
validation.Required, validation.Required,
validation.Length(1, 255), validation.Length(1, 255),
is.Email, is.EmailFormat,
), ),
) )
} }
@@ -78,7 +78,7 @@ func (form *UserVerificationRequest) Submit() error {
return err return err
} }
user, err := form.config.TxDao.FindUserByEmail(form.Email) user, err := form.config.Dao.FindUserByEmail(form.Email)
if err != nil { if err != nil {
return err return err
} }
@@ -100,5 +100,5 @@ func (form *UserVerificationRequest) Submit() error {
// update last sent timestamp // update last sent timestamp
user.LastVerificationSentAt = types.NowDateTime() 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 ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net/http"
"strings" "strings"
"github.com/gabriel-vasile/mimetype"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/rest" "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.") return validation.NewError("validation_invalid_mime_type", "Unsupported file type.")
} }
filetype := http.DetectContentType(v.Bytes()) filetype := mimetype.Detect(v.Bytes())
for _, t := range validTypes { for _, t := range validTypes {
if t == filetype { if filetype.Is(t) {
return nil // valid return nil // valid
} }
} }
+1 -1
View File
@@ -189,7 +189,7 @@ func (validator *RecordDataValidator) checkEmailValue(field *schema.SchemaField,
return nil // nothing to check 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") return validation.NewError("validation_invalid_email", "Must be a valid email")
} }
+45 -47
View File
@@ -4,83 +4,81 @@ go 1.18
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.5 github.com/AlecAivazis/survey/v2 v2.3.5
github.com/aws/aws-sdk-go v1.44.48 github.com/aws/aws-sdk-go v1.44.85
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.3.3 github.com/domodwyer/mailyak/v3 v3.3.4
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/ganigeorgiev/fexpr v0.1.1 github.com/ganigeorgiev/fexpr v0.1.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.4.2 github.com/golang-jwt/jwt/v4 v4.4.2
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
github.com/mattn/go-sqlite3 v1.14.14 github.com/mattn/go-sqlite3 v1.14.15
github.com/microcosm-cc/bluemonday v1.0.19
github.com/pocketbase/dbx v1.6.0 github.com/pocketbase/dbx v1.6.0
github.com/spf13/cast v1.5.0 github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
gocloud.dev v0.25.0 gocloud.dev v0.26.0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c
modernc.org/sqlite v1.17.3 golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
modernc.org/sqlite v1.18.1
) )
require ( require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.7 // indirect github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.15.13 // indirect github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.8 // 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.8 // 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.19 // 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.14 // 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.8 // 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.15 // 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.5 // 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.3 // 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.9 // 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.8 // 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.8 // 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.1 // 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.11 // 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.9 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
github.com/aws/smithy-go v1.12.0 // indirect github.com/aws/smithy-go v1.12.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect github.com/googleapis/gax-go/v2 v2.5.1 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
go.opencensus.io v0.23.0 // indirect go.opencensus.io v0.23.0 // indirect
golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 // indirect golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.11 // indirect golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/api v0.86.0 // indirect google.golang.org/api v0.94.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53 // indirect google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/grpc v1.47.0 // indirect google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.36.0 // indirect modernc.org/cc/v3 v3.36.3 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.16.14 // indirect modernc.org/libc v1.17.0 // indirect
modernc.org/mathutil v1.4.1 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.1.1 // indirect modernc.org/memory v1.2.0 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.2 // indirect modernc.org/strutil v1.1.2 // indirect
modernc.org/token v1.0.0 // indirect modernc.org/token v1.0.0 // indirect
+93 -104
View File
@@ -122,68 +122,66 @@ 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.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.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.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.48 h1:jLDC9RsNoYMLFlKpB8LdqUnoDdC2yvkS4QbuyPQJ8+M= github.com/aws/aws-sdk-go v1.44.85 h1:JM2rkKY/GtTDCQXW0StkImbLn6n4Q/Dm2bj+u1rm7Kw=
github.com/aws/aws-sdk-go v1.44.48/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.85/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.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2 v1.16.7 h1:zfBwXus3u14OszRxGcqCDS4MfMCv10e8SMJ2r8Xm0Ns= github.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ=
github.com/aws/aws-sdk-go-v2 v1.16.7/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=
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.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk= 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.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y= 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/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= 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.15.13 h1:CJH9zn/Enst7lDiGpoguVt0lZr5HcpNVlRJWbJ6qreo= github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA=
github.com/aws/aws-sdk-go-v2/config v1.15.13/go.mod h1:AcMu50uhV6wMBUlURnEXhr9b3fX6FLSTlEV89krTEGk= github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= 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.8 h1:niTa7zc7uyOP2ufri0jPESBt1h9yP3Zc0q+xzih3h8o= 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.8/go.mod h1:P2Hd4Sy7mXRxPNcQMPBmqszSJoDXexX8XEDaT6lucO0= github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY=
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.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 h1:VfBdn2AxwMbFyJN/lF/xuT3SakomJ86PZu3rCxb5K0s= 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.8/go.mod h1:oL1Q3KuCq1D4NykQnIvtRiBGLUXhcpY5pl6QZB2XEPU= 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/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= 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.19 h1:WfCYqsAADDRNCQQ5LGcrlqbR7SK3PYrP/UCh7qNGBQM= 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.19/go.mod h1:koLPv2oF6ksE3zBKLDP0GFmKfaCmYwVHqGIbaPrHIRg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27/go.mod h1:+tj2cHQkChanggNZn1J2fJ1Cv6RO1TV0AA3472do31I=
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.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 h1:2C0pYHcUBmdzPj+EKNC4qj97oK6yjrUhc1KoSodglvk= 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.14/go.mod h1:kdjrMwHwrC3+FsKhNcCMJ7tUVj/8uSD5CZXeQ4wV6fM= 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/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= 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.8 h1:2J+jdlBJWEmTyAwC82Ym68xCykIvnSnIN18b8xHGlcc= 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.8/go.mod h1:ZIV8GYoC6WLBW5KGs+o4rsc65/ozd+eQ0L31XF5VDwk= 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/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= 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.15 h1:QquxR7NH3ULBsKC+NoTpilzbKKS+5AELfNREInbhvas= 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.15/go.mod h1:Tkrthp/0sNBShQQsamR7j/zY4p19tVTAs+nnqhH6R3c= 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/v4a v1.0.5 h1:tEEHn+PGAxRVqMPEhtU8oCSW/1Ge3zP5nUgPrGQNUPs= 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.5/go.mod h1:aIwFF3dUk95ocCcA3zfk3nhz0oLkpzHFWuMp8l/4nNs= 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/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.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc= 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.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw= 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/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= 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.9 h1:gVv2vXOMqJeR4ZHHV32K7LElIJIIzyw/RU1b0lSfWTQ= 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.9/go.mod h1:EF5RLnD9l0xvEWwMRcktIS/dI6lF8lU5eV3B13k6sWo= 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/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= 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.8 h1:oKnAXxSF2FUvfgw8uzU/v9OTYorJJZ8eBmWhr9TWVVQ= 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.8/go.mod h1:rDVhIMAX9N2r8nWxDUlbubvvaFMnfsm+3jAV7q+rpM4= 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/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= 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.8 h1:TlN1UC39A0LUNoD51ubO5h32haznA+oVe15jO9O4Lj0= 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.8/go.mod h1:JlVwmWtT/1c5W+6oUsjXjAJ0iJZ+hlghdrDy/8JxGCU= 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/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= 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.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 h1:OKQIQ0QhEBmGr2LfT952meIZz3ujrPYnxH+dO/5ldnI= 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.1/go.mod h1:NffjpNsMUFXp6Ok/PahrktAncoekWrywvmIK83Q2raE= 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/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= 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/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= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
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.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 h1:XOJWXNFXJyapJqQuCIPfftsOf0XZZioM0kK6OPRt9MY= 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.11/go.mod h1:MO4qguFjs3wPGcCSpQ7kOFTwRvb+eu+fn+1vKleGHUk= 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/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= 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.9 h1:yOfILxyjmtr2ubRkRJldlHDFBhf5vw4CzhbwWIBmimQ= 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.9/go.mod h1:O1IvkYxr+39hRf960Us6j0x1P8pDqhTX+oXM5kQNl/Y= github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0= github.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -220,8 +218,8 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/domodwyer/mailyak/v3 v3.3.3 h1:E9cjqDUiwY1QSE5G2CbWHM7EJV5FybKPHnGovc2iaA8= github.com/domodwyer/mailyak/v3 v3.3.4 h1:AG/pvcz2/ocFqZkPEG7lPAa0MhCq1warfUEKJt6Fagk=
github.com/domodwyer/mailyak/v3 v3.3.3/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= github.com/domodwyer/mailyak/v3 v3.3.4/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -240,6 +238,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/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 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 h1:La9kYEgTcIutvOnqNZ8pOUD0O0Q/Gn15sTVEX+IeBE8=
github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -370,12 +370,11 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw=
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
@@ -386,8 +385,9 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@@ -462,25 +462,22 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-ieproxy v0.0.3 h1:YkaHmK1CzE5C4O7A3hv3TCbfNDPSCf0RKZFX+VhBeYk=
github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -565,8 +562,8 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.26.0 h1:4rM/SVL0lLs+rhC0Gmc+gt/82DBpb7nbpIZKXXnfMXg=
gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= gocloud.dev v0.26.0/go.mod h1:mkUgejbnbLotorqDyvedJO20XcZNTynmSeVSQS9btVg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -582,8 +579,8 @@ 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-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-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-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 h1:vJ2V3lFLg+bBhgroYuRfyN583UzVveQmIXjc8T/y3to=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503/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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -597,8 +594,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -665,7 +662,6 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -674,8 +670,8 @@ 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-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-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-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM= golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes=
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -698,9 +694,8 @@ 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-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-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-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-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 h1:VnGaRqoLmqZH/3TMLJwYCEWkR4j1nuIU1U9TvbqsDUw= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0/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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -770,13 +765,11 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -791,14 +784,15 @@ 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-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-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-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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-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-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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -814,8 +808,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -874,8 +868,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -932,8 +926,8 @@ 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.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= 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.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.86.0 h1:ZAnyOHQFIuWso1BodVfSaRyffD74T9ERGFa3k1fNk/U= google.golang.org/api v0.94.0 h1:KtKM9ru3nzQioV1HLlUf1cR7vMYJIpgls5VhAYQXIwA=
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1036,8 +1030,8 @@ 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-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-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-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53 h1:liFd7OL799HvMNYG5xozhUoWDj944y+zXPDOhu4PyaM= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI=
google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1068,8 +1062,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 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.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.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/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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1084,8 +1079,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -1109,42 +1105,35 @@ 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.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= 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 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= 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.17.0 h1:nbL2Lv0I323wLc1GmTh/AqVtI9JeBVc7Nhapdg9EONs=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.14 h1:MUIjk9Xwlkrp0BqGhMfRkiq0EkZsqfNiP4eixL3YiPk=
modernc.org/libc v1.16.14/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 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/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= 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/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 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 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8=
modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= 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 h1:iFBDH6j1Z0bN/Q9udJnnFoFpENA4252qe/7/5woE5MI=
modernc.org/strutil v1.1.2/go.mod h1:OYajnUAcI/MX+XD/Wx7v1bbdvcQSvxgtb0gC+u3d3eg= modernc.org/strutil v1.1.2/go.mod h1:OYajnUAcI/MX+XD/Wx7v1bbdvcQSvxgtb0gC+u3d3eg=
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 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= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+4 -4
View File
@@ -3,12 +3,12 @@ package mails
import ( import (
"fmt" "fmt"
"net/mail" "net/mail"
"strings"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates" "github.com/pocketbase/pocketbase/mails/templates"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tokens"
"github.com/pocketbase/pocketbase/tools/rest"
) )
// SendAdminPasswordReset sends a password reset request email to the specified admin. // SendAdminPasswordReset sends a password reset request email to the specified admin.
@@ -18,9 +18,9 @@ func SendAdminPasswordReset(app core.App, admin *models.Admin) error {
return tokenErr return tokenErr
} }
actionUrl, urlErr := normalizeUrl(fmt.Sprintf( actionUrl, urlErr := rest.NormalizeUrl(fmt.Sprintf(
"%s/#/confirm-password-reset/%s", "%s/_/#/confirm-password-reset/%s",
strings.TrimSuffix(app.Settings().Meta.AppUrl, "/"), app.Settings().Meta.AppUrl,
token, token,
)) ))
if urlErr != nil { if urlErr != nil {
+1 -1
View File
@@ -27,7 +27,7 @@ func TestSendAdminPasswordReset(t *testing.T) {
} }
expectedParts := []string{ expectedParts := []string{
"http://localhost:8090/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", "http://localhost:8090/_/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
} }
for _, part := range expectedParts { for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
-25
View File
@@ -4,34 +4,9 @@ package mails
import ( import (
"bytes" "bytes"
"net/url"
"path"
"strings"
"text/template" "text/template"
) )
// normalizeUrl removes duplicated slashes from a url path.
func normalizeUrl(originalUrl string) (string, error) {
u, err := url.Parse(originalUrl)
if err != nil {
return "", err
}
hasSlash := strings.HasSuffix(u.Path, "/")
// clean up path by removing duplicated /
u.Path = path.Clean(u.Path)
u.RawPath = path.Clean(u.RawPath)
// restore original trailing slash
if hasSlash && !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
u.RawPath += "/"
}
return u.String(), nil
}
// resolveTemplateContent resolves inline html template strings. // resolveTemplateContent resolves inline html template strings.
func resolveTemplateContent(data any, content ...string) (string, error) { func resolveTemplateContent(data any, content ...string) (string, error) {
if len(content) == 0 { if len(content) == 0 {
-1
View File
@@ -17,7 +17,6 @@ const AdminPasswordResetBody = `
<p> <p>
<a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Reset password</a> <a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Reset password</a>
<a class="fallback-link" href="{{.ActionUrl}}" target="_blank" rel="noopener">{{.ActionUrl}}</a>
</p> </p>
<p><i>If you did not request to reset your password, please ignore this email and the link will expire on its own.</i></p> <p><i>If you did not request to reset your password, please ignore this email and the link will expire on its own.</i></p>
-6
View File
@@ -50,12 +50,6 @@ const Layout = `
.hidden { .hidden {
display: none !important; display: none !important;
} }
.fallback-link {
display: none !important;
word-break: break-all;
font-size: 11px;
color: #666f75;
}
.btn { .btn {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
@@ -1,26 +0,0 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserConfirmEmailChangeBody = `
{{define "content"}}
<p>Hello,</p>
<p>Click on the button below to confirm your new email address.</p>
<p>
<a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Confirm new email</a>
<a class="fallback-link" href="{{.ActionUrl}}" target="_blank" rel="noopener">{{.ActionUrl}}</a>
</p>
<p><i>If you didn’t ask to change your email address, you can ignore this email.</i></p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
-26
View File
@@ -1,26 +0,0 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserPasswordResetBody = `
{{define "content"}}
<p>Hello,</p>
<p>Click on the button below to reset your password.</p>
<p>
<a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Reset password</a>
<a class="fallback-link" href="{{.ActionUrl}}" target="_blank" rel="noopener">{{.ActionUrl}}</a>
</p>
<p><i>If you didn’t ask to reset your password, you can ignore this email.</i></p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
-26
View File
@@ -1,26 +0,0 @@
package templates
// Available variables:
//
// ```
// User *models.User
// AppName string
// AppUrl string
// Token string
// ActionUrl string
// ```
const UserVerificationBody = `
{{define "content"}}
<p>Hello,</p>
<p>Thank you for joining us at {{.AppName}}.</p>
<p>Click on the button below to verify your email address.</p>
<p>
<a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Verify</a>
<a class="fallback-link" href="{{.ActionUrl}}" target="_blank" rel="noopener">{{.ActionUrl}}</a>
</p>
<p>
Thanks,<br/>
{{.AppName}} team
</p>
{{end}}
`
+46 -71
View File
@@ -1,8 +1,8 @@
package mails package mails
import ( import (
"html/template"
"net/mail" "net/mail"
"strings"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates" "github.com/pocketbase/pocketbase/mails/templates"
@@ -10,46 +10,6 @@ import (
"github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tokens"
) )
func prepareUserEmailBody(
app core.App,
user *models.User,
token string,
actionUrl string,
bodyTemplate string,
) (string, error) {
settings := app.Settings()
// replace action url placeholder params (if any)
actionUrlParams := map[string]string{
core.EmailPlaceholderAppUrl: settings.Meta.AppUrl,
core.EmailPlaceholderToken: token,
}
for k, v := range actionUrlParams {
actionUrl = strings.ReplaceAll(actionUrl, k, v)
}
var urlErr error
actionUrl, urlErr = normalizeUrl(actionUrl)
if urlErr != nil {
return "", urlErr
}
params := struct {
AppName string
AppUrl string
User *models.User
Token string
ActionUrl string
}{
AppName: settings.Meta.AppName,
AppUrl: settings.Meta.AppUrl,
User: user,
Token: token,
ActionUrl: actionUrl,
}
return resolveTemplateContent(params, templates.Layout, bodyTemplate)
}
// SendUserPasswordReset sends a password reset request email to the specified user. // SendUserPasswordReset sends a password reset request email to the specified user.
func SendUserPasswordReset(app core.App, user *models.User) error { func SendUserPasswordReset(app core.App, user *models.User) error {
token, tokenErr := tokens.NewUserResetPasswordToken(app, user) token, tokenErr := tokens.NewUserResetPasswordToken(app, user)
@@ -66,24 +26,20 @@ func SendUserPasswordReset(app core.App, user *models.User) error {
} }
sendErr := app.OnMailerBeforeUserResetPasswordSend().Trigger(event, func(e *core.MailerUserEvent) error { sendErr := app.OnMailerBeforeUserResetPasswordSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody( settings := app.Settings()
app,
user, subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ResetPasswordTemplate)
token,
app.Settings().Meta.UserResetPasswordUrl,
templates.UserPasswordResetBody,
)
if err != nil { if err != nil {
return err return err
} }
return e.MailClient.Send( return e.MailClient.Send(
mail.Address{ mail.Address{
Name: app.Settings().Meta.SenderName, Name: settings.Meta.SenderName,
Address: app.Settings().Meta.SenderAddress, Address: settings.Meta.SenderAddress,
}, },
mail.Address{Address: e.User.Email}, mail.Address{Address: e.User.Email},
("Reset your " + app.Settings().Meta.AppName + " password"), subject,
body, body,
nil, nil,
) )
@@ -112,24 +68,20 @@ func SendUserVerification(app core.App, user *models.User) error {
} }
sendErr := app.OnMailerBeforeUserVerificationSend().Trigger(event, func(e *core.MailerUserEvent) error { sendErr := app.OnMailerBeforeUserVerificationSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody( settings := app.Settings()
app,
user, subject, body, err := resolveEmailTemplate(app, token, settings.Meta.VerificationTemplate)
token,
app.Settings().Meta.UserVerificationUrl,
templates.UserVerificationBody,
)
if err != nil { if err != nil {
return err return err
} }
return e.MailClient.Send( return e.MailClient.Send(
mail.Address{ mail.Address{
Name: app.Settings().Meta.SenderName, Name: settings.Meta.SenderName,
Address: app.Settings().Meta.SenderAddress, Address: settings.Meta.SenderAddress,
}, },
mail.Address{Address: e.User.Email}, mail.Address{Address: e.User.Email},
("Verify your " + app.Settings().Meta.AppName + " email"), subject,
body, body,
nil, nil,
) )
@@ -161,24 +113,20 @@ func SendUserChangeEmail(app core.App, user *models.User, newEmail string) error
} }
sendErr := app.OnMailerBeforeUserChangeEmailSend().Trigger(event, func(e *core.MailerUserEvent) error { sendErr := app.OnMailerBeforeUserChangeEmailSend().Trigger(event, func(e *core.MailerUserEvent) error {
body, err := prepareUserEmailBody( settings := app.Settings()
app,
user, subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ConfirmEmailChangeTemplate)
token,
app.Settings().Meta.UserConfirmEmailChangeUrl,
templates.UserConfirmEmailChangeBody,
)
if err != nil { if err != nil {
return err return err
} }
return e.MailClient.Send( return e.MailClient.Send(
mail.Address{ mail.Address{
Name: app.Settings().Meta.SenderName, Name: settings.Meta.SenderName,
Address: app.Settings().Meta.SenderAddress, Address: settings.Meta.SenderAddress,
}, },
mail.Address{Address: newEmail}, mail.Address{Address: newEmail},
("Confirm your " + app.Settings().Meta.AppName + " new email address"), subject,
body, body,
nil, nil,
) )
@@ -190,3 +138,30 @@ func SendUserChangeEmail(app core.App, user *models.User, newEmail string) error
return sendErr return sendErr
} }
func resolveEmailTemplate(
app core.App,
token string,
emailTemplate core.EmailTemplate,
) (subject string, body string, err error) {
settings := app.Settings()
subject, rawBody, _ := emailTemplate.Resolve(
settings.Meta.AppName,
settings.Meta.AppUrl,
token,
)
params := struct {
HtmlContent template.HTML
}{
HtmlContent: template.HTML(rawBody),
}
body, err = resolveTemplateContent(params, templates.Layout, templates.HtmlBody)
if err != nil {
return "", "", err
}
return subject, body, nil
}
+3 -3
View File
@@ -27,7 +27,7 @@ func TestSendUserPasswordReset(t *testing.T) {
} }
expectedParts := []string{ expectedParts := []string{
"http://localhost:8090/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", "http://localhost:8090/_/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
} }
for _, part := range expectedParts { for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
@@ -52,7 +52,7 @@ func TestSendUserVerification(t *testing.T) {
} }
expectedParts := []string{ expectedParts := []string{
"http://localhost:8090/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", "http://localhost:8090/_/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
} }
for _, part := range expectedParts { for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
@@ -77,7 +77,7 @@ func TestSendUserChangeEmail(t *testing.T) {
} }
expectedParts := []string{ expectedParts := []string{
"http://localhost:8090/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", "http://localhost:8090/_/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
} }
for _, part := range expectedParts { for _, part := range expectedParts {
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
@@ -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
})
}
@@ -0,0 +1,57 @@
package logs
import (
"github.com/pocketbase/dbx"
)
func init() {
LogsMigrations.Register(func(db dbx.Builder) error {
// delete old index (don't check for error because of backward compatibility with old installations)
db.DropIndex("_requests", "_request_ip_idx").Execute()
// rename ip -> remoteIp
if _, err := db.RenameColumn("_requests", "ip", "remoteIp").Execute(); err != nil {
return err
}
// add new userIp column
if _, err := db.AddColumn("_requests", "userIp", `TEXT DEFAULT "127.0.0.1" NOT NULL`).Execute(); err != nil {
return err
}
// add new indexes
if _, err := db.CreateIndex("_requests", "_request_remote_ip_idx", "remoteIp").Execute(); err != nil {
return err
}
if _, err := db.CreateIndex("_requests", "_request_user_ip_idx", "userIp").Execute(); err != nil {
return err
}
return nil
}, func(db dbx.Builder) error {
// delete new indexes
if _, err := db.DropIndex("_requests", "_request_remote_ip_idx").Execute(); err != nil {
return err
}
if _, err := db.DropIndex("_requests", "_request_user_ip_idx").Execute(); err != nil {
return err
}
// drop userIp column
if _, err := db.DropColumn("_requests", "userIp").Execute(); err != nil {
return err
}
// restore original remoteIp column name
if _, err := db.RenameColumn("_requests", "remoteIp", "ip").Execute(); err != nil {
return err
}
// restore original index
if _, err := db.CreateIndex("_requests", "_request_ip_idx", "ip").Execute(); err != nil {
return err
}
return nil
})
}
+8 -3
View File
@@ -9,8 +9,13 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// DefaultIdLength is the default length of the generated model id. const (
const DefaultIdLength = 15 // DefaultIdLength is the default length of the generated model id.
DefaultIdLength = 15
// DefaultIdAlphabet is the default characters set used for generating the model id.
DefaultIdAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
)
// ColumnValueMapper defines an interface for custom db model data serialization. // ColumnValueMapper defines an interface for custom db model data serialization.
type ColumnValueMapper interface { type ColumnValueMapper interface {
@@ -98,7 +103,7 @@ func (m *BaseModel) GetUpdated() types.DateTime {
// //
// The generated id is a cryptographically random 15 characters length string. // The generated id is a cryptographically random 15 characters length string.
func (m *BaseModel) RefreshId() { func (m *BaseModel) RefreshId() {
m.Id = security.RandomString(DefaultIdLength) m.Id = security.RandomStringWithAlphabet(DefaultIdLength, DefaultIdAlphabet)
} }
// RefreshCreated updates the model Created field with the current datetime. // RefreshCreated updates the model Created field with the current datetime.
+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())
}
}
+2 -1
View File
@@ -18,7 +18,8 @@ type Request struct {
Method string `db:"method" json:"method"` Method string `db:"method" json:"method"`
Status int `db:"status" json:"status"` Status int `db:"status" json:"status"`
Auth string `db:"auth" json:"auth"` Auth string `db:"auth" json:"auth"`
Ip string `db:"ip" json:"ip"` UserIp string `db:"userIp" json:"userIp"`
RemoteIp string `db:"remoteIp" json:"remoteIp"`
Referer string `db:"referer" json:"referer"` Referer string `db:"referer" json:"referer"`
UserAgent string `db:"userAgent" json:"userAgent"` UserAgent string `db:"userAgent" json:"userAgent"`
Meta types.JsonMap `db:"meta" json:"meta"` Meta types.JsonMap `db:"meta" json:"meta"`
+5 -1
View File
@@ -7,6 +7,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is" "github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/types" "github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast" "github.com/spf13/cast"
@@ -458,7 +459,10 @@ func (o FileOptions) Validate() error {
return validation.ValidateStruct(&o, return validation.ValidateStruct(&o,
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)), validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
validation.Field(&o.MaxSize, validation.Required, validation.Min(1)), validation.Field(&o.MaxSize, validation.Required, validation.Min(1)),
validation.Field(&o.Thumbs, validation.Each(validation.Match(regexp.MustCompile(`^[1-9]\d*x[1-9]\d*$`)))), validation.Field(&o.Thumbs, validation.Each(
validation.NotIn("0x0", "0x0t", "0x0b", "0x0f"),
validation.Match(filesystem.ThumbSizeRegex),
)),
) )
} }
+6 -21
View File
@@ -1247,29 +1247,11 @@ func TestFileOptionsValidate(t *testing.T) {
[]string{"thumbs"}, []string{"thumbs"},
}, },
{ {
"invalid thumbs format - zero width", "invalid thumbs format - zero width and height",
schema.FileOptions{ schema.FileOptions{
MaxSize: 1, MaxSize: 1,
MaxSelect: 2, MaxSelect: 2,
Thumbs: []string{"0x100"}, Thumbs: []string{"0x0", "0x0t", "0x0b", "0x0f"},
},
[]string{"thumbs"},
},
{
"invalid thumbs format - zero height",
schema.FileOptions{
MaxSize: 1,
MaxSelect: 2,
Thumbs: []string{"100x0"},
},
[]string{"thumbs"},
},
{
"invalid thumbs format - zero with and height",
schema.FileOptions{
MaxSize: 1,
MaxSelect: 2,
Thumbs: []string{"0x0"},
}, },
[]string{"thumbs"}, []string{"thumbs"},
}, },
@@ -1278,7 +1260,10 @@ func TestFileOptionsValidate(t *testing.T) {
schema.FileOptions{ schema.FileOptions{
MaxSize: 1, MaxSize: 1,
MaxSelect: 2, MaxSelect: 2,
Thumbs: []string{"100x100", "200x100", "1x1"}, Thumbs: []string{
"100x100", "200x100", "0x100", "100x0",
"10x10t", "10x10b", "10x10f",
},
}, },
[]string{}, []string{},
}, },
+3
View File
@@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}
+76 -71
View File
@@ -26,50 +26,60 @@ type appWrapper struct {
// PocketBase defines a PocketBase app launcher. // 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()). // could be accessed directly through the instance (eg. PocketBase.DataDir()).
type PocketBase struct { type PocketBase struct {
*appWrapper *appWrapper
// RootCmd is the main cli command debugFlag bool
dataDirFlag string
encryptionEnvFlag string
hideStartBanner bool
// RootCmd is the main console command
RootCmd *cobra.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, // Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible. // aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when Start() is executed. // Everything will be initialized when [Start()] is executed.
// If you want to initialize the application before calling Start(), // If you want to initialize the application before calling [Start()],
// then you'll have to manually call Bootstrap(). // then you'll have to manually call [Bootstrap()].
func New() *PocketBase { func New() *PocketBase {
// try to find the base executable directory and how it was run _, isUsingGoRun := inspectRuntime()
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])
}
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{ pb := &PocketBase{
RootCmd: &cobra.Command{ RootCmd: &cobra.Command{
@@ -84,46 +94,31 @@ func New() *PocketBase {
DisableDefaultCmd: true, DisableDefaultCmd: true,
}, },
}, },
defaultDebug: withGoRun, debugFlag: config.DefaultDebug,
defaultDataDir: defaultDir, dataDirFlag: config.DefaultDataDir,
defaultEncryptionEnv: "", encryptionEnvFlag: config.DefaultEncryptionEnv,
showStartBanner: true, hideStartBanner: config.HideStartBanner,
} }
// parse base flags // parse base flags
// (errors are ignored, since the full flags parsing happens on Execute()) // (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.appWrapper = &appWrapper{core.NewBaseApp(
pb.dataDirFlag, pb.dataDirFlag,
pb.encryptionEnv, pb.encryptionEnvFlag,
pb.debugFlag, 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. // hook the bootstrap process
func (pb *PocketBase) DefaultDebug(val bool) *PocketBase { pb.RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
pb.defaultDebug = val return pb.Bootstrap()
return pb }
}
// 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 return pb
} }
@@ -131,7 +126,7 @@ func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
// commands (serve, migrate, version) and executes pb.RootCmd. // commands (serve, migrate, version) and executes pb.RootCmd.
func (pb *PocketBase) Start() error { func (pb *PocketBase) Start() error {
// register system commands // 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)) pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb))
return pb.Execute() return pb.Execute()
@@ -143,10 +138,6 @@ func (pb *PocketBase) Start() error {
// This method differs from pb.Start() by not registering the default // This method differs from pb.Start() by not registering the default
// system commands! // system commands!
func (pb *PocketBase) Execute() error { func (pb *PocketBase) Execute() error {
if err := pb.Bootstrap(); err != nil {
return err
}
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
@@ -180,27 +171,41 @@ func (pb *PocketBase) onTerminate() error {
// eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute(). // eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute().
// so we can have all PocketBase flags ready for use on initialization. // 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.RootCmd.PersistentFlags().StringVar(
&pb.dataDirFlag, &pb.dataDirFlag,
"dir", "dir",
pb.defaultDataDir, config.DefaultDataDir,
"the PocketBase data directory", "the PocketBase data directory",
) )
pb.RootCmd.PersistentFlags().StringVar( pb.RootCmd.PersistentFlags().StringVar(
&pb.encryptionEnv, &pb.encryptionEnvFlag,
"encryptionEnv", "encryptionEnv",
pb.defaultEncryptionEnv, config.DefaultEncryptionEnv,
"the env variable whose value of 32 chars will be used \nas encryption key for the app settings (default none)", "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.RootCmd.PersistentFlags().BoolVar(
&pb.debugFlag, &pb.debugFlag,
"debug", "debug",
pb.defaultDebug, config.DefaultDebug,
"enable debug mode, aka. showing more detailed logs", "enable debug mode, aka. showing more detailed logs",
) )
return pb.RootCmd.ParseFlags(os.Args[1:]) 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) { func TestNew(t *testing.T) {
testDir := "./pb_test_data_dir" // copy os.Args
defer os.RemoveAll(testDir) 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 = os.Args[0:1]
os.Args = append( os.Args = append(
os.Args, os.Args,
"--dir="+testDir, "--dir=test_dir",
"--encryptionEnv=test_encryption_env", "--encryptionEnv=test_encryption_env",
"--debug=true", "--debug=true",
) )
@@ -32,8 +37,8 @@ func TestNew(t *testing.T) {
t.Fatal("Expected appWrapper to be initialized, got nil") t.Fatal("Expected appWrapper to be initialized, got nil")
} }
if app.DataDir() != testDir { if app.DataDir() != "test_dir" {
t.Fatalf("Expected app.DataDir() %q, got %q", testDir, app.DataDir()) t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir", app.DataDir())
} }
if app.EncryptionEnv() != "test_encryption_env" { if app.EncryptionEnv() != "test_encryption_env" {
@@ -45,52 +50,93 @@ func TestNew(t *testing.T) {
} }
} }
func TestDefaultDebug(t *testing.T) { func TestNewWithConfig(t *testing.T) {
app := New() app := NewWithConfig(Config{
DefaultDebug: true,
DefaultDataDir: "test_dir",
DefaultEncryptionEnv: "test_encryption_env",
HideStartBanner: true,
})
app.DefaultDebug(true) if app == nil {
if app.defaultDebug != true { t.Fatal("Expected initialized PocketBase instance, got nil")
t.Fatalf("Expected defaultDebug %v, got %v", true, app.defaultDebug)
} }
app.DefaultDebug(false) if app.RootCmd == nil {
if app.defaultDebug != false { t.Fatal("Expected RootCmd to be initialized, got nil")
t.Fatalf("Expected defaultDebug %v, got %v", false, app.defaultDebug) }
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) { func TestNewWithConfigAndFlags(t *testing.T) {
app := New() // 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) app := NewWithConfig(Config{
if app.defaultDataDir != expected { DefaultDebug: true,
t.Fatalf("Expected defaultDataDir %v, got %v", expected, app.defaultDataDir) 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) { if app.RootCmd == nil {
app := New() 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.hideStartBanner != true {
if app.defaultEncryptionEnv != expected { t.Fatal("Expected app.hideStartBanner to be true, got false")
t.Fatalf("Expected defaultEncryptionEnv %v, got %v", expected, app.defaultEncryptionEnv)
} }
}
func TestShowStartBanner(t *testing.T) { if app.DataDir() != "test_dir_flag" {
app := New() t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir_flag", app.DataDir())
}
app.ShowStartBanner(true) if app.EncryptionEnv() != "test_encryption_env_flag" {
if app.showStartBanner != true { t.Fatalf("Expected app.DataDir() %q, got %q", "test_encryption_env_flag", app.EncryptionEnv())
t.Fatalf("Expected showStartBanner %v, got %v", true, app.showStartBanner)
} }
app.ShowStartBanner(false) if app.IsDebug() != false {
if app.showStartBanner != false { t.Fatal("Expected app.IsDebug() false, got true")
t.Fatalf("Expected showStartBanner %v, got %v", false, app.showStartBanner)
} }
} }
+17 -7
View File
@@ -36,13 +36,23 @@ type ApiScenario struct {
// test hooks // test hooks
// --- // ---
BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo) TestAppFactory func() (*TestApp, error)
AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo) 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. // Test executes the test case/scenario.
func (scenario *ApiScenario) Test(t *testing.T) { 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() defer testApp.Cleanup()
e, err := apis.InitApi(testApp) e, err := apis.InitApi(testApp)
@@ -50,8 +60,8 @@ func (scenario *ApiScenario) Test(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if scenario.BeforeFunc != nil { if scenario.BeforeTestFunc != nil {
scenario.BeforeFunc(t, testApp, e) scenario.BeforeTestFunc(t, testApp, e)
} }
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@@ -135,7 +145,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
} }
} }
if scenario.AfterFunc != nil { if scenario.AfterTestFunc != nil {
scenario.AfterFunc(t, testApp, e) scenario.AfterTestFunc(t, testApp, e)
} }
} }
+33 -21
View File
@@ -45,12 +45,21 @@ func (t *TestApp) ResetEventCalls() {
t.EventCalls = make(map[string]int) 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()` // It is the caller's responsibility to call `app.Cleanup()`
// when the app is no longer needed. // when the app is no longer needed.
func NewTestApp() (*TestApp, error) { func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
tempDir, err := NewTempDataDir() 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 { if err != nil {
return nil, err return nil, err
} }
@@ -188,21 +197,26 @@ func NewTestApp() (*TestApp, error) {
return nil 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.OnUserAuthRequest().Add(func(e *core.UserAuthEvent) error {
t.EventCalls["OnUserAuthRequest"]++ t.EventCalls["OnUserAuthRequest"]++
return nil 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.OnMailerBeforeAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error {
t.EventCalls["OnMailerBeforeAdminResetPasswordSend"]++ t.EventCalls["OnMailerBeforeAdminResetPasswordSend"]++
return nil return nil
@@ -376,21 +390,19 @@ func NewTestApp() (*TestApp, error) {
return t, nil 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)` // It is the caller's responsibility to call `os.RemoveAll(tempDir)`
// when the directory is no longer needed. // when the directory is no longer needed!
func NewTempDataDir() (string, error) { func TempDirClone(dirToClone string) (string, error) {
tempDir, err := os.MkdirTemp("", "pb_test_*") tempDir, err := os.MkdirTemp("", "pb_test_*")
if err != nil { if err != nil {
return "", err return "", err
} }
_, currentFile, _, _ := runtime.Caller(0)
testDataDir := filepath.Join(path.Dir(currentFile), "data")
// copy everything from testDataDir to tempDir // copy everything from testDataDir to tempDir
if err := copyDir(testDataDir, tempDir); err != nil { if err := copyDir(dirToClone, tempDir); err != nil {
return "", err return "", err
} }
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"UlDqO9tdyG/wZHge4djvfg=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"eVVDssRj5yVxpXa/D13Oxg=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"b+ZUvKPaIXEEHnFZW1BaAQ=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"DHDmiDWZfc+R37OBSxmx6A=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"UlDqO9tdyG/wZHge4djvfg=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"s3qk32WZOj9nnnteIt8Gew=="}
+4 -1
View File
@@ -10,7 +10,8 @@ func MockRequestLogsData(app *TestApp) error {
[[method]], [[method]],
[[status]], [[status]],
[[auth]], [[auth]],
[[ip]], [[userIp]],
[[remoteIp]],
[[referer]], [[referer]],
[[userAgent]], [[userAgent]],
[[meta]], [[meta]],
@@ -25,6 +26,7 @@ func MockRequestLogsData(app *TestApp) error {
200, 200,
"guest", "guest",
"127.0.0.1", "127.0.0.1",
"127.0.0.1",
"", "",
"", "",
"{}", "{}",
@@ -38,6 +40,7 @@ func MockRequestLogsData(app *TestApp) error {
400, 400,
"admin", "admin",
"127.0.0.1", "127.0.0.1",
"127.0.0.1",
"", "",
"", "",
'{"errorDetails":"error_details..."}', '{"errorDetails":"error_details..."}',
+12 -3
View File
@@ -11,18 +11,27 @@ var _ mailer.Mailer = (*TestMailer)(nil)
// TestMailer is a mock `mailer.Mailer` implementation. // TestMailer is a mock `mailer.Mailer` implementation.
type TestMailer struct { type TestMailer struct {
TotalSend int TotalSend int
LastHtmlBody string LastFromAddress mail.Address
LastToAddress mail.Address
LastHtmlSubject string
LastHtmlBody string
} }
// Reset clears any previously test collected data. // Reset clears any previously test collected data.
func (m *TestMailer) Reset() { func (m *TestMailer) Reset() {
m.LastHtmlBody = ""
m.TotalSend = 0 m.TotalSend = 0
m.LastFromAddress = mail.Address{}
m.LastToAddress = mail.Address{}
m.LastHtmlSubject = ""
m.LastHtmlBody = ""
} }
// Send implements `mailer.Mailer` interface. // Send implements `mailer.Mailer` interface.
func (m *TestMailer) Send(fromEmail mail.Address, toEmail mail.Address, subject string, html string, attachments map[string]io.Reader) error { func (m *TestMailer) Send(fromEmail mail.Address, toEmail mail.Address, subject string, html string, attachments map[string]io.Reader) error {
m.LastFromAddress = fromEmail
m.LastToAddress = toEmail
m.LastHtmlSubject = subject
m.LastHtmlBody = html m.LastHtmlBody = html
m.TotalSend++ m.TotalSend++
return nil return nil
+5
View File
@@ -11,6 +11,7 @@ import (
type AuthUser struct { type AuthUser struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"` AvatarUrl string `json:"avatarUrl"`
} }
@@ -90,6 +91,10 @@ func NewProviderByName(name string) (Provider, error) {
return NewGithubProvider(), nil return NewGithubProvider(), nil
case NameGitlab: case NameGitlab:
return NewGitlabProvider(), nil return NewGitlabProvider(), nil
case NameDiscord:
return NewDiscordProvider(), nil
case NameTwitter:
return NewTwitterProvider(), nil
default: default:
return nil, errors.New("Missing provider " + name) 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 { if _, ok := p.(*auth.Gitlab); !ok {
t.Error("Expected to be instance of *auth.Gitlab") 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) { func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://docs.github.com/en/rest/reference/users#get-the-authenticated-user // https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
rawData := struct { rawData := struct {
Login string `json:"login"`
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
@@ -45,6 +46,7 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(rawData.Id), Id: strconv.Itoa(rawData.Id),
Name: rawData.Name, Name: rawData.Name,
Username: rawData.Login,
Email: rawData.Email, Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl, AvatarUrl: rawData.AvatarUrl,
} }
+2
View File
@@ -32,6 +32,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
rawData := struct { rawData := struct {
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
AvatarUrl string `json:"avatar_url"` AvatarUrl string `json:"avatar_url"`
}{} }{}
@@ -43,6 +44,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(rawData.Id), Id: strconv.Itoa(rawData.Id),
Name: rawData.Name, Name: rawData.Name,
Username: rawData.Username,
Email: rawData.Email, Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl, AvatarUrl: rawData.AvatarUrl,
} }
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more