You've already forked pocketbase
							
							
				mirror of
				https://github.com/pocketbase/pocketbase.git
				synced 2025-10-31 08:37:38 +02:00 
			
		
		
		
	[#276] added support for linking external auths by provider id
This commit is contained in:
		
							
								
								
									
										67
									
								
								apis/user.go
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								apis/user.go
									
									
									
									
									
								
							| @@ -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 { | ||||
| @@ -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 reference. 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 | ||||
| } | ||||
|   | ||||
| @@ -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}, | ||||
| 		}, | ||||
| @@ -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, | ||||
| 			}, | ||||
| 			AfterFunc: 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, | ||||
| 			}, | ||||
| 			AfterFunc: 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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								core/app.go
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								core/app.go
									
									
									
									
									
								
							| @@ -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 auhts 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 | ||||
|   | ||||
							
								
								
									
										60
									
								
								core/base.go
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								core/base.go
									
									
									
									
									
								
							| @@ -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 | ||||
| } | ||||
|  | ||||
| // ------------------------------------------------------------------- | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
| // ------------------------------------------------------------------- | ||||
|   | ||||
							
								
								
									
										99
									
								
								daos/external_auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								daos/external_auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| 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.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 { | ||||
| 	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 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 set.") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return dao.Delete(model) | ||||
| } | ||||
							
								
								
									
										189
									
								
								daos/external_auth_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								daos/external_auth_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| 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() | ||||
|  | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
| @@ -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}, | ||||
|   | ||||
| @@ -22,15 +22,15 @@ type AdminLogin struct { | ||||
| // | ||||
| // NB! App is a required struct member. | ||||
| type AdminLoginConfig struct { | ||||
| 	App   core.App | ||||
| 	TxDao *daos.Dao | ||||
| 	App core.App | ||||
| 	Dao *daos.Dao | ||||
| } | ||||
|  | ||||
| // NewAdminLogin creates a new [AdminLogin] form with initializer | ||||
| // config created from the provided [core.App] instance. | ||||
| // | ||||
| // If you want to submit the form as part of another transaction, use | ||||
| // [NewAdminLoginWithConfig] with explicitly set TxDao. | ||||
| // [NewAdminLoginWithConfig] with explicitly set Dao. | ||||
| func NewAdminLogin(app core.App) *AdminLogin { | ||||
| 	return NewAdminLoginWithConfig(AdminLoginConfig{ | ||||
| 		App: app, | ||||
| @@ -46,8 +46,8 @@ func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin { | ||||
| 		panic("Missing required config.App instance.") | ||||
| 	} | ||||
|  | ||||
| 	if form.config.TxDao == nil { | ||||
| 		form.config.TxDao = form.config.App.Dao() | ||||
| 	if form.config.Dao == nil { | ||||
| 		form.config.Dao = form.config.App.Dao() | ||||
| 	} | ||||
|  | ||||
| 	return form | ||||
| @@ -68,7 +68,7 @@ func (form *AdminLogin) Submit() (*models.Admin, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	admin, err := form.config.TxDao.FindAdminByEmail(form.Email) | ||||
| 	admin, err := form.config.Dao.FindAdminByEmail(form.Email) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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...) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| @@ -286,7 +286,7 @@ func (form *RecordUpsert) Validate() error { | ||||
|  | ||||
| 	// record data validator | ||||
| 	dataValidator := validators.NewRecordDataValidator( | ||||
| 		form.config.TxDao, | ||||
| 		form.config.Dao, | ||||
| 		form.record, | ||||
| 		form.filesToUpload, | ||||
| 	) | ||||
| @@ -316,7 +316,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 +366,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,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), | ||||
| 		) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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,78 @@ 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 verified state | ||||
| 			if !user.Verified { | ||||
| 				user.Verified = true | ||||
| 				if err := txDao.SaveUser(user); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 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 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										76
									
								
								migrations/1661586591_add_externalAuths_table.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								migrations/1661586591_add_externalAuths_table.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package migrations | ||||
|  | ||||
| import "github.com/pocketbase/dbx" | ||||
|  | ||||
| func init() { | ||||
| 	AppMigrations.Register(func(db dbx.Builder) error { | ||||
| 		_, createErr := db.NewQuery(` | ||||
| 			CREATE TABLE {{_externalAuths}} ( | ||||
| 				[[id]]         TEXT PRIMARY KEY, | ||||
| 				[[userId]]     TEXT NOT NULL, | ||||
| 				[[provider]]   TEXT NOT NULL, | ||||
| 				[[providerId]] TEXT NOT NULL, | ||||
| 				[[created]]    TEXT DEFAULT "" NOT NULL, | ||||
| 				[[updated]]    TEXT DEFAULT "" NOT NULL, | ||||
| 				--- | ||||
| 				FOREIGN KEY ([[userId]]) REFERENCES {{_users}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE | ||||
| 			); | ||||
|  | ||||
| 			CREATE UNIQUE INDEX _externalAuths_userId_provider_idx on {{_externalAuths}} ([[userId]], [[provider]]); | ||||
| 			CREATE UNIQUE INDEX _externalAuths_provider_providerId_idx on {{_externalAuths}} ([[provider]], [[providerId]]); | ||||
| 		`).Execute() | ||||
| 		if createErr != nil { | ||||
| 			return createErr | ||||
| 		} | ||||
|  | ||||
| 		// remove the unique email index from the _users table and | ||||
| 		// replace it with partial index | ||||
| 		_, alterErr := db.NewQuery(` | ||||
| 			-- crate new users table | ||||
| 			CREATE TABLE {{_newUsers}} ( | ||||
| 				[[id]]                     TEXT PRIMARY KEY, | ||||
| 				[[verified]]               BOOLEAN DEFAULT FALSE NOT NULL, | ||||
| 				[[email]]                  TEXT DEFAULT "" NOT NULL, | ||||
| 				[[tokenKey]]               TEXT NOT NULL, | ||||
| 				[[passwordHash]]           TEXT NOT NULL, | ||||
| 				[[lastResetSentAt]]        TEXT DEFAULT "" NOT NULL, | ||||
| 				[[lastVerificationSentAt]] TEXT DEFAULT "" NOT NULL, | ||||
| 				[[created]]                TEXT DEFAULT "" NOT NULL, | ||||
| 				[[updated]]                TEXT DEFAULT "" NOT NULL | ||||
| 			); | ||||
|  | ||||
| 			-- copy all data from the old users table to the new one | ||||
| 			INSERT INTO {{_newUsers}} SELECT * FROM {{_users}}; | ||||
|  | ||||
| 			-- drop old table | ||||
| 			DROP TABLE {{_users}}; | ||||
|  | ||||
| 			-- rename new table | ||||
| 			ALTER TABLE {{_newUsers}} RENAME TO {{_users}}; | ||||
|  | ||||
| 			-- create named indexes | ||||
| 			CREATE UNIQUE INDEX _users_email_idx ON {{_users}} ([[email]]) WHERE [[email]] != ""; | ||||
| 			CREATE UNIQUE INDEX _users_tokenKey_idx ON {{_users}} ([[tokenKey]]); | ||||
| 		`).Execute() | ||||
| 		if alterErr != nil { | ||||
| 			return alterErr | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}, func(db dbx.Builder) error { | ||||
| 		if _, err := db.DropTable("_externalAuths").Execute(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// drop the partial email unique index and replace it with normal unique index | ||||
| 		_, indexErr := db.NewQuery(` | ||||
| 			DROP INDEX IF EXISTS _users_email_idx; | ||||
| 			CREATE UNIQUE INDEX _users_email_idx on {{_users}} ([[email]]); | ||||
| 		`).Execute() | ||||
| 		if indexErr != nil { | ||||
| 			return indexErr | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										15
									
								
								models/external_auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								models/external_auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| package models | ||||
|  | ||||
| var _ Model = (*ExternalAuth)(nil) | ||||
|  | ||||
| type ExternalAuth struct { | ||||
| 	BaseModel | ||||
|  | ||||
| 	UserId     string `db:"userId" json:"userId"` | ||||
| 	Provider   string `db:"provider" json:"provider"` | ||||
| 	ProviderId string `db:"providerId" json:"providerId"` | ||||
| } | ||||
|  | ||||
| func (m *ExternalAuth) TableName() string { | ||||
| 	return "_externalAuths" | ||||
| } | ||||
							
								
								
									
										14
									
								
								models/external_auth_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								models/external_auth_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package models_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/pocketbase/pocketbase/models" | ||||
| ) | ||||
|  | ||||
| func TestExternalAuthTableName(t *testing.T) { | ||||
| 	m := models.ExternalAuth{} | ||||
| 	if m.TableName() != "_externalAuths" { | ||||
| 		t.Fatalf("Unexpected table name, got %q", m.TableName()) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										25
									
								
								tests/app.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								tests/app.go
									
									
									
									
									
								
							| @@ -188,21 +188,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 | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -197,9 +197,12 @@ | ||||
|  | ||||
|                             <td class="col-type-email col-field-email"> | ||||
|                                 <div class="inline-flex"> | ||||
|                                     <span class="txt" title={user.email}> | ||||
|                                         {user.email} | ||||
|                                     </span> | ||||
|                                     {#if user.email} | ||||
|                                         <span class="txt" title={user.email}>{user.email}</span> | ||||
|                                     {:else} | ||||
|                                         <div class="txt-hint">N/A</div> | ||||
|                                     {/if} | ||||
|  | ||||
|                                     <span | ||||
|                                         class="label" | ||||
|                                         class:label-success={user.verified} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user