mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-20 13:30:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be8f3f3cb | ||
|
|
00fd007d50 | ||
|
|
96bfc69c8e | ||
|
|
e542b0d8c5 | ||
|
|
9814dda8e4 | ||
|
|
978fdd3ce7 | ||
|
|
9cf89870e7 | ||
|
|
b8c54568e3 | ||
|
|
843bbf99cc | ||
|
|
daffb0f86e | ||
|
|
1f5c3328e5 | ||
|
|
ed5f3b86f5 | ||
|
|
1ba2d14231 | ||
|
|
6cda610ede | ||
|
|
2fa5233fa6 | ||
|
|
030dfc2690 | ||
|
|
8c11e2ef01 | ||
|
|
4b64e0910b | ||
|
|
4a6bc453de | ||
|
|
96d09a30c4 | ||
|
|
a0d7f23d77 | ||
|
|
74108d84ca | ||
|
|
4bc28abac4 | ||
|
|
b79a7982bf | ||
|
|
b717896232 | ||
|
|
9d53aec9ee | ||
|
|
9d30ca81cb | ||
|
|
93b3788448 | ||
|
|
0ac4c9e1fc | ||
|
|
df1a947b61 | ||
|
|
06a7f1af05 | ||
|
|
07ac5bf6a2 | ||
|
|
f56c52a1f7 | ||
|
|
f0b57c6b91 | ||
|
|
f61d0ec6f7 | ||
|
|
4e78f1e4ad | ||
|
|
235670fae8 | ||
|
|
f5ff7193a9 | ||
|
|
9fe94f5c7d | ||
|
|
0f9ddbf7ec | ||
|
|
f14105b04a | ||
|
|
3bfa0b6dd4 | ||
|
|
9e3c59f966 | ||
|
|
a908d20dcd | ||
|
|
49b084cf50 | ||
|
|
0b8c7f6883 | ||
|
|
587cfc335c | ||
|
|
3f4f4cf031 | ||
|
|
d4f160d959 | ||
|
|
07cd758112 | ||
|
|
beb8e7924d | ||
|
|
72fdf0d116 | ||
|
|
7e14ea7cfb | ||
|
|
25c4db7a30 | ||
|
|
e86b1f54a0 | ||
|
|
7be389704d | ||
|
|
cfaff31d97 | ||
|
|
9bf66be870 | ||
|
|
efda3d5a0b | ||
|
|
a516435f2e | ||
|
|
ccd010c490 | ||
|
|
456ced75ce | ||
|
|
b7d32c23aa | ||
|
|
04ddae46c9 | ||
|
|
7d10d20de1 | ||
|
|
1de56d3d9e | ||
|
|
80f710731c |
@@ -34,7 +34,7 @@ The easiest way to interact with the API is to use one of the official SDK clien
|
||||
|
||||
## Overview
|
||||
|
||||
PocketBase could be used as a standalone app or as a Go framework/toolkit that enables you to build
|
||||
PocketBase could be [downloaded directly as a standalone app](https://github.com/pocketbase/pocketbase/releases) or it could be used as a Go framework/toolkit which allows you to build
|
||||
your own custom app specific business logic and still have a single portable executable at the end.
|
||||
|
||||
### Installation
|
||||
@@ -121,7 +121,11 @@ PocketBase is free and open source project licensed under the [MIT License](LICE
|
||||
You could help continuing its development by:
|
||||
|
||||
- [Contribute to the source code](CONTRIBUTING.md)
|
||||
|
||||
> **NB! PR for new features will not be merged until we manage to complete the users refactoring in [#376](https://github.com/pocketbase/pocketbase/issues/376)!**
|
||||
|
||||
- [Suggest new features and report issues](https://github.com/pocketbase/pocketbase/issues)
|
||||
|
||||
- [Donate a small amount](https://pocketbase.io/support-us)
|
||||
|
||||
> Please note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)),
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ func (api *adminApi) refresh(c echo.Context) error {
|
||||
func (api *adminApi) emailAuth(c echo.Context) error {
|
||||
form := forms.NewAdminLogin(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
admin, submitErr := form.Submit()
|
||||
@@ -80,7 +80,7 @@ func (api *adminApi) emailAuth(c echo.Context) error {
|
||||
func (api *adminApi) requestPasswordReset(c echo.Context) error {
|
||||
form := forms.NewAdminPasswordResetRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error 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 {
|
||||
@@ -101,7 +101,7 @@ func (api *adminApi) requestPasswordReset(c echo.Context) error {
|
||||
func (api *adminApi) confirmPasswordReset(c echo.Context) error {
|
||||
form := forms.NewAdminPasswordResetConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
admin, submitErr := form.Submit()
|
||||
|
||||
+3
-3
@@ -119,7 +119,7 @@ func TestAdminRequestPasswordReset(t *testing.T) {
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent password request
|
||||
admin, err := app.Dao().FindAdminByEmail("test@example.com")
|
||||
if err != nil {
|
||||
@@ -446,7 +446,7 @@ func TestAdminDelete(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// delete all admins except the authorized one
|
||||
adminModel := &models.Admin{}
|
||||
_, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{
|
||||
@@ -483,7 +483,7 @@ func TestAdminCreate(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins",
|
||||
Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":3}`),
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// delete all admins
|
||||
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
|
||||
if err != nil {
|
||||
|
||||
+4
-4
@@ -54,7 +54,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
Name: "custom route",
|
||||
Method: http.MethodGet,
|
||||
Url: "/custom",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/custom",
|
||||
@@ -70,7 +70,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
Name: "route with HTTPError",
|
||||
Method: http.MethodGet,
|
||||
Url: "/http-error",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/http-error",
|
||||
@@ -86,7 +86,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
Name: "route with api error",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api-error",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api-error",
|
||||
@@ -102,7 +102,7 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
Name: "route with plain error",
|
||||
Method: http.MethodGet,
|
||||
Url: "/plain-error",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/plain-error",
|
||||
|
||||
@@ -475,7 +475,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
`"data":{`,
|
||||
`"collections":{"code":"validation_required"`,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -501,7 +501,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionsBeforeImportRequest": 1,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -542,7 +542,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
"OnCollectionsBeforeImportRequest": 1,
|
||||
"OnModelBeforeCreate": 2,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -590,7 +590,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
"OnModelBeforeCreate": 2,
|
||||
"OnModelAfterCreate": 2,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -689,7 +689,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
"OnModelBeforeCreate": 1,
|
||||
"OnModelAfterCreate": 1,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+1
-1
@@ -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
|
||||
tAttrs, tAttrsErr := fs.Attributes(servedPath)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+85
-5
@@ -16,7 +16,12 @@ func TestFileDownload(t *testing.T) {
|
||||
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")
|
||||
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)
|
||||
if fileErr != nil {
|
||||
@@ -28,7 +33,32 @@ func TestFileDownload(t *testing.T) {
|
||||
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 {
|
||||
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,
|
||||
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,
|
||||
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{
|
||||
"OnFileDownloadRequest": 1,
|
||||
},
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ type logsApi struct {
|
||||
var requestFilterFields = []string{
|
||||
"rowid", "id", "created", "updated",
|
||||
"url", "method", "status", "auth",
|
||||
"ip", "referer", "userAgent",
|
||||
"remoteIp", "userIp", "referer", "userAgent",
|
||||
}
|
||||
|
||||
func (api *logsApi) requestsList(c echo.Context) error {
|
||||
|
||||
+6
-6
@@ -34,7 +34,7 @@ func TestRequestsList(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func TestRequestsList(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func TestRequestView(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestRequestView(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func TestRequestsStats(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func TestRequestsStats(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := tests.MockRequestLogsData(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+31
-1
@@ -3,6 +3,7 @@ package apis
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -247,12 +248,15 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
|
||||
requestAuth = models.RequestAuthAdmin
|
||||
}
|
||||
|
||||
ip, _, _ := net.SplitHostPort(httpRequest.RemoteAddr)
|
||||
|
||||
model := &models.Request{
|
||||
Url: httpRequest.URL.RequestURI(),
|
||||
Method: strings.ToLower(httpRequest.Method),
|
||||
Status: status,
|
||||
Auth: requestAuth,
|
||||
Ip: httpRequest.RemoteAddr,
|
||||
UserIp: realUserIp(httpRequest, ip),
|
||||
RemoteIp: ip,
|
||||
Referer: httpRequest.Referer(),
|
||||
UserAgent: httpRequest.UserAgent(),
|
||||
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
@@ -18,7 +18,7 @@ func TestRequireGuestOnly(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -40,7 +40,7 @@ func TestRequireGuestOnly(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -62,7 +62,7 @@ func TestRequireGuestOnly(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -81,7 +81,7 @@ func TestRequireGuestOnly(t *testing.T) {
|
||||
Name: "guest",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -109,7 +109,7 @@ func TestRequireUserAuth(t *testing.T) {
|
||||
Name: "guest",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -131,7 +131,7 @@ func TestRequireUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -153,7 +153,7 @@ func TestRequireUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -175,7 +175,7 @@ func TestRequireUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -203,7 +203,7 @@ func TestRequireAdminAuth(t *testing.T) {
|
||||
Name: "guest",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -225,7 +225,7 @@ func TestRequireAdminAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -247,7 +247,7 @@ func TestRequireAdminAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -269,7 +269,7 @@ func TestRequireAdminAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -297,7 +297,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
Name: "guest (while having at least 1 existing admin)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -316,7 +316,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
Name: "guest (while having 0 existing admins)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// delete all admins
|
||||
_, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute()
|
||||
if err != nil {
|
||||
@@ -344,7 +344,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -366,7 +366,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -388,7 +388,7 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -416,7 +416,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
|
||||
Name: "guest",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -438,7 +438,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -460,7 +460,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -482,7 +482,7 @@ func TestRequireAdminOrUserAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test",
|
||||
@@ -510,7 +510,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
|
||||
Name: "guest",
|
||||
Method: http.MethodGet,
|
||||
Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test/:id",
|
||||
@@ -532,7 +532,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test/:id",
|
||||
@@ -555,7 +555,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
|
||||
// test3@example.com
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test/:id",
|
||||
@@ -577,7 +577,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test/:id",
|
||||
@@ -599,7 +599,7 @@ func TestRequireAdminOrOwnerAuth(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/my/test/:custom",
|
||||
|
||||
@@ -48,6 +48,9 @@ func (api *realtimeApi) connect(c echo.Context) error {
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream; charset=UTF-8")
|
||||
c.Response().Header().Set("Cache-Control", "no-store")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
// https://github.com/pocketbase/pocketbase/discussions/480#discussioncomment-3657640
|
||||
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
||||
c.Response().Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
event := &core.RealtimeConnectEvent{
|
||||
HttpContext: c,
|
||||
|
||||
+11
-11
@@ -27,7 +27,7 @@ func TestRealtimeConnect(t *testing.T) {
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRealtimeConnectRequest": 1,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if len(app.SubscriptionsBroker().Clients()) != 0 {
|
||||
t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients()))
|
||||
}
|
||||
@@ -68,11 +68,11 @@ func TestRealtimeSubscribe(t *testing.T) {
|
||||
"OnRealtimeBeforeSubscribeRequest": 1,
|
||||
"OnRealtimeAfterSubscribeRequest": 1,
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
client.Subscribe("test0")
|
||||
app.SubscriptionsBroker().Register(client)
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if len(client.Subscriptions()) != 0 {
|
||||
t.Errorf("Expected no subscriptions, got %v", client.Subscriptions())
|
||||
}
|
||||
@@ -89,11 +89,11 @@ func TestRealtimeSubscribe(t *testing.T) {
|
||||
"OnRealtimeBeforeSubscribeRequest": 1,
|
||||
"OnRealtimeAfterSubscribeRequest": 1,
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
client.Subscribe("test0")
|
||||
app.SubscriptionsBroker().Register(client)
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
expectedSubs := []string{"test1", "test2"}
|
||||
if len(expectedSubs) != len(client.Subscriptions()) {
|
||||
t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions())
|
||||
@@ -120,10 +120,10 @@ func TestRealtimeSubscribe(t *testing.T) {
|
||||
"OnRealtimeBeforeSubscribeRequest": 1,
|
||||
"OnRealtimeAfterSubscribeRequest": 1,
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
app.SubscriptionsBroker().Register(client)
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
admin, _ := client.Get(apis.ContextAdminKey).(*models.Admin)
|
||||
if admin == nil {
|
||||
t.Errorf("Expected admin auth model, got nil")
|
||||
@@ -144,10 +144,10 @@ func TestRealtimeSubscribe(t *testing.T) {
|
||||
"OnRealtimeBeforeSubscribeRequest": 1,
|
||||
"OnRealtimeAfterSubscribeRequest": 1,
|
||||
},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
app.SubscriptionsBroker().Register(client)
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
user, _ := client.Get(apis.ContextUserKey).(*models.User)
|
||||
if user == nil {
|
||||
t.Errorf("Expected user auth model, got nil")
|
||||
@@ -165,14 +165,14 @@ func TestRealtimeSubscribe(t *testing.T) {
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
initialAuth := &models.User{}
|
||||
initialAuth.RefreshId()
|
||||
client.Set(apis.ContextUserKey, initialAuth)
|
||||
|
||||
app.SubscriptionsBroker().Register(client)
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
user, _ := client.Get(apis.ContextUserKey).(*models.User)
|
||||
if user == nil {
|
||||
t.Errorf("Expected user auth model, got nil")
|
||||
|
||||
+5
-5
@@ -459,7 +459,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnModelBeforeDelete": 3, // +2 cascade delete related records
|
||||
"OnModelAfterDelete": 3, // +2 cascade delete related records
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
|
||||
@@ -481,7 +481,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnModelBeforeDelete": 3, // +2 cascade delete related records
|
||||
"OnModelAfterDelete": 3, // +2 cascade delete related records
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
|
||||
@@ -501,7 +501,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnModelBeforeDelete": 1,
|
||||
"OnModelAfterDelete": 1,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
|
||||
},
|
||||
},
|
||||
@@ -529,7 +529,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnModelBeforeDelete": 1,
|
||||
"OnModelAfterDelete": 1,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
|
||||
},
|
||||
},
|
||||
@@ -562,7 +562,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnModelBeforeDelete": 3,
|
||||
"OnModelAfterDelete": 3,
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
recId := "63c2ab80-84ab-4057-a592-4604a731f78f"
|
||||
col, _ := app.Dao().FindCollectionByNameOrId("demo2")
|
||||
rec, _ := app.Dao().FindRecordById(col, recId, nil)
|
||||
|
||||
+51
-1
@@ -3,10 +3,12 @@ package apis
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// 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.GET("", api.list)
|
||||
subGroup.PATCH("", api.set)
|
||||
subGroup.POST("/test/s3", api.testS3)
|
||||
subGroup.POST("/test/email", api.testEmail)
|
||||
}
|
||||
|
||||
type settingsApi struct {
|
||||
@@ -43,7 +47,7 @@ func (api *settingsApi) set(c echo.Context) error {
|
||||
|
||||
// load request
|
||||
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{
|
||||
@@ -76,3 +80,49 @@ func (api *settingsApi) set(c echo.Context) error {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,7 @@ func TestSettingsList(t *testing.T) {
|
||||
`"facebookAuth":{`,
|
||||
`"githubAuth":{`,
|
||||
`"gitlabAuth":{`,
|
||||
`"discordAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
},
|
||||
@@ -113,6 +115,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"facebookAuth":{`,
|
||||
`"githubAuth":{`,
|
||||
`"gitlabAuth":{`,
|
||||
`"discordAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"Acme"`,
|
||||
@@ -165,6 +168,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"facebookAuth":{`,
|
||||
`"githubAuth":{`,
|
||||
`"gitlabAuth":{`,
|
||||
`"discordAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"update_test"`,
|
||||
@@ -183,3 +187,192 @@ func TestSettingsSet(t *testing.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
@@ -38,6 +38,8 @@ func BindUserApi(app core.App, rg *echo.Group) {
|
||||
subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id"))
|
||||
subGroup.PATCH("/:id", api.update, RequireAdminAuth())
|
||||
subGroup.DELETE("/:id", api.delete, RequireAdminOrOwnerAuth("id"))
|
||||
subGroup.GET("/:id/external-auths", api.listExternalAuths, RequireAdminOrOwnerAuth("id"))
|
||||
subGroup.DELETE("/:id/external-auths/:provider", api.unlinkExternalAuth, RequireAdminOrOwnerAuth("id"))
|
||||
}
|
||||
|
||||
type userApi struct {
|
||||
@@ -129,7 +131,7 @@ func (api *userApi) authMethods(c echo.Context) error {
|
||||
}
|
||||
|
||||
state := security.RandomString(30)
|
||||
codeVerifier := security.RandomString(30)
|
||||
codeVerifier := security.RandomString(43)
|
||||
codeChallenge := security.S256Challenge(codeVerifier)
|
||||
codeChallengeMethod := "S256"
|
||||
result.AuthProviders = append(result.AuthProviders, providerInfo{
|
||||
@@ -152,7 +154,7 @@ func (api *userApi) authMethods(c echo.Context) error {
|
||||
func (api *userApi) oauth2Auth(c echo.Context) error {
|
||||
form := forms.NewUserOauth2Login(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, authData, submitErr := form.Submit()
|
||||
@@ -170,7 +172,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
|
||||
|
||||
form := forms.NewUserEmailLogin(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -184,7 +186,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
|
||||
func (api *userApi) requestPasswordReset(c echo.Context) error {
|
||||
form := forms.NewUserPasswordResetRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error 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 {
|
||||
@@ -205,7 +207,7 @@ func (api *userApi) requestPasswordReset(c echo.Context) error {
|
||||
func (api *userApi) confirmPasswordReset(c echo.Context) error {
|
||||
form := forms.NewUserPasswordResetConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -224,7 +226,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
|
||||
|
||||
form := forms.NewUserEmailChangeRequest(api.app, loggedUser)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error 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 {
|
||||
@@ -237,7 +239,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
|
||||
func (api *userApi) confirmEmailChange(c echo.Context) error {
|
||||
form := forms.NewUserEmailChangeConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -251,7 +253,7 @@ func (api *userApi) confirmEmailChange(c echo.Context) error {
|
||||
func (api *userApi) requestVerification(c echo.Context) error {
|
||||
form := forms.NewUserVerificationRequest(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return rest.NewBadRequestError("An error 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 {
|
||||
@@ -272,7 +274,7 @@ func (api *userApi) requestVerification(c echo.Context) error {
|
||||
func (api *userApi) confirmVerification(c echo.Context) error {
|
||||
form := forms.NewUserVerificationConfirm(api.app)
|
||||
if readErr := c.Bind(form); readErr != nil {
|
||||
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
|
||||
return rest.NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
user, submitErr := form.Submit()
|
||||
@@ -450,3 +452,68 @@ func (api *userApi) delete(c echo.Context) error {
|
||||
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
func (api *userApi) listExternalAuths(c echo.Context) error {
|
||||
id := c.PathParam("id")
|
||||
if id == "" {
|
||||
return rest.NewNotFoundError("", nil)
|
||||
}
|
||||
|
||||
user, err := api.app.Dao().FindUserById(id)
|
||||
if err != nil || user == nil {
|
||||
return rest.NewNotFoundError("", err)
|
||||
}
|
||||
|
||||
externalAuths, err := api.app.Dao().FindAllExternalAuthsByUserId(user.Id)
|
||||
if err != nil {
|
||||
return rest.NewBadRequestError("Failed to fetch the external auths for the specified user.", err)
|
||||
}
|
||||
|
||||
event := &core.UserListExternalAuthsEvent{
|
||||
HttpContext: c,
|
||||
User: user,
|
||||
ExternalAuths: externalAuths,
|
||||
}
|
||||
|
||||
return api.app.OnUserListExternalAuths().Trigger(event, func(e *core.UserListExternalAuthsEvent) error {
|
||||
return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths)
|
||||
})
|
||||
}
|
||||
|
||||
func (api *userApi) unlinkExternalAuth(c echo.Context) error {
|
||||
id := c.PathParam("id")
|
||||
provider := c.PathParam("provider")
|
||||
if id == "" || provider == "" {
|
||||
return rest.NewNotFoundError("", nil)
|
||||
}
|
||||
|
||||
user, err := api.app.Dao().FindUserById(id)
|
||||
if err != nil || user == nil {
|
||||
return rest.NewNotFoundError("", err)
|
||||
}
|
||||
|
||||
externalAuth, err := api.app.Dao().FindExternalAuthByUserIdAndProvider(user.Id, provider)
|
||||
if err != nil {
|
||||
return rest.NewNotFoundError("Missing external auth provider relation.", err)
|
||||
}
|
||||
|
||||
event := &core.UserUnlinkExternalAuthEvent{
|
||||
HttpContext: c,
|
||||
User: user,
|
||||
ExternalAuth: externalAuth,
|
||||
}
|
||||
|
||||
handlerErr := api.app.OnUserBeforeUnlinkExternalAuthRequest().Trigger(event, func(e *core.UserUnlinkExternalAuthEvent) error {
|
||||
if err := api.app.Dao().DeleteExternalAuth(externalAuth); err != nil {
|
||||
return rest.NewBadRequestError("Cannot unlink the external auth provider. Make sure that the user has other linked auth providers OR has an email address.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.NoContent(http.StatusNoContent)
|
||||
})
|
||||
|
||||
if handlerErr == nil {
|
||||
api.app.OnUserAfterUnlinkExternalAuthRequest().Trigger(event)
|
||||
}
|
||||
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
+192
-7
@@ -84,7 +84,7 @@ func TestUserEmailAuth(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/auth-via-email",
|
||||
Body: strings.NewReader(`{"email":"test@example.com","password":"123456"}`),
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
app.Settings().EmailAuth.Enabled = false
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
@@ -159,7 +159,7 @@ func TestUserRequestPasswordReset(t *testing.T) {
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent password request
|
||||
user, err := app.Dao().FindUserByEmail("test@example.com")
|
||||
if err != nil {
|
||||
@@ -285,7 +285,7 @@ func TestUserRequestVerification(t *testing.T) {
|
||||
Body: strings.NewReader(`{"email":"test2@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent verification sent
|
||||
user, err := app.Dao().FindUserByEmail("test2@example.com")
|
||||
if err != nil {
|
||||
@@ -584,11 +584,12 @@ func TestUsersList(t *testing.T) {
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalItems":3`,
|
||||
`"totalItems":4`,
|
||||
`"items":[{`,
|
||||
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
|
||||
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
|
||||
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
|
||||
`"id":"cx9u0dh2udo8xol"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
|
||||
},
|
||||
@@ -603,8 +604,9 @@ func TestUsersList(t *testing.T) {
|
||||
ExpectedContent: []string{
|
||||
`"page":2`,
|
||||
`"perPage":2`,
|
||||
`"totalItems":3`,
|
||||
`"totalItems":4`,
|
||||
`"items":[{`,
|
||||
`"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`,
|
||||
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
|
||||
@@ -630,10 +632,11 @@ func TestUsersList(t *testing.T) {
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalItems":2`,
|
||||
`"totalItems":3`,
|
||||
`"items":[{`,
|
||||
`"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`,
|
||||
`"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
|
||||
`"id":"cx9u0dh2udo8xol"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUsersListRequest": 1},
|
||||
},
|
||||
@@ -804,7 +807,7 @@ func TestUserCreate(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users",
|
||||
Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`),
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
app.Settings().EmailAuth.Enabled = false
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
@@ -926,3 +929,185 @@ func TestUserUpdate(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserListExternalsAuths(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + nonexisting user id",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/000000000000000/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + existing user id and no external auths",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/97cc3d3d-6ba2-383f-b42a-7bc84d27410c/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`[]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin + existing user id and 2 external auths",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"abcdefghijklmn1"`,
|
||||
`"id":"abcdefghijklmn0"`,
|
||||
`"userId":"cx9u0dh2udo8xol"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user - trying to list another user external auths",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user - owner without external auths",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`[]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user - owner with 2 external auths",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImN4OXUwZGgydWRvOHhvbCIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.NgFYG2D7PftFW1tcfe5E2oDi_AVakDR9J6WI6VUZQfw",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"abcdefghijklmn1"`,
|
||||
`"id":"abcdefghijklmn0"`,
|
||||
`"userId":"cx9u0dh2udo8xol"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnUserListExternalAuths": 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserUnlinkExternalsAuth(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin - nonexisting user id",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/000000000000000/external-auths/google",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin - nonexisting provider",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths/facebook",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin - existing provider",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
ExpectedContent: []string{},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterDelete": 1,
|
||||
"OnModelBeforeDelete": 1,
|
||||
"OnUserAfterUnlinkExternalAuthRequest": 1,
|
||||
"OnUserBeforeUnlinkExternalAuthRequest": 1,
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
auth, _ := app.Dao().FindExternalAuthByUserIdAndProvider("cx9u0dh2udo8xol", "google")
|
||||
if auth != nil {
|
||||
t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user - trying to unlink another user external auth",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as user - owner with existing external auth",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/users/cx9u0dh2udo8xol/external-auths/google",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImN4OXUwZGgydWRvOHhvbCIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.NgFYG2D7PftFW1tcfe5E2oDi_AVakDR9J6WI6VUZQfw",
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
ExpectedContent: []string{},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterDelete": 1,
|
||||
"OnModelBeforeDelete": 1,
|
||||
"OnUserAfterUnlinkExternalAuthRequest": 1,
|
||||
"OnUserBeforeUnlinkExternalAuthRequest": 1,
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
auth, _ := app.Dao().FindExternalAuthByUserIdAndProvider("cx9u0dh2udo8xol", "google")
|
||||
if auth != nil {
|
||||
t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -87,10 +86,11 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
if httpsAddr != "" {
|
||||
schema = "https"
|
||||
}
|
||||
regular := color.New()
|
||||
bold := color.New(color.Bold).Add(color.FgGreen)
|
||||
bold.Printf("> Server started at: %s\n", color.CyanString("%s://%s", schema, serverConfig.Addr))
|
||||
fmt.Printf(" - REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
|
||||
fmt.Printf(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
|
||||
regular.Printf(" - REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
|
||||
regular.Printf(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
|
||||
}
|
||||
|
||||
var serveErr error
|
||||
|
||||
+13
-8
@@ -317,16 +317,21 @@ type App interface {
|
||||
// authenticated user data and token.
|
||||
OnUserAuthRequest() *hook.Hook[*UserAuthEvent]
|
||||
|
||||
// OnUserBeforeOauth2Register hook is triggered before each User OAuth2
|
||||
// authentication request (when the client config has enabled new users registration).
|
||||
// OnUserListExternalAuths hook is triggered on each API user's external auths list request.
|
||||
//
|
||||
// Could be used to additionally validate or modify the new user
|
||||
// before persisting in the DB.
|
||||
OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent]
|
||||
// Could be used to validate or modify the response before returning it to the client.
|
||||
OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent]
|
||||
|
||||
// OnUserAfterOauth2Register hook is triggered after each successful User
|
||||
// OAuth2 authentication sign-up request (right after the new user persistence).
|
||||
OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent]
|
||||
// OnUserBeforeUnlinkExternalAuthRequest hook is triggered before each API user's
|
||||
// external auth unlink request (after models load and before the actual relation deletion).
|
||||
//
|
||||
// Could be used to additionally validate the request data or implement
|
||||
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||
OnUserBeforeUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent]
|
||||
|
||||
// OnUserAfterUnlinkExternalAuthRequest hook is triggered after each
|
||||
// successful API user's external auth unlink request.
|
||||
OnUserAfterUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent]
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Record API event hooks
|
||||
|
||||
+33
-27
@@ -85,18 +85,19 @@ type BaseApp struct {
|
||||
onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent]
|
||||
onAdminAuthRequest *hook.Hook[*AdminAuthEvent]
|
||||
|
||||
// user api event hooks
|
||||
onUsersListRequest *hook.Hook[*UsersListEvent]
|
||||
onUserViewRequest *hook.Hook[*UserViewEvent]
|
||||
onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent]
|
||||
onUserAfterCreateRequest *hook.Hook[*UserCreateEvent]
|
||||
onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent]
|
||||
onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent]
|
||||
onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent]
|
||||
onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent]
|
||||
onUserAuthRequest *hook.Hook[*UserAuthEvent]
|
||||
onUserBeforeOauth2Register *hook.Hook[*UserOauth2RegisterEvent]
|
||||
onUserAfterOauth2Register *hook.Hook[*UserOauth2RegisterEvent]
|
||||
// user api event hooks
|
||||
onUsersListRequest *hook.Hook[*UsersListEvent]
|
||||
onUserViewRequest *hook.Hook[*UserViewEvent]
|
||||
onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent]
|
||||
onUserAfterCreateRequest *hook.Hook[*UserCreateEvent]
|
||||
onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent]
|
||||
onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent]
|
||||
onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent]
|
||||
onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent]
|
||||
onUserAuthRequest *hook.Hook[*UserAuthEvent]
|
||||
onUserListExternalAuths *hook.Hook[*UserListExternalAuthsEvent]
|
||||
onUserBeforeUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
|
||||
onUserAfterUnlinkExternalAuthRequest *hook.Hook[*UserUnlinkExternalAuthEvent]
|
||||
|
||||
// record api event hooks
|
||||
onRecordsListRequest *hook.Hook[*RecordsListEvent]
|
||||
@@ -180,17 +181,18 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
|
||||
onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{},
|
||||
|
||||
// user API event hooks
|
||||
onUsersListRequest: &hook.Hook[*UsersListEvent]{},
|
||||
onUserViewRequest: &hook.Hook[*UserViewEvent]{},
|
||||
onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{},
|
||||
onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{},
|
||||
onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
|
||||
onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
|
||||
onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
|
||||
onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
|
||||
onUserAuthRequest: &hook.Hook[*UserAuthEvent]{},
|
||||
onUserBeforeOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{},
|
||||
onUserAfterOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{},
|
||||
onUsersListRequest: &hook.Hook[*UsersListEvent]{},
|
||||
onUserViewRequest: &hook.Hook[*UserViewEvent]{},
|
||||
onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{},
|
||||
onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{},
|
||||
onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
|
||||
onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{},
|
||||
onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
|
||||
onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{},
|
||||
onUserAuthRequest: &hook.Hook[*UserAuthEvent]{},
|
||||
onUserListExternalAuths: &hook.Hook[*UserListExternalAuthsEvent]{},
|
||||
onUserBeforeUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
|
||||
onUserAfterUnlinkExternalAuthRequest: &hook.Hook[*UserUnlinkExternalAuthEvent]{},
|
||||
|
||||
// record API event hooks
|
||||
onRecordsListRequest: &hook.Hook[*RecordsListEvent]{},
|
||||
@@ -611,12 +613,16 @@ func (app *BaseApp) OnUserAuthRequest() *hook.Hook[*UserAuthEvent] {
|
||||
return app.onUserAuthRequest
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
|
||||
return app.onUserBeforeOauth2Register
|
||||
func (app *BaseApp) OnUserListExternalAuths() *hook.Hook[*UserListExternalAuthsEvent] {
|
||||
return app.onUserListExternalAuths
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] {
|
||||
return app.onUserAfterOauth2Register
|
||||
func (app *BaseApp) OnUserBeforeUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
|
||||
return app.onUserBeforeUnlinkExternalAuthRequest
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnUserAfterUnlinkExternalAuthRequest() *hook.Hook[*UserUnlinkExternalAuthEvent] {
|
||||
return app.onUserAfterUnlinkExternalAuthRequest
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
+8
-4
@@ -319,12 +319,16 @@ func TestBaseAppGetters(t *testing.T) {
|
||||
t.Fatalf("Getter app.OnUserAuthRequest does not match or nil (%v vs %v)", app.OnUserAuthRequest(), app.onUserAuthRequest)
|
||||
}
|
||||
|
||||
if app.onUserBeforeOauth2Register != app.OnUserBeforeOauth2Register() || app.OnUserBeforeOauth2Register() == nil {
|
||||
t.Fatalf("Getter app.OnUserBeforeOauth2Register does not match or nil (%v vs %v)", app.OnUserBeforeOauth2Register(), app.onUserBeforeOauth2Register)
|
||||
if app.onUserListExternalAuths != app.OnUserListExternalAuths() || app.OnUserListExternalAuths() == nil {
|
||||
t.Fatalf("Getter app.OnUserListExternalAuths does not match or nil (%v vs %v)", app.OnUserListExternalAuths(), app.onUserListExternalAuths)
|
||||
}
|
||||
|
||||
if app.onUserAfterOauth2Register != app.OnUserAfterOauth2Register() || app.OnUserAfterOauth2Register() == nil {
|
||||
t.Fatalf("Getter app.OnUserAfterOauth2Register does not match or nil (%v vs %v)", app.OnUserAfterOauth2Register(), app.onUserAfterOauth2Register)
|
||||
if app.onUserBeforeUnlinkExternalAuthRequest != app.OnUserBeforeUnlinkExternalAuthRequest() || app.OnUserBeforeUnlinkExternalAuthRequest() == nil {
|
||||
t.Fatalf("Getter app.OnUserBeforeUnlinkExternalAuthRequest does not match or nil (%v vs %v)", app.OnUserBeforeUnlinkExternalAuthRequest(), app.onUserBeforeUnlinkExternalAuthRequest)
|
||||
}
|
||||
|
||||
if app.onUserAfterUnlinkExternalAuthRequest != app.OnUserAfterUnlinkExternalAuthRequest() || app.OnUserAfterUnlinkExternalAuthRequest() == nil {
|
||||
t.Fatalf("Getter app.OnUserAfterUnlinkExternalAuthRequest does not match or nil (%v vs %v)", app.OnUserAfterUnlinkExternalAuthRequest(), app.onUserAfterUnlinkExternalAuthRequest)
|
||||
}
|
||||
|
||||
if app.onRecordsListRequest != app.OnRecordsListRequest() || app.OnRecordsListRequest() == nil {
|
||||
|
||||
+10
-5
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||
@@ -180,10 +179,16 @@ type UserAuthEvent struct {
|
||||
Meta any
|
||||
}
|
||||
|
||||
type UserOauth2RegisterEvent struct {
|
||||
HttpContext echo.Context
|
||||
User *models.User
|
||||
AuthData *auth.AuthUser
|
||||
type UserListExternalAuthsEvent struct {
|
||||
HttpContext echo.Context
|
||||
User *models.User
|
||||
ExternalAuths []*models.ExternalAuth
|
||||
}
|
||||
|
||||
type UserUnlinkExternalAuthEvent struct {
|
||||
HttpContext echo.Context
|
||||
User *models.User
|
||||
ExternalAuth *models.ExternalAuth
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
+154
-80
@@ -10,48 +10,49 @@ import (
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// Common settings placeholder tokens
|
||||
const (
|
||||
EmailPlaceholderAppUrl string = "%APP_URL%"
|
||||
EmailPlaceholderToken string = "%TOKEN%"
|
||||
)
|
||||
|
||||
// Settings defines common app configuration options.
|
||||
type Settings struct {
|
||||
mux sync.RWMutex
|
||||
|
||||
Meta MetaConfig `form:"meta" json:"meta"`
|
||||
Logs LogsConfig `form:"logs" json:"logs"`
|
||||
Smtp SmtpConfig `form:"smtp" json:"smtp"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
|
||||
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
|
||||
UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"`
|
||||
UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"`
|
||||
UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"`
|
||||
UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"`
|
||||
EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`
|
||||
GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
|
||||
FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"`
|
||||
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
|
||||
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
|
||||
Meta MetaConfig `form:"meta" json:"meta"`
|
||||
Logs LogsConfig `form:"logs" json:"logs"`
|
||||
Smtp SmtpConfig `form:"smtp" json:"smtp"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
|
||||
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
|
||||
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
|
||||
UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"`
|
||||
UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"`
|
||||
UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"`
|
||||
UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"`
|
||||
|
||||
EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`
|
||||
GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
|
||||
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.
|
||||
func NewSettings() *Settings {
|
||||
return &Settings{
|
||||
Meta: MetaConfig{
|
||||
AppName: "Acme",
|
||||
AppUrl: "http://localhost:8090",
|
||||
SenderName: "Support",
|
||||
SenderAddress: "support@example.com",
|
||||
UserVerificationUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-verification/" + EmailPlaceholderToken,
|
||||
UserResetPasswordUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-password-reset/" + EmailPlaceholderToken,
|
||||
UserConfirmEmailChangeUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-email-change/" + EmailPlaceholderToken,
|
||||
AppName: "Acme",
|
||||
AppUrl: "http://localhost:8090",
|
||||
HideControls: false,
|
||||
SenderName: "Support",
|
||||
SenderAddress: "support@example.com",
|
||||
VerificationTemplate: defaultVerificationTemplate,
|
||||
ResetPasswordTemplate: defaultResetPasswordTemplate,
|
||||
ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
|
||||
},
|
||||
|
||||
Logs: LogsConfig{
|
||||
MaxDays: 7,
|
||||
},
|
||||
@@ -107,6 +108,14 @@ func NewSettings() *Settings {
|
||||
Enabled: false,
|
||||
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.GithubAuth),
|
||||
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.GithubAuth.ClientSecret,
|
||||
&clone.GitlabAuth.ClientSecret,
|
||||
&clone.DiscordAuth.ClientSecret,
|
||||
&clone.TwitterAuth.ClientSecret,
|
||||
}
|
||||
|
||||
// mask all sensitive fields
|
||||
@@ -194,11 +207,16 @@ func (s *Settings) RedactClone() (*Settings, error) {
|
||||
// NamedAuthProviderConfigs returns a map with all registered OAuth2
|
||||
// provider configurations (indexed by their name identifier).
|
||||
func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
return map[string]AuthProviderConfig{
|
||||
auth.NameGoogle: s.GoogleAuth,
|
||||
auth.NameFacebook: s.FacebookAuth,
|
||||
auth.NameGithub: s.GithubAuth,
|
||||
auth.NameGitlab: s.GitlabAuth,
|
||||
auth.NameDiscord: s.DiscordAuth,
|
||||
auth.NameTwitter: s.TwitterAuth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +231,7 @@ type TokenConfig struct {
|
||||
func (c TokenConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)),
|
||||
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(31536000)),
|
||||
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(63072000)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -267,13 +285,14 @@ func (c S3Config) Validate() error {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type MetaConfig struct {
|
||||
AppName string `form:"appName" json:"appName"`
|
||||
AppUrl string `form:"appUrl" json:"appUrl"`
|
||||
SenderName string `form:"senderName" json:"senderName"`
|
||||
SenderAddress string `form:"senderAddress" json:"senderAddress"`
|
||||
UserVerificationUrl string `form:"userVerificationUrl" json:"userVerificationUrl"`
|
||||
UserResetPasswordUrl string `form:"userResetPasswordUrl" json:"userResetPasswordUrl"`
|
||||
UserConfirmEmailChangeUrl string `form:"userConfirmEmailChangeUrl" json:"userConfirmEmailChangeUrl"`
|
||||
AppName string `form:"appName" json:"appName"`
|
||||
AppUrl string `form:"appUrl" json:"appUrl"`
|
||||
HideControls bool `form:"hideControls" json:"hideControls"`
|
||||
SenderName string `form:"senderName" json:"senderName"`
|
||||
SenderAddress string `form:"senderAddress" json:"senderAddress"`
|
||||
VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
|
||||
ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
|
||||
ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
|
||||
}
|
||||
|
||||
// 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.AppUrl, validation.Required, is.URL),
|
||||
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
|
||||
validation.Field(&c.SenderAddress, is.Email, validation.Required),
|
||||
validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
|
||||
validation.Field(&c.VerificationTemplate, validation.Required),
|
||||
validation.Field(&c.ResetPasswordTemplate, validation.Required),
|
||||
validation.Field(&c.ConfirmEmailChangeTemplate, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
&c.UserVerificationUrl,
|
||||
&t.Body,
|
||||
validation.Required,
|
||||
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
|
||||
validation.By(checkPlaceholderParams(EmailPlaceholderActionUrl)),
|
||||
),
|
||||
validation.Field(
|
||||
&c.UserResetPasswordUrl,
|
||||
&t.ActionUrl,
|
||||
validation.Required,
|
||||
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
|
||||
),
|
||||
validation.Field(
|
||||
&c.UserConfirmEmailChangeUrl,
|
||||
validation.Required,
|
||||
validation.By(c.checkPlaceholders(EmailPlaceholderToken)),
|
||||
validation.By(checkPlaceholderParams(EmailPlaceholderToken)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *MetaConfig) checkPlaceholders(params ...string) validation.RuleFunc {
|
||||
func checkPlaceholderParams(params ...string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
for _, param := range params {
|
||||
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 {
|
||||
@@ -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 {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
AllowRegistrations bool `form:"allowRegistrations" json:"allowRegistrations"`
|
||||
@@ -382,32 +485,3 @@ func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -2,9 +2,11 @@ package core_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
)
|
||||
@@ -35,6 +37,10 @@ func TestSettingsValidate(t *testing.T) {
|
||||
s.GithubAuth.ClientId = ""
|
||||
s.GitlabAuth.Enabled = true
|
||||
s.GitlabAuth.ClientId = ""
|
||||
s.DiscordAuth.Enabled = true
|
||||
s.DiscordAuth.ClientId = ""
|
||||
s.TwitterAuth.Enabled = true
|
||||
s.TwitterAuth.ClientId = ""
|
||||
|
||||
// check if Validate() is triggering the members validate methods.
|
||||
err := s.Validate()
|
||||
@@ -58,6 +64,7 @@ func TestSettingsValidate(t *testing.T) {
|
||||
`"facebookAuth":{`,
|
||||
`"githubAuth":{`,
|
||||
`"gitlabAuth":{`,
|
||||
`"discordAuth":{`,
|
||||
}
|
||||
|
||||
errBytes, _ := json.Marshal(err)
|
||||
@@ -96,6 +103,10 @@ func TestSettingsMerge(t *testing.T) {
|
||||
s2.GithubAuth.ClientId = "github_test"
|
||||
s2.GitlabAuth.Enabled = true
|
||||
s2.GitlabAuth.ClientId = "gitlab_test"
|
||||
s2.DiscordAuth.Enabled = true
|
||||
s2.DiscordAuth.ClientId = "discord_test"
|
||||
s2.TwitterAuth.Enabled = true
|
||||
s2.TwitterAuth.ClientId = "twitter_test"
|
||||
|
||||
if err := s1.Merge(s2); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -161,6 +172,8 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
s1.FacebookAuth.ClientSecret = "test123"
|
||||
s1.GithubAuth.ClientSecret = "test123"
|
||||
s1.GitlabAuth.ClientSecret = "test123"
|
||||
s1.DiscordAuth.ClientSecret = "test123"
|
||||
s1.TwitterAuth.ClientSecret = "test123"
|
||||
|
||||
s2, err := s1.RedactClone()
|
||||
if err != nil {
|
||||
@@ -172,7 +185,7 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("Expected %v, got \n%v", expected, encodedStr)
|
||||
@@ -187,6 +200,8 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
s.GithubAuth.ClientId = "github_test"
|
||||
s.GitlabAuth.ClientId = "gitlab_test"
|
||||
s.GitlabAuth.Enabled = true
|
||||
s.DiscordAuth.ClientId = "discord_test"
|
||||
s.TwitterAuth.ClientId = "twitter_test"
|
||||
|
||||
result := s.NamedAuthProviderConfigs()
|
||||
|
||||
@@ -195,7 +210,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"}}`
|
||||
expected := `{"discord":{"enabled":false,"allowRegistrations":true,"clientId":"discord_test"},"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"},"twitter":{"enabled":false,"allowRegistrations":true,"clientId":"twitter_test"}}`
|
||||
|
||||
if encodedStr := string(encoded); encodedStr != expected {
|
||||
t.Fatalf("Expected the same serialization, got %v", encodedStr)
|
||||
@@ -215,15 +230,23 @@ func TestTokenConfigValidate(t *testing.T) {
|
||||
// invalid data
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: "test",
|
||||
Secret: strings.Repeat("a", 5),
|
||||
Duration: 4,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid secret but invalid duration
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: strings.Repeat("a", 30),
|
||||
Duration: 63072000 + 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: "testtesttesttesttesttesttestte",
|
||||
Secret: strings.Repeat("a", 30),
|
||||
Duration: 100,
|
||||
},
|
||||
false,
|
||||
@@ -355,6 +378,24 @@ func TestS3ConfigValidate(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 {
|
||||
config core.MetaConfig
|
||||
expectError bool
|
||||
@@ -367,39 +408,39 @@ func TestMetaConfigValidate(t *testing.T) {
|
||||
// invalid data
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: strings.Repeat("a", 300),
|
||||
AppUrl: "test",
|
||||
SenderName: strings.Repeat("a", 300),
|
||||
SenderAddress: "invalid_email",
|
||||
UserVerificationUrl: "test",
|
||||
UserResetPasswordUrl: "test",
|
||||
UserConfirmEmailChangeUrl: "test",
|
||||
AppName: strings.Repeat("a", 300),
|
||||
AppUrl: "test",
|
||||
SenderName: strings.Repeat("a", 300),
|
||||
SenderAddress: "invalid_email",
|
||||
VerificationTemplate: invalidTemplate,
|
||||
ResetPasswordTemplate: invalidTemplate,
|
||||
ConfirmEmailChangeTemplate: invalidTemplate,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// invalid data (missing required placeholders)
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
UserVerificationUrl: "https://example.com",
|
||||
UserResetPasswordUrl: "https://example.com",
|
||||
UserConfirmEmailChangeUrl: "https://example.com",
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
VerificationTemplate: noPlaceholdersTemplate,
|
||||
ResetPasswordTemplate: noPlaceholdersTemplate,
|
||||
ConfirmEmailChangeTemplate: noPlaceholdersTemplate,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
UserVerificationUrl: "https://example.com/" + core.EmailPlaceholderToken,
|
||||
UserResetPasswordUrl: "https://example.com/" + core.EmailPlaceholderToken,
|
||||
UserConfirmEmailChangeUrl: "https://example.com/" + core.EmailPlaceholderToken,
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
VerificationTemplate: withPlaceholdersTemplate,
|
||||
ResetPasswordTemplate: withPlaceholdersTemplate,
|
||||
ConfirmEmailChangeTemplate: withPlaceholdersTemplate,
|
||||
},
|
||||
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) {
|
||||
scenarios := []struct {
|
||||
config core.LogsConfig
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -94,7 +94,7 @@ func (dao *Dao) FindUserById(id string) (*models.User, error) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// FindUserByEmail finds a single User model by its email address.
|
||||
// FindUserByEmail finds a single User model by its non-empty email address.
|
||||
//
|
||||
// This method also auto loads the related user profile record
|
||||
// into the found model.
|
||||
@@ -102,6 +102,7 @@ func (dao *Dao) FindUserByEmail(email string) (*models.User, error) {
|
||||
model := &models.User{}
|
||||
|
||||
err := dao.UserQuery().
|
||||
AndWhere(dbx.Not(dbx.HashExp{"email": ""})).
|
||||
AndWhere(dbx.HashExp{"email": email}).
|
||||
Limit(1).
|
||||
One(model)
|
||||
|
||||
@@ -110,6 +110,7 @@ func TestFindUserByEmail(t *testing.T) {
|
||||
email string
|
||||
expectError bool
|
||||
}{
|
||||
{"", true},
|
||||
{"invalid", true},
|
||||
{"missing@example.com", true},
|
||||
{"test@example.com", false},
|
||||
|
||||
+23
-3
@@ -2,20 +2,40 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func defaultPublicDir() string {
|
||||
if strings.HasPrefix(os.Args[0], os.TempDir()) {
|
||||
// most likely ran with go run
|
||||
return "./pb_public"
|
||||
}
|
||||
|
||||
return filepath.Join(os.Args[0], "../pb_public")
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := pocketbase.New()
|
||||
|
||||
var publicDirFlag string
|
||||
|
||||
// add "--publicDir" option flag
|
||||
app.RootCmd.PersistentFlags().StringVar(
|
||||
&publicDirFlag,
|
||||
"publicDir",
|
||||
defaultPublicDir(),
|
||||
"the directory to serve static files",
|
||||
)
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
// serves static files from the provided public dir (if exists)
|
||||
subFs := echo.MustSubFS(e.Router.Filesystem, "pb_public")
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false))
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDirFlag), false))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -22,15 +22,15 @@ type AdminLogin struct {
|
||||
//
|
||||
// NB! App is a required struct member.
|
||||
type AdminLoginConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewAdminLogin creates a new [AdminLogin] form with initializer
|
||||
// config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewAdminLoginWithConfig] with explicitly set TxDao.
|
||||
// [NewAdminLoginWithConfig] with explicitly set Dao.
|
||||
func NewAdminLogin(app core.App) *AdminLogin {
|
||||
return NewAdminLoginWithConfig(AdminLoginConfig{
|
||||
App: app,
|
||||
@@ -46,8 +46,8 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin {
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -56,7 +56,7 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin {
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *AdminLogin) Validate() error {
|
||||
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)),
|
||||
)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (form *AdminLogin) Submit() (*models.Admin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
admin, err := form.config.TxDao.FindAdminByEmail(form.Email)
|
||||
admin, err := form.config.Dao.FindAdminByEmail(form.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ type AdminPasswordResetConfirm struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type AdminPasswordResetConfirmConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm]
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewAdminPasswordResetConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewAdminPasswordResetConfirmWithConfig] with explicitly set Dao.
|
||||
func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm {
|
||||
return NewAdminPasswordResetConfirmWithConfig(AdminPasswordResetConfirmConfig{
|
||||
App: app,
|
||||
@@ -45,8 +45,8 @@ func NewAdminPasswordResetConfirmWithConfig(config AdminPasswordResetConfirmConf
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -67,7 +67,7 @@ func (form *AdminPasswordResetConfirm) checkToken(value any) error {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
admin, err := form.config.TxDao.FindAdminByToken(
|
||||
admin, err := form.config.Dao.FindAdminByToken(
|
||||
v,
|
||||
form.config.App.Settings().AdminPasswordResetToken.Secret,
|
||||
)
|
||||
@@ -85,7 +85,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
admin, err := form.config.TxDao.FindAdminByToken(
|
||||
admin, err := form.config.Dao.FindAdminByToken(
|
||||
form.Token,
|
||||
form.config.App.Settings().AdminPasswordResetToken.Secret,
|
||||
)
|
||||
@@ -97,7 +97,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := form.config.TxDao.SaveAdmin(admin); err != nil {
|
||||
if err := form.config.Dao.SaveAdmin(admin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ type AdminPasswordResetRequest struct {
|
||||
// NB! App is required struct member.
|
||||
type AdminPasswordResetRequestConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
Dao *daos.Dao
|
||||
ResendThreshold float64 // in seconds
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type AdminPasswordResetRequestConfig struct {
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewAdminPasswordResetRequestWithConfig] with explicitly set TxDao.
|
||||
// [NewAdminPasswordResetRequestWithConfig] with explicitly set Dao.
|
||||
func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest {
|
||||
return NewAdminPasswordResetRequestWithConfig(AdminPasswordResetRequestConfig{
|
||||
App: app,
|
||||
@@ -49,8 +49,8 @@ func NewAdminPasswordResetRequestWithConfig(config AdminPasswordResetRequestConf
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -65,7 +65,7 @@ func (form *AdminPasswordResetRequest) Validate() error {
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (form *AdminPasswordResetRequest) Submit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
admin, err := form.config.TxDao.FindAdminByEmail(form.Email)
|
||||
admin, err := form.config.Dao.FindAdminByEmail(form.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,5 +95,5 @@ func (form *AdminPasswordResetRequest) Submit() error {
|
||||
// update last sent timestamp
|
||||
admin.LastResetSentAt = types.NowDateTime()
|
||||
|
||||
return form.config.TxDao.SaveAdmin(admin)
|
||||
return form.config.Dao.SaveAdmin(admin)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ type AdminUpsert struct {
|
||||
//
|
||||
// NB! App is a required struct member.
|
||||
type AdminUpsertConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewAdminUpsert creates a new [AdminUpsert] form with initializer
|
||||
@@ -34,7 +34,7 @@ type AdminUpsertConfig struct {
|
||||
// (for create you could pass a pointer to an empty Admin - `&models.Admin{}`).
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewAdminUpsertWithConfig] with explicitly set TxDao.
|
||||
// [NewAdminUpsertWithConfig] with explicitly set Dao.
|
||||
func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert {
|
||||
return NewAdminUpsertWithConfig(AdminUpsertConfig{
|
||||
App: app,
|
||||
@@ -54,8 +54,8 @@ func NewAdminUpsertWithConfig(config AdminUpsertConfig, admin *models.Admin) *Ad
|
||||
panic("Invalid initializer config or nil upsert model.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
// load defaults
|
||||
@@ -86,7 +86,7 @@ func (form *AdminUpsert) Validate() error {
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
validation.By(form.checkUniqueEmail),
|
||||
),
|
||||
validation.Field(
|
||||
@@ -105,7 +105,7 @@ func (form *AdminUpsert) Validate() error {
|
||||
func (form *AdminUpsert) checkUniqueEmail(value any) error {
|
||||
v, _ := value.(string)
|
||||
|
||||
if form.config.TxDao.IsAdminEmailUnique(v, form.admin.Id) {
|
||||
if form.config.Dao.IsAdminEmailUnique(v, form.admin.Id) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -135,6 +135,6 @@ func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
}
|
||||
|
||||
return runInterceptors(func() error {
|
||||
return form.config.TxDao.SaveAdmin(form.admin)
|
||||
return form.config.Dao.SaveAdmin(form.admin)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
+10
-10
@@ -36,8 +36,8 @@ type CollectionUpsert struct {
|
||||
//
|
||||
// NB! App is a required struct member.
|
||||
type CollectionUpsertConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer
|
||||
@@ -45,7 +45,7 @@ type CollectionUpsertConfig struct {
|
||||
// (for create you could pass a pointer to an empty Collection - `&models.Collection{}`).
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewCollectionUpsertWithConfig] with explicitly set TxDao.
|
||||
// [NewCollectionUpsertWithConfig] with explicitly set Dao.
|
||||
func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert {
|
||||
return NewCollectionUpsertWithConfig(CollectionUpsertConfig{
|
||||
App: app,
|
||||
@@ -65,8 +65,8 @@ func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *mo
|
||||
panic("Invalid initializer config or nil upsert model.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
// load defaults
|
||||
@@ -130,11 +130,11 @@ func (form *CollectionUpsert) Validate() error {
|
||||
func (form *CollectionUpsert) checkUniqueName(value any) error {
|
||||
v, _ := value.(string)
|
||||
|
||||
if !form.config.TxDao.IsCollectionNameUnique(v, form.collection.Id) {
|
||||
if !form.config.Dao.IsCollectionNameUnique(v, form.collection.Id) {
|
||||
return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).")
|
||||
}
|
||||
|
||||
if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.TxDao.HasTable(v) {
|
||||
if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.Dao.HasTable(v) {
|
||||
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) erro
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := form.config.TxDao.FindCollectionByNameOrId(options.CollectionId); err != nil {
|
||||
if _, err := form.config.Dao.FindCollectionByNameOrId(options.CollectionId); err != nil {
|
||||
return validation.Errors{fmt.Sprint(i): validation.NewError(
|
||||
"validation_field_invalid_relation",
|
||||
"The relation collection doesn't exist.",
|
||||
@@ -228,7 +228,7 @@ func (form *CollectionUpsert) checkRule(value any) error {
|
||||
}
|
||||
|
||||
dummy := &models.Collection{Schema: form.Schema}
|
||||
r := resolvers.NewRecordFieldResolver(form.config.TxDao, dummy, nil)
|
||||
r := resolvers.NewRecordFieldResolver(form.config.Dao, dummy, nil)
|
||||
|
||||
_, err := search.FilterData(*v).BuildExpr(r)
|
||||
if err != nil {
|
||||
@@ -273,6 +273,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
form.collection.DeleteRule = form.DeleteRule
|
||||
|
||||
return runInterceptors(func() error {
|
||||
return form.config.TxDao.SaveCollection(form.collection)
|
||||
return form.config.Dao.SaveCollection(form.collection)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@ type CollectionsImport struct {
|
||||
//
|
||||
// NB! App is a required struct member.
|
||||
type CollectionsImportConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewCollectionsImport creates a new [CollectionsImport] form with
|
||||
// initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewCollectionsImportWithConfig] with explicitly set TxDao.
|
||||
// [NewCollectionsImportWithConfig] with explicitly set Dao.
|
||||
func NewCollectionsImport(app core.App) *CollectionsImport {
|
||||
return NewCollectionsImportWithConfig(CollectionsImportConfig{
|
||||
App: app,
|
||||
@@ -48,8 +48,8 @@ func NewCollectionsImportWithConfig(config CollectionsImportConfig) *Collections
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -79,7 +79,7 @@ func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc) error {
|
||||
}
|
||||
|
||||
return runInterceptors(func() error {
|
||||
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
importErr := txDao.ImportCollections(
|
||||
form.Collections,
|
||||
form.DeleteMissing,
|
||||
@@ -122,8 +122,8 @@ func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, map
|
||||
}
|
||||
|
||||
upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{
|
||||
App: form.config.App,
|
||||
TxDao: txDao,
|
||||
App: form.config.App,
|
||||
Dao: txDao,
|
||||
}, upsertModel)
|
||||
|
||||
// load form fields with the refreshed collection state
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
)
|
||||
|
||||
// RealtimeSubscribe specifies a RealtimeSubscribe request form.
|
||||
// RealtimeSubscribe is a realtime subscriptions request form.
|
||||
type RealtimeSubscribe struct {
|
||||
ClientId string `form:"clientId" json:"clientId"`
|
||||
Subscriptions []string `form:"subscriptions" json:"subscriptions"`
|
||||
|
||||
+34
-22
@@ -37,8 +37,8 @@ type RecordUpsert struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type RecordUpsertConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewRecordUpsert creates a new [RecordUpsert] form with initializer
|
||||
@@ -46,7 +46,7 @@ type RecordUpsertConfig struct {
|
||||
// (for create you could pass a pointer to an empty Record - `models.NewRecord(collection)`).
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewRecordUpsertWithConfig] with explicitly set TxDao.
|
||||
// [NewRecordUpsertWithConfig] with explicitly set Dao.
|
||||
func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
|
||||
return NewRecordUpsertWithConfig(RecordUpsertConfig{
|
||||
App: app,
|
||||
@@ -68,8 +68,8 @@ func NewRecordUpsertWithConfig(config RecordUpsertConfig, record *models.Record)
|
||||
panic("Invalid initializer config or nil upsert model.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
form.Id = record.Id
|
||||
@@ -194,21 +194,26 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
|
||||
options, _ := field.Options.(*schema.FileOptions)
|
||||
oldNames := list.ToUniqueStringSlice(form.Data[key])
|
||||
|
||||
// delete previously uploaded file(s)
|
||||
if options.MaxSelect == 1 {
|
||||
// search for unset zero indexed key as a fallback
|
||||
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
|
||||
// -----------------------------------------------------------
|
||||
// Delete previously uploaded file(s)
|
||||
// -----------------------------------------------------------
|
||||
|
||||
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") {
|
||||
if len(oldNames) > 0 {
|
||||
form.filesToDelete = append(form.filesToDelete, oldNames...)
|
||||
}
|
||||
form.Data[key] = ""
|
||||
}
|
||||
} else if options.MaxSelect > 1 {
|
||||
// search for individual file index to delete (eg. "file.0")
|
||||
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
|
||||
// if empty value was set, mark all previously uploaded files for deletion
|
||||
if len(list.ToUniqueStringSlice(value)) == 0 && len(oldNames) > 0 {
|
||||
form.filesToDelete = append(form.filesToDelete, oldNames...)
|
||||
form.Data[key] = []string{}
|
||||
} else if len(oldNames) > 0 {
|
||||
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 {
|
||||
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
|
||||
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
|
||||
@@ -234,7 +239,14 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
|
||||
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)
|
||||
if err != nil {
|
||||
if form.config.App.IsDebug() {
|
||||
@@ -286,7 +298,7 @@ func (form *RecordUpsert) Validate() error {
|
||||
|
||||
// record data validator
|
||||
dataValidator := validators.NewRecordDataValidator(
|
||||
form.config.TxDao,
|
||||
form.config.Dao,
|
||||
form.record,
|
||||
form.filesToUpload,
|
||||
)
|
||||
@@ -316,7 +328,7 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
|
||||
return err
|
||||
}
|
||||
|
||||
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
tx, ok := txDao.DB().(*dbx.Tx)
|
||||
if !ok {
|
||||
return errors.New("failed to get transaction db")
|
||||
@@ -366,7 +378,7 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
}
|
||||
|
||||
return runInterceptors(func() error {
|
||||
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
// persist record model
|
||||
if err := txDao.SaveRecord(form.record); err != nil {
|
||||
return err
|
||||
|
||||
+20
-19
@@ -98,7 +98,7 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
|
||||
"onefile": nil,
|
||||
"manyfiles.0": "",
|
||||
"manyfiles.1": "test.png", // should be ignored
|
||||
"onlyimages": nil, // should be ignored
|
||||
"onlyimages": nil,
|
||||
}
|
||||
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
@@ -136,18 +136,12 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
|
||||
}
|
||||
manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles))
|
||||
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, ok := form.Data["onlyimages"]
|
||||
if !ok || onlyimages == nil {
|
||||
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)
|
||||
onlyimages := form.Data["onlyimages"]
|
||||
if len(list.ToUniqueStringSlice(onlyimages)) != 0 {
|
||||
t.Fatalf("Expect onlyimages field to be deleted, got \n%v", onlyimages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,9 +161,10 @@ func TestRecordUpsertLoadDataMultipart(t *testing.T) {
|
||||
"unknown": "test456",
|
||||
// file fields unset/delete
|
||||
"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
|
||||
"onlyimages": "", // should be ignored
|
||||
"onlyimages": "",
|
||||
}, "onlyimages")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -208,16 +203,16 @@ func TestRecordUpsertLoadDataMultipart(t *testing.T) {
|
||||
t.Fatal("Expect manyfiles field to be set")
|
||||
}
|
||||
manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles))
|
||||
if manyfilesRemains != 1 {
|
||||
t.Fatalf("Expect only 1 manyfiles to remain, got %v", manyfiles)
|
||||
if manyfilesRemains != 0 {
|
||||
t.Fatalf("Expect 0 manyfiles to remain, got %v", manyfiles)
|
||||
}
|
||||
|
||||
onlyimages, ok := form.Data["onlyimages"]
|
||||
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))
|
||||
expectedRemains := 3 // 2 existing + 1 new upload
|
||||
expectedRemains := 1 // -2 removed + 1 new upload
|
||||
if onlyimagesRemains != expectedRemains {
|
||||
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{
|
||||
"title": "test_save",
|
||||
"onefile": "",
|
||||
"title": "test_save",
|
||||
"onefile": "",
|
||||
"onlyimages": "",
|
||||
}, "manyfiles.1", "manyfiles") // replace + new file
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -535,6 +531,11 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
|
||||
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"))
|
||||
if len(manyfiles) != 3 {
|
||||
t.Fatalf("Expected 3 manyfiles, got %d (%v)", len(manyfiles), manyfiles)
|
||||
|
||||
+10
-10
@@ -20,16 +20,16 @@ type SettingsUpsert struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type SettingsUpsertConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
TxLogsDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
LogsDao *daos.Dao
|
||||
}
|
||||
|
||||
// NewSettingsUpsert creates a new [SettingsUpsert] form with initializer
|
||||
// config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewSettingsUpsertWithConfig] with explicitly set TxDao.
|
||||
// [NewSettingsUpsertWithConfig] with explicitly set Dao.
|
||||
func NewSettingsUpsert(app core.App) *SettingsUpsert {
|
||||
return NewSettingsUpsertWithConfig(SettingsUpsertConfig{
|
||||
App: app,
|
||||
@@ -45,12 +45,12 @@ func NewSettingsUpsertWithConfig(config SettingsUpsertConfig) *SettingsUpsert {
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
if form.config.TxLogsDao == nil {
|
||||
form.config.TxLogsDao = form.config.App.LogsDao()
|
||||
if form.config.LogsDao == nil {
|
||||
form.config.LogsDao = form.config.App.LogsDao()
|
||||
}
|
||||
|
||||
// load the application settings into the form
|
||||
@@ -78,7 +78,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
encryptionKey := os.Getenv(form.config.App.EncryptionEnv())
|
||||
|
||||
return runInterceptors(func() error {
|
||||
saveErr := form.config.TxDao.SaveParam(
|
||||
saveErr := form.config.Dao.SaveParam(
|
||||
models.ParamAppSettings,
|
||||
form.Settings,
|
||||
encryptionKey,
|
||||
@@ -88,7 +88,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
}
|
||||
|
||||
// explicitly trigger old logs deletion
|
||||
form.config.TxLogsDao.DeleteOldRequests(
|
||||
form.config.LogsDao.DeleteOldRequests(
|
||||
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ type UserEmailChangeConfirm struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserEmailChangeConfirmConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserEmailChangeConfirm creates a new [UserEmailChangeConfirm]
|
||||
@@ -29,7 +29,7 @@ type UserEmailChangeConfirmConfig struct {
|
||||
//
|
||||
// This factory method is used primarily for convenience (and backward compatibility).
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
|
||||
func NewUserEmailChangeConfirm(app core.App) *UserEmailChangeConfirm {
|
||||
return NewUserEmailChangeConfirmWithConfig(UserEmailChangeConfirmConfig{
|
||||
App: app,
|
||||
@@ -45,8 +45,8 @@ func NewUserEmailChangeConfirmWithConfig(config UserEmailChangeConfirmConfig) *U
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -103,12 +103,12 @@ func (form *UserEmailChangeConfirm) parseToken(token string) (*models.User, stri
|
||||
}
|
||||
|
||||
// ensure that there aren't other users with the new email
|
||||
if !form.config.TxDao.IsUserEmailUnique(newEmail, "") {
|
||||
if !form.config.Dao.IsUserEmailUnique(newEmail, "") {
|
||||
return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail)
|
||||
}
|
||||
|
||||
// verify that the token is not expired and its signature is valid
|
||||
user, err := form.config.TxDao.FindUserByToken(
|
||||
user, err := form.config.Dao.FindUserByToken(
|
||||
token,
|
||||
form.config.App.Settings().UserEmailChangeToken.Secret,
|
||||
)
|
||||
@@ -135,7 +135,7 @@ func (form *UserEmailChangeConfirm) Submit() (*models.User, error) {
|
||||
user.Verified = true
|
||||
user.RefreshTokenKey() // invalidate old tokens
|
||||
|
||||
if err := form.config.TxDao.SaveUser(user); err != nil {
|
||||
if err := form.config.Dao.SaveUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,15 @@ type UserEmailChangeRequest struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserEmailChangeRequestConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserEmailChangeRequest creates a new [UserEmailChangeRequest]
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
|
||||
func NewUserEmailChangeRequest(app core.App, user *models.User) *UserEmailChangeRequest {
|
||||
return NewUserEmailChangeRequestWithConfig(UserEmailChangeRequestConfig{
|
||||
App: app,
|
||||
@@ -48,8 +48,8 @@ func NewUserEmailChangeRequestWithConfig(config UserEmailChangeRequestConfig, us
|
||||
panic("Invalid initializer config or nil user model.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -62,7 +62,7 @@ func (form *UserEmailChangeRequest) Validate() error {
|
||||
&form.NewEmail,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
validation.By(form.checkUniqueEmail),
|
||||
),
|
||||
)
|
||||
@@ -71,7 +71,7 @@ func (form *UserEmailChangeRequest) Validate() error {
|
||||
func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error {
|
||||
v, _ := value.(string)
|
||||
|
||||
if !form.config.TxDao.IsUserEmailUnique(v, "") {
|
||||
if !form.config.Dao.IsUserEmailUnique(v, "") {
|
||||
return validation.NewError("validation_user_email_exists", "User email already exists.")
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ type UserEmailLogin struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserEmailLoginConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserEmailLogin creates a new [UserEmailLogin] form with
|
||||
@@ -29,7 +29,7 @@ type UserEmailLoginConfig struct {
|
||||
//
|
||||
// This factory method is used primarily for convenience (and backward compatibility).
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserEmailLoginWithConfig] with explicitly set TxDao.
|
||||
// [NewUserEmailLoginWithConfig] with explicitly set Dao.
|
||||
func NewUserEmailLogin(app core.App) *UserEmailLogin {
|
||||
return NewUserEmailLoginWithConfig(UserEmailLoginConfig{
|
||||
App: app,
|
||||
@@ -45,8 +45,8 @@ func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin {
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -55,7 +55,7 @@ func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin {
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *UserEmailLogin) Validate() error {
|
||||
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)),
|
||||
)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (form *UserEmailLogin) Submit() (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByEmail(form.Email)
|
||||
user, err := form.config.Dao.FindUserByEmail(form.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+71
-44
@@ -35,15 +35,15 @@ type UserOauth2Login struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserOauth2LoginConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserOauth2Login creates a new [UserOauth2Login] form with
|
||||
// initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserOauth2LoginWithConfig] with explicitly set TxDao.
|
||||
// [NewUserOauth2LoginWithConfig] with explicitly set Dao.
|
||||
func NewUserOauth2Login(app core.App) *UserOauth2Login {
|
||||
return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{
|
||||
App: app,
|
||||
@@ -59,8 +59,8 @@ func NewUserOauth2LoginWithConfig(config UserOauth2LoginConfig) *UserOauth2Login
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -99,8 +99,11 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// load provider configuration
|
||||
config := form.config.App.Settings().NamedAuthProviderConfigs()[form.Provider]
|
||||
config.SetupProvider(provider)
|
||||
if err := config.SetupProvider(provider); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
provider.SetRedirectUrl(form.RedirectUrl)
|
||||
|
||||
@@ -113,55 +116,79 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// fetch auth user
|
||||
// fetch external auth user
|
||||
authData, err := provider.FetchAuthUser(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// login/register the auth user
|
||||
user, _ := form.config.TxDao.FindUserByEmail(authData.Email)
|
||||
if user != nil {
|
||||
// update the existing user's verified state
|
||||
if !user.Verified {
|
||||
var user *models.User
|
||||
|
||||
// check for existing relation with the external auth user
|
||||
rel, _ := form.config.Dao.FindExternalAuthByProvider(form.Provider, authData.Id)
|
||||
if rel != nil {
|
||||
user, err = form.config.Dao.FindUserById(rel.UserId)
|
||||
if err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
} else if authData.Email != "" {
|
||||
// look for an existing user by the external user's email
|
||||
user, _ = form.config.Dao.FindUserByEmail(authData.Email)
|
||||
}
|
||||
|
||||
if user == nil && !config.AllowRegistrations {
|
||||
return nil, authData, errors.New("New users registration is not allowed for the authorized provider.")
|
||||
}
|
||||
|
||||
saveErr := form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
|
||||
if user == nil {
|
||||
user = &models.User{}
|
||||
user.Verified = true
|
||||
if err := form.config.TxDao.SaveUser(user); err != nil {
|
||||
return nil, authData, err
|
||||
user.Email = authData.Email
|
||||
user.SetPassword(security.RandomString(30))
|
||||
|
||||
// create the new user
|
||||
if err := txDao.SaveUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// update the existing user empty email if the authData has one
|
||||
// (this in case previously the user was created with
|
||||
// an OAuth2 provider that didn't return an email address)
|
||||
if user.Email == "" && authData.Email != "" {
|
||||
user.Email = authData.Email
|
||||
if err := txDao.SaveUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update the existing user verified state
|
||||
// (only if the user doesn't have an email or the user email match with the one in authData)
|
||||
if !user.Verified && (user.Email == "" || user.Email == authData.Email) {
|
||||
user.Verified = true
|
||||
if err := txDao.SaveUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return user, authData, nil
|
||||
}
|
||||
|
||||
if !config.AllowRegistrations {
|
||||
// registration of new users is not allowed via the Oauth2 provider
|
||||
return nil, authData, errors.New("Cannot find user with the authorized email.")
|
||||
}
|
||||
// create ExternalAuth relation if missing
|
||||
if rel == nil {
|
||||
rel = &models.ExternalAuth{
|
||||
UserId: user.Id,
|
||||
Provider: form.Provider,
|
||||
ProviderId: authData.Id,
|
||||
}
|
||||
if err := txDao.SaveExternalAuth(rel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create new user
|
||||
user = &models.User{Verified: true}
|
||||
upsertForm := NewUserUpsertWithConfig(UserUpsertConfig{
|
||||
App: form.config.App,
|
||||
TxDao: form.config.TxDao,
|
||||
}, user)
|
||||
upsertForm.Email = authData.Email
|
||||
upsertForm.Password = security.RandomString(30)
|
||||
upsertForm.PasswordConfirm = upsertForm.Password
|
||||
return nil
|
||||
})
|
||||
|
||||
event := &core.UserOauth2RegisterEvent{
|
||||
User: user,
|
||||
AuthData: authData,
|
||||
}
|
||||
|
||||
if err := form.config.App.OnUserBeforeOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
|
||||
if err := upsertForm.Submit(); err != nil {
|
||||
return nil, authData, err
|
||||
}
|
||||
|
||||
if err := form.config.App.OnUserAfterOauth2Register().Trigger(event); err != nil {
|
||||
return nil, authData, err
|
||||
if saveErr != nil {
|
||||
return nil, authData, saveErr
|
||||
}
|
||||
|
||||
return user, authData, nil
|
||||
|
||||
@@ -22,15 +22,15 @@ type UserPasswordResetConfirm struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserPasswordResetConfirmConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserPasswordResetConfirm creates a new [UserPasswordResetConfirm]
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserPasswordResetConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewUserPasswordResetConfirmWithConfig] with explicitly set Dao.
|
||||
func NewUserPasswordResetConfirm(app core.App) *UserPasswordResetConfirm {
|
||||
return NewUserPasswordResetConfirmWithConfig(UserPasswordResetConfirmConfig{
|
||||
App: app,
|
||||
@@ -46,8 +46,8 @@ func NewUserPasswordResetConfirmWithConfig(config UserPasswordResetConfirmConfig
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -70,7 +70,7 @@ func (form *UserPasswordResetConfirm) checkToken(value any) error {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByToken(
|
||||
user, err := form.config.Dao.FindUserByToken(
|
||||
v,
|
||||
form.config.App.Settings().UserPasswordResetToken.Secret,
|
||||
)
|
||||
@@ -88,7 +88,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByToken(
|
||||
user, err := form.config.Dao.FindUserByToken(
|
||||
form.Token,
|
||||
form.config.App.Settings().UserPasswordResetToken.Secret,
|
||||
)
|
||||
@@ -100,7 +100,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := form.config.TxDao.SaveUser(user); err != nil {
|
||||
if err := form.config.Dao.SaveUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type UserPasswordResetRequest struct {
|
||||
// NB! App is required struct member.
|
||||
type UserPasswordResetRequestConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
Dao *daos.Dao
|
||||
ResendThreshold float64 // in seconds
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type UserPasswordResetRequestConfig struct {
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserPasswordResetRequestWithConfig] with explicitly set TxDao.
|
||||
// [NewUserPasswordResetRequestWithConfig] with explicitly set Dao.
|
||||
func NewUserPasswordResetRequest(app core.App) *UserPasswordResetRequest {
|
||||
return NewUserPasswordResetRequestWithConfig(UserPasswordResetRequestConfig{
|
||||
App: app,
|
||||
@@ -50,8 +50,8 @@ func NewUserPasswordResetRequestWithConfig(config UserPasswordResetRequestConfig
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -66,7 +66,7 @@ func (form *UserPasswordResetRequest) Validate() error {
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func (form *UserPasswordResetRequest) Submit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByEmail(form.Email)
|
||||
user, err := form.config.Dao.FindUserByEmail(form.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,5 +96,5 @@ func (form *UserPasswordResetRequest) Submit() error {
|
||||
// update last sent timestamp
|
||||
user.LastResetSentAt = types.NowDateTime()
|
||||
|
||||
return form.config.TxDao.SaveUser(user)
|
||||
return form.config.Dao.SaveUser(user)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ type UserUpsert struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserUpsertConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserUpsert creates a new [UserUpsert] form with initializer
|
||||
@@ -37,7 +37,7 @@ type UserUpsertConfig struct {
|
||||
// (for create you could pass a pointer to an empty User - `&models.User{}`).
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewUserEmailChangeConfirmWithConfig] with explicitly set Dao.
|
||||
func NewUserUpsert(app core.App, user *models.User) *UserUpsert {
|
||||
return NewUserUpsertWithConfig(UserUpsertConfig{
|
||||
App: app,
|
||||
@@ -57,8 +57,8 @@ func NewUserUpsertWithConfig(config UserUpsertConfig, user *models.User) *UserUp
|
||||
panic("Invalid initializer config or nil upsert model.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
// load defaults
|
||||
@@ -83,7 +83,7 @@ func (form *UserUpsert) Validate() error {
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
validation.By(form.checkEmailDomain),
|
||||
validation.By(form.checkUniqueEmail),
|
||||
),
|
||||
@@ -103,7 +103,7 @@ func (form *UserUpsert) Validate() error {
|
||||
func (form *UserUpsert) checkUniqueEmail(value any) error {
|
||||
v, _ := value.(string)
|
||||
|
||||
if v == "" || form.config.TxDao.IsUserEmailUnique(v, form.user.Id) {
|
||||
if v == "" || form.config.Dao.IsUserEmailUnique(v, form.user.Id) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -160,6 +160,6 @@ func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error {
|
||||
form.user.Email = form.Email
|
||||
|
||||
return runInterceptors(func() error {
|
||||
return form.config.TxDao.SaveUser(form.user)
|
||||
return form.config.Dao.SaveUser(form.user)
|
||||
}, interceptors...)
|
||||
}
|
||||
|
||||
@@ -19,15 +19,15 @@ type UserVerificationConfirm struct {
|
||||
//
|
||||
// NB! App is required struct member.
|
||||
type UserVerificationConfirmConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
App core.App
|
||||
Dao *daos.Dao
|
||||
}
|
||||
|
||||
// NewUserVerificationConfirm creates a new [UserVerificationConfirm]
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserVerificationConfirmWithConfig] with explicitly set TxDao.
|
||||
// [NewUserVerificationConfirmWithConfig] with explicitly set Dao.
|
||||
func NewUserVerificationConfirm(app core.App) *UserVerificationConfirm {
|
||||
return NewUserVerificationConfirmWithConfig(UserVerificationConfirmConfig{
|
||||
App: app,
|
||||
@@ -43,8 +43,8 @@ func NewUserVerificationConfirmWithConfig(config UserVerificationConfirmConfig)
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -63,7 +63,7 @@ func (form *UserVerificationConfirm) checkToken(value any) error {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByToken(
|
||||
user, err := form.config.Dao.FindUserByToken(
|
||||
v,
|
||||
form.config.App.Settings().UserVerificationToken.Secret,
|
||||
)
|
||||
@@ -81,7 +81,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByToken(
|
||||
user, err := form.config.Dao.FindUserByToken(
|
||||
form.Token,
|
||||
form.config.App.Settings().UserVerificationToken.Secret,
|
||||
)
|
||||
@@ -95,7 +95,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) {
|
||||
|
||||
user.Verified = true
|
||||
|
||||
if err := form.config.TxDao.SaveUser(user); err != nil {
|
||||
if err := form.config.Dao.SaveUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type UserVerificationRequest struct {
|
||||
// NB! App is required struct member.
|
||||
type UserVerificationRequestConfig struct {
|
||||
App core.App
|
||||
TxDao *daos.Dao
|
||||
Dao *daos.Dao
|
||||
ResendThreshold float64 // in seconds
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type UserVerificationRequestConfig struct {
|
||||
// form with initializer config created from the provided [core.App] instance.
|
||||
//
|
||||
// If you want to submit the form as part of another transaction, use
|
||||
// [NewUserVerificationRequestWithConfig] with explicitly set TxDao.
|
||||
// [NewUserVerificationRequestWithConfig] with explicitly set Dao.
|
||||
func NewUserVerificationRequest(app core.App) *UserVerificationRequest {
|
||||
return NewUserVerificationRequestWithConfig(UserVerificationRequestConfig{
|
||||
App: app,
|
||||
@@ -50,8 +50,8 @@ func NewUserVerificationRequestWithConfig(config UserVerificationRequestConfig)
|
||||
panic("Missing required config.App instance.")
|
||||
}
|
||||
|
||||
if form.config.TxDao == nil {
|
||||
form.config.TxDao = form.config.App.Dao()
|
||||
if form.config.Dao == nil {
|
||||
form.config.Dao = form.config.App.Dao()
|
||||
}
|
||||
|
||||
return form
|
||||
@@ -66,7 +66,7 @@ func (form *UserVerificationRequest) Validate() error {
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.Email,
|
||||
is.EmailFormat,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func (form *UserVerificationRequest) Submit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := form.config.TxDao.FindUserByEmail(form.Email)
|
||||
user, err := form.config.Dao.FindUserByEmail(form.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,5 +100,5 @@ func (form *UserVerificationRequest) Submit() error {
|
||||
// update last sent timestamp
|
||||
user.LastVerificationSentAt = types.NowDateTime()
|
||||
|
||||
return form.config.TxDao.SaveUser(user)
|
||||
return form.config.Dao.SaveUser(user)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package validators
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
)
|
||||
@@ -47,10 +47,10 @@ func UploadedFileMimeType(validTypes []string) validation.RuleFunc {
|
||||
return validation.NewError("validation_invalid_mime_type", "Unsupported file type.")
|
||||
}
|
||||
|
||||
filetype := http.DetectContentType(v.Bytes())
|
||||
filetype := mimetype.Detect(v.Bytes())
|
||||
|
||||
for _, t := range validTypes {
|
||||
if t == filetype {
|
||||
if filetype.Is(t) {
|
||||
return nil // valid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func (validator *RecordDataValidator) checkEmailValue(field *schema.SchemaField,
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
if is.Email.Validate(val) != nil {
|
||||
if is.EmailFormat.Validate(val) != nil {
|
||||
return validation.NewError("validation_invalid_email", "Must be a valid email")
|
||||
}
|
||||
|
||||
|
||||
@@ -4,83 +4,81 @@ go 1.18
|
||||
|
||||
require (
|
||||
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/domodwyer/mailyak/v3 v3.3.3
|
||||
github.com/domodwyer/mailyak/v3 v3.3.4
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/ganigeorgiev/fexpr v0.1.1
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||
github.com/mattn/go-sqlite3 v1.14.14
|
||||
github.com/microcosm-cc/bluemonday v1.0.19
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/pocketbase/dbx v1.6.0
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
gocloud.dev v0.25.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
|
||||
modernc.org/sqlite v1.17.3
|
||||
gocloud.dev v0.26.0
|
||||
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503
|
||||
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
|
||||
modernc.org/sqlite v1.18.1
|
||||
)
|
||||
|
||||
require (
|
||||
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/aws/protocol/eventstream v1.4.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5 // 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/checksum v1.1.9 // 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/s3shared v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.9 // indirect
|
||||
github.com/aws/smithy-go v1.12.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
|
||||
github.com/aws/smithy-go v1.12.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // 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-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/image v0.0.0-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/net v0.0.0-20220706163947-c90051bbdb60 // indirect
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
golang.org/x/tools v0.1.12 // 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/genproto v0.0.0-20220706132729-d86698d07c53 // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.14 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.36.3 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/libc v1.17.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.2.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.2 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
|
||||
@@ -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.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.48 h1:jLDC9RsNoYMLFlKpB8LdqUnoDdC2yvkS4QbuyPQJ8+M=
|
||||
github.com/aws/aws-sdk-go v1.44.48/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.85 h1:JM2rkKY/GtTDCQXW0StkImbLn6n4Q/Dm2bj+u1rm7Kw=
|
||||
github.com/aws/aws-sdk-go v1.44.85/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go-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.7/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=
|
||||
github.com/aws/aws-sdk-go-v2/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.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 h1:zfT11pa7ifu/VlLDpmc5OY2W4nYmnKkFDGeMVnmqAI0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4/go.mod h1:ES0I1GBs+YYgcDS1ek47Erbn4TOL811JKqBXtgzqyZ8=
|
||||
github.com/aws/aws-sdk-go-v2/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.15.13/go.mod h1:AcMu50uhV6wMBUlURnEXhr9b3fX6FLSTlEV89krTEGk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0=
|
||||
github.com/aws/aws-sdk-go-v2/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.8/go.mod h1:P2Hd4Sy7mXRxPNcQMPBmqszSJoDXexX8XEDaT6lucO0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY=
|
||||
github.com/aws/aws-sdk-go-v2/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.8/go.mod h1:oL1Q3KuCq1D4NykQnIvtRiBGLUXhcpY5pl6QZB2XEPU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/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.19/go.mod h1:koLPv2oF6ksE3zBKLDP0GFmKfaCmYwVHqGIbaPrHIRg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 h1:xFXIMBci0UXStoOHq/8w0XIZPB2hgb9CD7uATJhqt10=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27/go.mod h1:+tj2cHQkChanggNZn1J2fJ1Cv6RO1TV0AA3472do31I=
|
||||
github.com/aws/aws-sdk-go-v2/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.14/go.mod h1:kdjrMwHwrC3+FsKhNcCMJ7tUVj/8uSD5CZXeQ4wV6fM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/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.8/go.mod h1:ZIV8GYoC6WLBW5KGs+o4rsc65/ozd+eQ0L31XF5VDwk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 h1:5mvQDtNWtI6H56+E4LUnLWEmATMB7oEh+Z9RurtIuC0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/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.15/go.mod h1:Tkrthp/0sNBShQQsamR7j/zY4p19tVTAs+nnqhH6R3c=
|
||||
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.5/go.mod h1:aIwFF3dUk95ocCcA3zfk3nhz0oLkpzHFWuMp8l/4nNs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 h1:g5qq9sgtEzt2szMaDqQO6fqKe026T6dHTFJp5NsPzkQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 h1:agLpf3vtYX1rtKTrOGpevdP3iC2W0hKDmzmhhxJzL+A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9/go.mod h1:cv+n1mdyh+0B8tAtlEBzTYFA2Uv15SISEn6kabYhIgE=
|
||||
github.com/aws/aws-sdk-go-v2/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.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 h1:g1ITJ9i9ixa+/WVggLNK20KyliAA8ltnuxfZEDfo2hM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5/go.mod h1:oehQLbMQkppKLXvpx/1Eo0X47Fe+0971DXC9UjGnKcI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/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.9/go.mod h1:EF5RLnD9l0xvEWwMRcktIS/dI6lF8lU5eV3B13k6sWo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 h1:3GamN8jcdz/a3nvL/ZVtoH/6xxeshfsiXj5O+6GW4Rg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13/go.mod h1:89CSPn69UECDLVn0H6FwKNgbtirksl8C8i3aBeeeihw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/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.8/go.mod h1:rDVhIMAX9N2r8nWxDUlbubvvaFMnfsm+3jAV7q+rpM4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/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.8/go.mod h1:JlVwmWtT/1c5W+6oUsjXjAJ0iJZ+hlghdrDy/8JxGCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 h1:QFjSOmHSb77qRTv7KI9UFon9X5wLWY5/M+6la3dTcZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12/go.mod h1:MADjAN0GHFDuc5lRa5Y5ki+oIO/w7X4qczHy+OUx0IA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 h1:OKQIQ0QhEBmGr2LfT952meIZz3ujrPYnxH+dO/5ldnI=
|
||||
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 h1:h9qqTedYnA9JcWjKyLV6UYIMSdp91ExLCUbjbpDLH7A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5/go.mod h1:J8SS5Tp/zeLxaubB0xGfKnVrvssNBNLwTipreTKLhjQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
|
||||
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.11 h1:XOJWXNFXJyapJqQuCIPfftsOf0XZZioM0kK6OPRt9MY=
|
||||
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 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=
|
||||
github.com/aws/aws-sdk-go-v2/service/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.9/go.mod h1:O1IvkYxr+39hRf960Us6j0x1P8pDqhTX+oXM5kQNl/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0=
|
||||
github.com/aws/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.0/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/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=
|
||||
github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -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/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
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.3/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||
github.com/domodwyer/mailyak/v3 v3.3.4 h1:AG/pvcz2/ocFqZkPEG7lPAa0MhCq1warfUEKJt6Fagk=
|
||||
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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
github.com/ganigeorgiev/fexpr v0.1.1 h1:La9kYEgTcIutvOnqNZ8pOUD0O0Q/Gn15sTVEX+IeBE8=
|
||||
github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@@ -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.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.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.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/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/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=
|
||||
@@ -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/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/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.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/v2 v2.0.0/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.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.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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.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.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.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-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
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/mapstructure v1.3.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.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk=
|
||||
gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y=
|
||||
gocloud.dev v0.26.0 h1:4rM/SVL0lLs+rhC0Gmc+gt/82DBpb7nbpIZKXXnfMXg=
|
||||
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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
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-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-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 h1:vJ2V3lFLg+bBhgroYuRfyN583UzVveQmIXjc8T/y3to=
|
||||
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -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-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-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
|
||||
golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -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-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-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-20220225172249-27dd8689420f/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-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-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM=
|
||||
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes=
|
||||
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -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-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 h1:VnGaRqoLmqZH/3TMLJwYCEWkR4j1nuIU1U9TvbqsDUw=
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -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-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-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-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-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-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-20220128215802-99c3d69c2c27/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-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-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -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-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-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -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.4/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.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
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-20190513163551-3ee3066db522/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.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||
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.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.94.0 h1:KtKM9ru3nzQioV1HLlUf1cR7vMYJIpgls5VhAYQXIwA=
|
||||
google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -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-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-20220706132729-d86698d07c53 h1:liFd7OL799HvMNYG5xozhUoWDj944y+zXPDOhu4PyaM=
|
||||
google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI=
|
||||
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -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.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
|
||||
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/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=
|
||||
@@ -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/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.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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
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.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg=
|
||||
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||
modernc.org/libc v1.16.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/libc v1.17.0 h1:nbL2Lv0I323wLc1GmTh/AqVtI9JeBVc7Nhapdg9EONs=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.2.0 h1:zXehBrt9n+Pjn+4RoRCZ0KqRA/0ePFqcecxZ/hXCIVw=
|
||||
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI=
|
||||
modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k=
|
||||
modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8=
|
||||
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.2 h1:iFBDH6j1Z0bN/Q9udJnnFoFpENA4252qe/7/5woE5MI=
|
||||
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/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
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/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
+4
-4
@@ -3,12 +3,12 @@ package mails
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/mails/templates"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tokens"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
actionUrl, urlErr := normalizeUrl(fmt.Sprintf(
|
||||
"%s/#/confirm-password-reset/%s",
|
||||
strings.TrimSuffix(app.Settings().Meta.AppUrl, "/"),
|
||||
actionUrl, urlErr := rest.NormalizeUrl(fmt.Sprintf(
|
||||
"%s/_/#/confirm-password-reset/%s",
|
||||
app.Settings().Meta.AppUrl,
|
||||
token,
|
||||
))
|
||||
if urlErr != nil {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ func TestSendAdminPasswordReset(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedParts := []string{
|
||||
"http://localhost:8090/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
"http://localhost:8090/_/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
}
|
||||
for _, part := range expectedParts {
|
||||
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
|
||||
|
||||
@@ -4,34 +4,9 @@ package mails
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"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.
|
||||
func resolveTemplateContent(data any, content ...string) (string, error) {
|
||||
if len(content) == 0 {
|
||||
|
||||
@@ -17,7 +17,6 @@ const AdminPasswordResetBody = `
|
||||
|
||||
<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 did not request to reset your password, please ignore this email and the link will expire on its own.</i></p>
|
||||
|
||||
@@ -50,12 +50,6 @@ const Layout = `
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.fallback-link {
|
||||
display: none !important;
|
||||
word-break: break-all;
|
||||
font-size: 11px;
|
||||
color: #666f75;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
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}}
|
||||
`
|
||||
@@ -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}}
|
||||
`
|
||||
@@ -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
@@ -1,8 +1,8 @@
|
||||
package mails
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/mails/templates"
|
||||
@@ -10,46 +10,6 @@ import (
|
||||
"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.
|
||||
func SendUserPasswordReset(app core.App, user *models.User) error {
|
||||
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 {
|
||||
body, err := prepareUserEmailBody(
|
||||
app,
|
||||
user,
|
||||
token,
|
||||
app.Settings().Meta.UserResetPasswordUrl,
|
||||
templates.UserPasswordResetBody,
|
||||
)
|
||||
settings := app.Settings()
|
||||
|
||||
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ResetPasswordTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.MailClient.Send(
|
||||
mail.Address{
|
||||
Name: app.Settings().Meta.SenderName,
|
||||
Address: app.Settings().Meta.SenderAddress,
|
||||
Name: settings.Meta.SenderName,
|
||||
Address: settings.Meta.SenderAddress,
|
||||
},
|
||||
mail.Address{Address: e.User.Email},
|
||||
("Reset your " + app.Settings().Meta.AppName + " password"),
|
||||
subject,
|
||||
body,
|
||||
nil,
|
||||
)
|
||||
@@ -112,24 +68,20 @@ func SendUserVerification(app core.App, user *models.User) error {
|
||||
}
|
||||
|
||||
sendErr := app.OnMailerBeforeUserVerificationSend().Trigger(event, func(e *core.MailerUserEvent) error {
|
||||
body, err := prepareUserEmailBody(
|
||||
app,
|
||||
user,
|
||||
token,
|
||||
app.Settings().Meta.UserVerificationUrl,
|
||||
templates.UserVerificationBody,
|
||||
)
|
||||
settings := app.Settings()
|
||||
|
||||
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.VerificationTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.MailClient.Send(
|
||||
mail.Address{
|
||||
Name: app.Settings().Meta.SenderName,
|
||||
Address: app.Settings().Meta.SenderAddress,
|
||||
Name: settings.Meta.SenderName,
|
||||
Address: settings.Meta.SenderAddress,
|
||||
},
|
||||
mail.Address{Address: e.User.Email},
|
||||
("Verify your " + app.Settings().Meta.AppName + " email"),
|
||||
subject,
|
||||
body,
|
||||
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 {
|
||||
body, err := prepareUserEmailBody(
|
||||
app,
|
||||
user,
|
||||
token,
|
||||
app.Settings().Meta.UserConfirmEmailChangeUrl,
|
||||
templates.UserConfirmEmailChangeBody,
|
||||
)
|
||||
settings := app.Settings()
|
||||
|
||||
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ConfirmEmailChangeTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.MailClient.Send(
|
||||
mail.Address{
|
||||
Name: app.Settings().Meta.SenderName,
|
||||
Address: app.Settings().Meta.SenderAddress,
|
||||
Name: settings.Meta.SenderName,
|
||||
Address: settings.Meta.SenderAddress,
|
||||
},
|
||||
mail.Address{Address: newEmail},
|
||||
("Confirm your " + app.Settings().Meta.AppName + " new email address"),
|
||||
subject,
|
||||
body,
|
||||
nil,
|
||||
)
|
||||
@@ -190,3 +138,30 @@ func SendUserChangeEmail(app core.App, user *models.User, newEmail string) error
|
||||
|
||||
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
@@ -27,7 +27,7 @@ func TestSendUserPasswordReset(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedParts := []string{
|
||||
"http://localhost:8090/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
"http://localhost:8090/_/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
}
|
||||
for _, part := range expectedParts {
|
||||
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
|
||||
@@ -52,7 +52,7 @@ func TestSendUserVerification(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedParts := []string{
|
||||
"http://localhost:8090/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
"http://localhost:8090/_/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
}
|
||||
for _, part := range expectedParts {
|
||||
if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) {
|
||||
@@ -77,7 +77,7 @@ func TestSendUserChangeEmail(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedParts := []string{
|
||||
"http://localhost:8090/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
"http://localhost:8090/_/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
|
||||
}
|
||||
for _, part := range expectedParts {
|
||||
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
@@ -9,8 +9,13 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// DefaultIdLength is the default length of the generated model id.
|
||||
const DefaultIdLength = 15
|
||||
const (
|
||||
// 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.
|
||||
type ColumnValueMapper interface {
|
||||
@@ -98,7 +103,7 @@ func (m *BaseModel) GetUpdated() types.DateTime {
|
||||
//
|
||||
// The generated id is a cryptographically random 15 characters length string.
|
||||
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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -18,7 +18,8 @@ type Request struct {
|
||||
Method string `db:"method" json:"method"`
|
||||
Status int `db:"status" json:"status"`
|
||||
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"`
|
||||
UserAgent string `db:"userAgent" json:"userAgent"`
|
||||
Meta types.JsonMap `db:"meta" json:"meta"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"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/types"
|
||||
"github.com/spf13/cast"
|
||||
@@ -458,7 +459,10 @@ func (o FileOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, 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),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1247,29 +1247,11 @@ func TestFileOptionsValidate(t *testing.T) {
|
||||
[]string{"thumbs"},
|
||||
},
|
||||
{
|
||||
"invalid thumbs format - zero width",
|
||||
"invalid thumbs format - zero width and height",
|
||||
schema.FileOptions{
|
||||
MaxSize: 1,
|
||||
MaxSelect: 2,
|
||||
Thumbs: []string{"0x100"},
|
||||
},
|
||||
[]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"},
|
||||
Thumbs: []string{"0x0", "0x0t", "0x0b", "0x0f"},
|
||||
},
|
||||
[]string{"thumbs"},
|
||||
},
|
||||
@@ -1278,7 +1260,10 @@ func TestFileOptionsValidate(t *testing.T) {
|
||||
schema.FileOptions{
|
||||
MaxSize: 1,
|
||||
MaxSelect: 2,
|
||||
Thumbs: []string{"100x100", "200x100", "1x1"},
|
||||
Thumbs: []string{
|
||||
"100x100", "200x100", "0x100", "100x0",
|
||||
"10x10t", "10x10b", "10x10f",
|
||||
},
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
|
||||
Generated
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
+76
-71
@@ -26,50 +26,60 @@ type appWrapper struct {
|
||||
|
||||
// PocketBase defines a PocketBase app launcher.
|
||||
//
|
||||
// It implements [core.App] via embedding and all of interface methods
|
||||
// It implements [core.App] via embedding and all of the app interface methods
|
||||
// could be accessed directly through the instance (eg. PocketBase.DataDir()).
|
||||
type PocketBase struct {
|
||||
*appWrapper
|
||||
|
||||
// RootCmd is the main cli command
|
||||
debugFlag bool
|
||||
dataDirFlag string
|
||||
encryptionEnvFlag string
|
||||
hideStartBanner bool
|
||||
|
||||
// RootCmd is the main console command
|
||||
RootCmd *cobra.Command
|
||||
|
||||
// console flags
|
||||
debugFlag bool
|
||||
dataDirFlag string
|
||||
encryptionEnv string
|
||||
|
||||
// console flag fallback values
|
||||
defaultDebug bool
|
||||
defaultDataDir string
|
||||
defaultEncryptionEnv string
|
||||
|
||||
// serve start banner
|
||||
showStartBanner bool
|
||||
}
|
||||
|
||||
// New creates a new PocketBase instance.
|
||||
// Config is the PocketBase initialization config struct.
|
||||
type Config struct {
|
||||
// optional default values for the console flags
|
||||
DefaultDebug bool
|
||||
DefaultDataDir string // if not set, it will fallback to "./pb_data"
|
||||
DefaultEncryptionEnv string
|
||||
|
||||
// hide the default console server info on app startup
|
||||
HideStartBanner bool
|
||||
}
|
||||
|
||||
// New creates a new PocketBase instance with the default configuration.
|
||||
// Use [NewWithConfig()] if you want to provide a custom configuration.
|
||||
//
|
||||
// Note that the application will not be initialized/bootstrapped yet,
|
||||
// aka. DB connections, migrations, app settings, etc. will not be accessible.
|
||||
// Everything will be initialized when Start() is executed.
|
||||
// If you want to initialize the application before calling Start(),
|
||||
// then you'll have to manually call Bootstrap().
|
||||
// Everything will be initialized when [Start()] is executed.
|
||||
// If you want to initialize the application before calling [Start()],
|
||||
// then you'll have to manually call [Bootstrap()].
|
||||
func New() *PocketBase {
|
||||
// try to find the base executable directory and how it was run
|
||||
var withGoRun bool
|
||||
var baseDir string
|
||||
if strings.HasPrefix(os.Args[0], os.TempDir()) {
|
||||
// probably ran with go run...
|
||||
withGoRun = true
|
||||
baseDir, _ = os.Getwd()
|
||||
} else {
|
||||
// probably ran with go build...
|
||||
withGoRun = false
|
||||
baseDir = filepath.Dir(os.Args[0])
|
||||
}
|
||||
_, isUsingGoRun := inspectRuntime()
|
||||
|
||||
defaultDir := filepath.Join(baseDir, "pb_data")
|
||||
return NewWithConfig(Config{
|
||||
DefaultDebug: isUsingGoRun,
|
||||
})
|
||||
}
|
||||
|
||||
// NewWithConfig creates a new PocketBase instance with the provided config.
|
||||
//
|
||||
// Note that the application will not be initialized/bootstrapped yet,
|
||||
// aka. DB connections, migrations, app settings, etc. will not be accessible.
|
||||
// Everything will be initialized when [Start()] is executed.
|
||||
// If you want to initialize the application before calling [Start()],
|
||||
// then you'll have to manually call [Bootstrap()].
|
||||
func NewWithConfig(config Config) *PocketBase {
|
||||
// initialize a default data directory based on the executable baseDir
|
||||
if config.DefaultDataDir == "" {
|
||||
baseDir, _ := inspectRuntime()
|
||||
config.DefaultDataDir = filepath.Join(baseDir, "pb_data")
|
||||
}
|
||||
|
||||
pb := &PocketBase{
|
||||
RootCmd: &cobra.Command{
|
||||
@@ -84,46 +94,31 @@ func New() *PocketBase {
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
},
|
||||
defaultDebug: withGoRun,
|
||||
defaultDataDir: defaultDir,
|
||||
defaultEncryptionEnv: "",
|
||||
showStartBanner: true,
|
||||
debugFlag: config.DefaultDebug,
|
||||
dataDirFlag: config.DefaultDataDir,
|
||||
encryptionEnvFlag: config.DefaultEncryptionEnv,
|
||||
hideStartBanner: config.HideStartBanner,
|
||||
}
|
||||
|
||||
// parse base flags
|
||||
// (errors are ignored, since the full flags parsing happens on Execute())
|
||||
pb.eagerParseFlags()
|
||||
pb.eagerParseFlags(config)
|
||||
|
||||
// initialize the app instance
|
||||
pb.appWrapper = &appWrapper{core.NewBaseApp(
|
||||
pb.dataDirFlag,
|
||||
pb.encryptionEnv,
|
||||
pb.encryptionEnvFlag,
|
||||
pb.debugFlag,
|
||||
)}
|
||||
|
||||
return pb
|
||||
}
|
||||
// hide the default help command (allow only `--help` flag)
|
||||
pb.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
||||
|
||||
// DefaultDebug sets the default --debug flag value.
|
||||
func (pb *PocketBase) DefaultDebug(val bool) *PocketBase {
|
||||
pb.defaultDebug = val
|
||||
return pb
|
||||
}
|
||||
// hook the bootstrap process
|
||||
pb.RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
return pb.Bootstrap()
|
||||
}
|
||||
|
||||
// DefaultDataDir sets the default --dir flag value.
|
||||
func (pb *PocketBase) DefaultDataDir(val string) *PocketBase {
|
||||
pb.defaultDataDir = val
|
||||
return pb
|
||||
}
|
||||
|
||||
// DefaultEncryptionEnv sets the default --encryptionEnv flag value.
|
||||
func (pb *PocketBase) DefaultEncryptionEnv(val string) *PocketBase {
|
||||
pb.defaultEncryptionEnv = val
|
||||
return pb
|
||||
}
|
||||
|
||||
// ShowStartBanner shows/hides the web server start banner.
|
||||
func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
|
||||
pb.showStartBanner = val
|
||||
return pb
|
||||
}
|
||||
|
||||
@@ -131,7 +126,7 @@ func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
|
||||
// commands (serve, migrate, version) and executes pb.RootCmd.
|
||||
func (pb *PocketBase) Start() error {
|
||||
// register system commands
|
||||
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, pb.showStartBanner))
|
||||
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner))
|
||||
pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb))
|
||||
|
||||
return pb.Execute()
|
||||
@@ -143,10 +138,6 @@ func (pb *PocketBase) Start() error {
|
||||
// This method differs from pb.Start() by not registering the default
|
||||
// system commands!
|
||||
func (pb *PocketBase) Execute() error {
|
||||
if err := pb.Bootstrap(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
@@ -180,27 +171,41 @@ func (pb *PocketBase) onTerminate() error {
|
||||
|
||||
// eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute().
|
||||
// so we can have all PocketBase flags ready for use on initialization.
|
||||
func (pb *PocketBase) eagerParseFlags() error {
|
||||
func (pb *PocketBase) eagerParseFlags(config Config) error {
|
||||
pb.RootCmd.PersistentFlags().StringVar(
|
||||
&pb.dataDirFlag,
|
||||
"dir",
|
||||
pb.defaultDataDir,
|
||||
config.DefaultDataDir,
|
||||
"the PocketBase data directory",
|
||||
)
|
||||
|
||||
pb.RootCmd.PersistentFlags().StringVar(
|
||||
&pb.encryptionEnv,
|
||||
&pb.encryptionEnvFlag,
|
||||
"encryptionEnv",
|
||||
pb.defaultEncryptionEnv,
|
||||
"the env variable whose value of 32 chars will be used \nas encryption key for the app settings (default none)",
|
||||
config.DefaultEncryptionEnv,
|
||||
"the env variable whose value of 32 characters will be used \nas encryption key for the app settings (default none)",
|
||||
)
|
||||
|
||||
pb.RootCmd.PersistentFlags().BoolVar(
|
||||
&pb.debugFlag,
|
||||
"debug",
|
||||
pb.defaultDebug,
|
||||
config.DefaultDebug,
|
||||
"enable debug mode, aka. showing more detailed logs",
|
||||
)
|
||||
|
||||
return pb.RootCmd.ParseFlags(os.Args[1:])
|
||||
}
|
||||
|
||||
// tries to find the base executable directory and how it was run
|
||||
func inspectRuntime() (baseDir string, withGoRun bool) {
|
||||
if strings.HasPrefix(os.Args[0], os.TempDir()) {
|
||||
// probably ran with go run
|
||||
withGoRun = true
|
||||
baseDir, _ = os.Getwd()
|
||||
} else {
|
||||
// probably ran with go build
|
||||
withGoRun = false
|
||||
baseDir = filepath.Dir(os.Args[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+82
-36
@@ -6,14 +6,19 @@ import (
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
testDir := "./pb_test_data_dir"
|
||||
defer os.RemoveAll(testDir)
|
||||
// copy os.Args
|
||||
originalArgs := []string{}
|
||||
copy(originalArgs, os.Args)
|
||||
defer func() {
|
||||
// restore os.Args
|
||||
copy(os.Args, originalArgs)
|
||||
}()
|
||||
|
||||
// reset os.Args
|
||||
// change os.Args
|
||||
os.Args = os.Args[0:1]
|
||||
os.Args = append(
|
||||
os.Args,
|
||||
"--dir="+testDir,
|
||||
"--dir=test_dir",
|
||||
"--encryptionEnv=test_encryption_env",
|
||||
"--debug=true",
|
||||
)
|
||||
@@ -32,8 +37,8 @@ func TestNew(t *testing.T) {
|
||||
t.Fatal("Expected appWrapper to be initialized, got nil")
|
||||
}
|
||||
|
||||
if app.DataDir() != testDir {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", testDir, app.DataDir())
|
||||
if app.DataDir() != "test_dir" {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir", app.DataDir())
|
||||
}
|
||||
|
||||
if app.EncryptionEnv() != "test_encryption_env" {
|
||||
@@ -45,52 +50,93 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDebug(t *testing.T) {
|
||||
app := New()
|
||||
func TestNewWithConfig(t *testing.T) {
|
||||
app := NewWithConfig(Config{
|
||||
DefaultDebug: true,
|
||||
DefaultDataDir: "test_dir",
|
||||
DefaultEncryptionEnv: "test_encryption_env",
|
||||
HideStartBanner: true,
|
||||
})
|
||||
|
||||
app.DefaultDebug(true)
|
||||
if app.defaultDebug != true {
|
||||
t.Fatalf("Expected defaultDebug %v, got %v", true, app.defaultDebug)
|
||||
if app == nil {
|
||||
t.Fatal("Expected initialized PocketBase instance, got nil")
|
||||
}
|
||||
|
||||
app.DefaultDebug(false)
|
||||
if app.defaultDebug != false {
|
||||
t.Fatalf("Expected defaultDebug %v, got %v", false, app.defaultDebug)
|
||||
if app.RootCmd == nil {
|
||||
t.Fatal("Expected RootCmd to be initialized, got nil")
|
||||
}
|
||||
|
||||
if app.appWrapper == nil {
|
||||
t.Fatal("Expected appWrapper to be initialized, got nil")
|
||||
}
|
||||
|
||||
if app.hideStartBanner != true {
|
||||
t.Fatal("Expected app.hideStartBanner to be true, got false")
|
||||
}
|
||||
|
||||
if app.DataDir() != "test_dir" {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir", app.DataDir())
|
||||
}
|
||||
|
||||
if app.EncryptionEnv() != "test_encryption_env" {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", "test_encryption_env", app.EncryptionEnv())
|
||||
}
|
||||
|
||||
if app.IsDebug() != true {
|
||||
t.Fatal("Expected app.IsDebug() true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDataDir(t *testing.T) {
|
||||
app := New()
|
||||
func TestNewWithConfigAndFlags(t *testing.T) {
|
||||
// copy os.Args
|
||||
originalArgs := []string{}
|
||||
copy(originalArgs, os.Args)
|
||||
defer func() {
|
||||
// restore os.Args
|
||||
copy(os.Args, originalArgs)
|
||||
}()
|
||||
|
||||
expected := "test_default"
|
||||
// change os.Args
|
||||
os.Args = os.Args[0:1]
|
||||
os.Args = append(
|
||||
os.Args,
|
||||
"--dir=test_dir_flag",
|
||||
"--encryptionEnv=test_encryption_env_flag",
|
||||
"--debug=false",
|
||||
)
|
||||
|
||||
app.DefaultDataDir(expected)
|
||||
if app.defaultDataDir != expected {
|
||||
t.Fatalf("Expected defaultDataDir %v, got %v", expected, app.defaultDataDir)
|
||||
app := NewWithConfig(Config{
|
||||
DefaultDebug: true,
|
||||
DefaultDataDir: "test_dir",
|
||||
DefaultEncryptionEnv: "test_encryption_env",
|
||||
HideStartBanner: true,
|
||||
})
|
||||
|
||||
if app == nil {
|
||||
t.Fatal("Expected initialized PocketBase instance, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultEncryptionEnv(t *testing.T) {
|
||||
app := New()
|
||||
if app.RootCmd == nil {
|
||||
t.Fatal("Expected RootCmd to be initialized, got nil")
|
||||
}
|
||||
|
||||
expected := "test_env"
|
||||
if app.appWrapper == nil {
|
||||
t.Fatal("Expected appWrapper to be initialized, got nil")
|
||||
}
|
||||
|
||||
app.DefaultEncryptionEnv(expected)
|
||||
if app.defaultEncryptionEnv != expected {
|
||||
t.Fatalf("Expected defaultEncryptionEnv %v, got %v", expected, app.defaultEncryptionEnv)
|
||||
if app.hideStartBanner != true {
|
||||
t.Fatal("Expected app.hideStartBanner to be true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowStartBanner(t *testing.T) {
|
||||
app := New()
|
||||
if app.DataDir() != "test_dir_flag" {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", "test_dir_flag", app.DataDir())
|
||||
}
|
||||
|
||||
app.ShowStartBanner(true)
|
||||
if app.showStartBanner != true {
|
||||
t.Fatalf("Expected showStartBanner %v, got %v", true, app.showStartBanner)
|
||||
if app.EncryptionEnv() != "test_encryption_env_flag" {
|
||||
t.Fatalf("Expected app.DataDir() %q, got %q", "test_encryption_env_flag", app.EncryptionEnv())
|
||||
}
|
||||
|
||||
app.ShowStartBanner(false)
|
||||
if app.showStartBanner != false {
|
||||
t.Fatalf("Expected showStartBanner %v, got %v", false, app.showStartBanner)
|
||||
if app.IsDebug() != false {
|
||||
t.Fatal("Expected app.IsDebug() false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
props := strings.Split(fieldName, ".")
|
||||
|
||||
currentCollectionName := r.baseCollection.Name
|
||||
currentTableAlias := currentCollectionName
|
||||
currentTableAlias := inflector.Columnify(currentCollectionName)
|
||||
|
||||
// check for @collection field (aka. non-relational join)
|
||||
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
|
||||
@@ -106,14 +106,14 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
}
|
||||
|
||||
currentCollectionName = props[1]
|
||||
currentTableAlias = "__collection_" + currentCollectionName
|
||||
currentTableAlias = inflector.Columnify("__collection_" + currentCollectionName)
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
r.addJoin(collection.Name, currentTableAlias, nil)
|
||||
r.addJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)
|
||||
|
||||
props = props[2:] // leave only the collection fields
|
||||
} else if props[0] == "@request" {
|
||||
@@ -129,7 +129,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
|
||||
// resolve the profile collection fields
|
||||
currentCollectionName = models.ProfileCollectionName
|
||||
currentTableAlias = "__user_" + currentCollectionName
|
||||
currentTableAlias = inflector.Columnify("__user_" + currentCollectionName)
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
@@ -146,12 +146,16 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
}
|
||||
|
||||
// join the profile collection
|
||||
r.addJoin(collection.Name, currentTableAlias, dbx.NewExp(fmt.Sprintf(
|
||||
// aka. profiles.id = profileId
|
||||
"[[%s.id]] = %s",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
profileIdPlaceholder,
|
||||
), profileIdPlaceholderParam))
|
||||
r.addJoin(
|
||||
inflector.Columnify(collection.Name),
|
||||
currentTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf(
|
||||
// aka. profiles.id = profileId
|
||||
"[[%s.id]] = %s",
|
||||
currentTableAlias,
|
||||
profileIdPlaceholder,
|
||||
), profileIdPlaceholderParam),
|
||||
)
|
||||
|
||||
props = props[3:] // leave only the profile fields
|
||||
}
|
||||
@@ -168,7 +172,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
|
||||
// base model prop (always available but not part of the collection schema)
|
||||
if list.ExistInSlice(prop, baseModelFields) {
|
||||
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
field := collection.Schema.GetFieldByName(prop)
|
||||
@@ -178,7 +182,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
|
||||
// last prop
|
||||
if i == totalProps-1 {
|
||||
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
// check if it is a relation field
|
||||
@@ -198,20 +202,27 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
if relErr != nil {
|
||||
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
|
||||
}
|
||||
|
||||
cleanFieldName := inflector.Columnify(field.Name)
|
||||
newCollectionName := relCollection.Name
|
||||
newTableAlias := currentTableAlias + "_" + field.Name
|
||||
newTableAlias := currentTableAlias + "_" + cleanFieldName
|
||||
|
||||
jeTable := currentTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair := currentTableAlias + "." + cleanFieldName
|
||||
|
||||
r.addJoin(
|
||||
newCollectionName,
|
||||
fmt.Sprintf(
|
||||
// note: the case is used to normalize value access for single and multiple relations.
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
jePair, jePair, jePair,
|
||||
),
|
||||
jeTable,
|
||||
nil,
|
||||
)
|
||||
r.addJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf(
|
||||
// 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array)
|
||||
"[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
inflector.Columnify(field.Name),
|
||||
inflector.Columnify(newTableAlias),
|
||||
inflector.Columnify("id"),
|
||||
)),
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeTable)),
|
||||
)
|
||||
|
||||
currentCollectionName = newCollectionName
|
||||
@@ -296,11 +307,7 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
tableExpr := fmt.Sprintf(
|
||||
"%s %s",
|
||||
inflector.Columnify(tableName),
|
||||
inflector.Columnify(tableAlias),
|
||||
)
|
||||
tableExpr := fmt.Sprintf("%s %s", tableName, tableAlias)
|
||||
|
||||
join := join{
|
||||
id: tableAlias,
|
||||
|
||||
@@ -52,32 +52,32 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
{
|
||||
"single rel",
|
||||
[]string{"onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
},
|
||||
{
|
||||
"non-relation field + single rel",
|
||||
[]string{"title", "onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
},
|
||||
{
|
||||
"nested incomplete rels",
|
||||
[]string{"manyrels.onerel"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]]",
|
||||
},
|
||||
{
|
||||
"nested complete rels",
|
||||
[]string{"manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]]",
|
||||
},
|
||||
{
|
||||
"repeated nested rels",
|
||||
[]string{"manyrels.onerel.manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel.manyrels]]) THEN [[demo4_manyrels_onerel.manyrels]] ELSE json_array([[demo4_manyrels_onerel.manyrels]]) END) `demo4_manyrels_onerel_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel_manyrels.id]] = [[demo4_manyrels_onerel_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel_manyrels.onerel]]) THEN [[demo4_manyrels_onerel_manyrels.onerel]] ELSE json_array([[demo4_manyrels_onerel_manyrels.onerel]]) END) `demo4_manyrels_onerel_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels_onerel.id]] = [[demo4_manyrels_onerel_manyrels_onerel_je.value]]",
|
||||
},
|
||||
{
|
||||
"multiple rels",
|
||||
[]string{"manyrels.title", "onerel.onefile"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
},
|
||||
{
|
||||
"@collection join",
|
||||
@@ -95,12 +95,12 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"^" +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
|
||||
" {:.*} " +
|
||||
regexp.QuoteMeta("LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles.rel]] LIKE ('%' || [[__user_profiles_rel.id]] || '%')") +
|
||||
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__user_profiles.rel]]) THEN [[__user_profiles.rel]] ELSE json_array([[__user_profiles.rel]]) END) `__user_profiles_rel_je` LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles_rel.id]] = [[__user_profiles_rel_je.value]]") +
|
||||
"$",
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
for _, s := range scenarios {
|
||||
query := app.Dao().RecordQuery(collection)
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
@@ -109,14 +109,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
if err := r.UpdateQuery(query); err != nil {
|
||||
t.Errorf("(%d) UpdateQuery failed with error %v", i, err)
|
||||
t.Errorf("(%s) UpdateQuery failed with error %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
rawQuery := query.Build().SQL()
|
||||
|
||||
if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
|
||||
t.Errorf("(%d) Expected query\n %v \ngot:\n %v", i, s.expectQuery, rawQuery)
|
||||
t.Errorf("(%s) Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -36,13 +36,23 @@ type ApiScenario struct {
|
||||
|
||||
// test hooks
|
||||
// ---
|
||||
BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
TestAppFactory func() (*TestApp, error)
|
||||
BeforeTestFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
AfterTestFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
}
|
||||
|
||||
// Test executes the test case/scenario.
|
||||
func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
testApp, _ := NewTestApp()
|
||||
var testApp *TestApp
|
||||
var testAppErr error
|
||||
if scenario.TestAppFactory != nil {
|
||||
testApp, testAppErr = scenario.TestAppFactory()
|
||||
} else {
|
||||
testApp, testAppErr = NewTestApp()
|
||||
}
|
||||
if testAppErr != nil {
|
||||
t.Fatalf("Failed to initialize the test app instance: %v", testAppErr)
|
||||
}
|
||||
defer testApp.Cleanup()
|
||||
|
||||
e, err := apis.InitApi(testApp)
|
||||
@@ -50,8 +60,8 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if scenario.BeforeFunc != nil {
|
||||
scenario.BeforeFunc(t, testApp, e)
|
||||
if scenario.BeforeTestFunc != nil {
|
||||
scenario.BeforeTestFunc(t, testApp, e)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -135,7 +145,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if scenario.AfterFunc != nil {
|
||||
scenario.AfterFunc(t, testApp, e)
|
||||
if scenario.AfterTestFunc != nil {
|
||||
scenario.AfterTestFunc(t, testApp, e)
|
||||
}
|
||||
}
|
||||
|
||||
+33
-21
@@ -45,12 +45,21 @@ func (t *TestApp) ResetEventCalls() {
|
||||
t.EventCalls = make(map[string]int)
|
||||
}
|
||||
|
||||
// NewTestApp creates and initializes a full application instance for testing.
|
||||
// NewTestApp creates and initializes a test application instance.
|
||||
//
|
||||
// It is the caller's responsibility to call `app.Cleanup()`
|
||||
// when the app is no longer needed.
|
||||
func NewTestApp() (*TestApp, error) {
|
||||
tempDir, err := NewTempDataDir()
|
||||
func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
|
||||
var testDataDir string
|
||||
if len(optTestDataDir) == 0 || optTestDataDir[0] == "" {
|
||||
// fallback to the default test data directory
|
||||
_, currentFile, _, _ := runtime.Caller(0)
|
||||
testDataDir = filepath.Join(path.Dir(currentFile), "data")
|
||||
} else {
|
||||
testDataDir = optTestDataDir[0]
|
||||
}
|
||||
|
||||
tempDir, err := TempDirClone(testDataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -188,21 +197,26 @@ func NewTestApp() (*TestApp, error) {
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserBeforeOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error {
|
||||
t.EventCalls["OnUserBeforeOauth2Register"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserAfterOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error {
|
||||
t.EventCalls["OnUserAfterOauth2Register"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserAuthRequest().Add(func(e *core.UserAuthEvent) error {
|
||||
t.EventCalls["OnUserAuthRequest"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserListExternalAuths().Add(func(e *core.UserListExternalAuthsEvent) error {
|
||||
t.EventCalls["OnUserListExternalAuths"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserBeforeUnlinkExternalAuthRequest().Add(func(e *core.UserUnlinkExternalAuthEvent) error {
|
||||
t.EventCalls["OnUserBeforeUnlinkExternalAuthRequest"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnUserAfterUnlinkExternalAuthRequest().Add(func(e *core.UserUnlinkExternalAuthEvent) error {
|
||||
t.EventCalls["OnUserAfterUnlinkExternalAuthRequest"]++
|
||||
return nil
|
||||
})
|
||||
|
||||
t.OnMailerBeforeAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error {
|
||||
t.EventCalls["OnMailerBeforeAdminResetPasswordSend"]++
|
||||
return nil
|
||||
@@ -376,21 +390,19 @@ func NewTestApp() (*TestApp, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// NewTempDataDir creates a new temporary directory copy of the test data.
|
||||
// TempDirClone creates a new temporary directory copy from the
|
||||
// provided directory path.
|
||||
//
|
||||
// It is the caller's responsibility to call `os.RemoveAll(dir)`
|
||||
// when the directory is no longer needed.
|
||||
func NewTempDataDir() (string, error) {
|
||||
// It is the caller's responsibility to call `os.RemoveAll(tempDir)`
|
||||
// when the directory is no longer needed!
|
||||
func TempDirClone(dirToClone string) (string, error) {
|
||||
tempDir, err := os.MkdirTemp("", "pb_test_*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, currentFile, _, _ := runtime.Caller(0)
|
||||
testDataDir := filepath.Join(path.Dir(currentFile), "data")
|
||||
|
||||
// copy everything from testDataDir to tempDir
|
||||
if err := copyDir(testDataDir, tempDir); err != nil {
|
||||
if err := copyDir(dirToClone, tempDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 884 B |
+1
@@ -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=="}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
+1
@@ -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=="}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
+1
@@ -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=="}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
+1
@@ -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=="}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 884 B |
+1
@@ -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=="}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
+1
@@ -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
@@ -10,7 +10,8 @@ func MockRequestLogsData(app *TestApp) error {
|
||||
[[method]],
|
||||
[[status]],
|
||||
[[auth]],
|
||||
[[ip]],
|
||||
[[userIp]],
|
||||
[[remoteIp]],
|
||||
[[referer]],
|
||||
[[userAgent]],
|
||||
[[meta]],
|
||||
@@ -25,6 +26,7 @@ func MockRequestLogsData(app *TestApp) error {
|
||||
200,
|
||||
"guest",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1",
|
||||
"",
|
||||
"",
|
||||
"{}",
|
||||
@@ -38,6 +40,7 @@ func MockRequestLogsData(app *TestApp) error {
|
||||
400,
|
||||
"admin",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1",
|
||||
"",
|
||||
"",
|
||||
'{"errorDetails":"error_details..."}',
|
||||
|
||||
+12
-3
@@ -11,18 +11,27 @@ var _ mailer.Mailer = (*TestMailer)(nil)
|
||||
|
||||
// TestMailer is a mock `mailer.Mailer` implementation.
|
||||
type TestMailer struct {
|
||||
TotalSend int
|
||||
LastHtmlBody string
|
||||
TotalSend int
|
||||
LastFromAddress mail.Address
|
||||
LastToAddress mail.Address
|
||||
LastHtmlSubject string
|
||||
LastHtmlBody string
|
||||
}
|
||||
|
||||
// Reset clears any previously test collected data.
|
||||
func (m *TestMailer) Reset() {
|
||||
m.LastHtmlBody = ""
|
||||
m.TotalSend = 0
|
||||
m.LastFromAddress = mail.Address{}
|
||||
m.LastToAddress = mail.Address{}
|
||||
m.LastHtmlSubject = ""
|
||||
m.LastHtmlBody = ""
|
||||
}
|
||||
|
||||
// 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 {
|
||||
m.LastFromAddress = fromEmail
|
||||
m.LastToAddress = toEmail
|
||||
m.LastHtmlSubject = subject
|
||||
m.LastHtmlBody = html
|
||||
m.TotalSend++
|
||||
return nil
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type AuthUser struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
}
|
||||
@@ -90,6 +91,10 @@ func NewProviderByName(name string) (Provider, error) {
|
||||
return NewGithubProvider(), nil
|
||||
case NameGitlab:
|
||||
return NewGitlabProvider(), nil
|
||||
case NameDiscord:
|
||||
return NewDiscordProvider(), nil
|
||||
case NameTwitter:
|
||||
return NewTwitterProvider(), nil
|
||||
default:
|
||||
return nil, errors.New("Missing provider " + name)
|
||||
}
|
||||
|
||||
@@ -54,4 +54,13 @@ func TestNewProviderByName(t *testing.T) {
|
||||
if _, ok := p.(*auth.Gitlab); !ok {
|
||||
t.Error("Expected to be instance of *auth.Gitlab")
|
||||
}
|
||||
|
||||
// discord
|
||||
p, err = auth.NewProviderByName(auth.NameDiscord)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Discord); !ok {
|
||||
t.Error("Expected to be instance of *auth.Discord")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user