Skip to content

Commit

Permalink
[pocketbase#468] added record auth verification, password reset and e…
Browse files Browse the repository at this point in the history
…mail change request event hooks
  • Loading branch information
ganigeorgiev committed Dec 3, 2022
1 parent 02f7263 commit 604009b
Show file tree
Hide file tree
Showing 22 changed files with 1,013 additions and 142 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
app.OnRealtimeDisconnectRequest()
app.OnRealtimeBeforeMessageSend()
app.OnRealtimeAfterMessageSend()
app.OnRecordBeforeRequestPasswordResetRequest()
app.OnRecordAfterRequestPasswordResetRequest()
app.OnRecordBeforeConfirmPasswordResetRequest()
app.OnRecordAfterConfirmPasswordResetRequest()
app.OnRecordBeforeRequestVerificationRequest()
app.OnRecordAfterRequestVerificationRequest()
app.OnRecordBeforeConfirmVerificationRequest()
app.OnRecordAfterConfirmVerificationRequest()
app.OnRecordBeforeRequestEmailChangeRequest()
app.OnRecordAfterRequestEmailChangeRequest()
app.OnRecordBeforeConfirmEmailChangeRequest()
app.OnRecordAfterConfirmEmailChangeRequest()
```

- Refactored the `migrate` command to support **external JavaScript migration files** using an embedded JS interpreter ([goja](https://github.com/dop251/goja)).
Expand Down
4 changes: 2 additions & 2 deletions apis/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ func InitApi(app core.App) (*echo.Echo, error) {
Error: apiErr,
}

// send error response
hookErr := app.OnBeforeApiError().Trigger(event, func(e *core.ApiErrorEvent) error {
// Send response
// @see https://github.com/labstack/echo/issues/608
if e.HttpContext.Request().Method == http.MethodHead {
// @see https://github.com/labstack/echo/issues/608
return e.HttpContext.NoContent(apiErr.Code)
}

Expand Down
170 changes: 143 additions & 27 deletions apis/record_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,15 +283,39 @@ func (api *recordAuthApi) requestPasswordReset(c echo.Context) error {
return NewBadRequestError("An error occurred while validating the form.", err)
}

// run in background because we don't need to show
// the result to the user (prevents users enumeration)
routine.FireAndForget(func() {
if err := form.Submit(); err != nil && api.app.IsDebug() {
log.Println(err)
event := &core.RecordRequestPasswordResetEvent{
HttpContext: c,
}

submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
event.Record = record

return api.app.OnRecordBeforeRequestPasswordResetRequest().Trigger(event, func(e *core.RecordRequestPasswordResetEvent) error {
// run in background because we don't need to show the result to the client
routine.FireAndForget(func() {
if err := next(e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
})

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

return c.NoContent(http.StatusNoContent)
if submitErr == nil {
api.app.OnRecordAfterRequestPasswordResetRequest().Trigger(event)
} else if api.app.IsDebug() {
log.Println(submitErr)
}

// don't return the response error to prevent emails enumeration
if !c.Response().Committed {
c.NoContent(http.StatusNoContent)
}

return nil
}

func (api *recordAuthApi) confirmPasswordReset(c echo.Context) error {
Expand All @@ -305,12 +329,29 @@ func (api *recordAuthApi) confirmPasswordReset(c echo.Context) error {
return NewBadRequestError("An error occurred while loading the submitted data.", readErr)
}

_, submitErr := form.Submit()
if submitErr != nil {
return NewBadRequestError("Failed to set new password.", submitErr)
event := &core.RecordConfirmPasswordResetEvent{
HttpContext: c,
}

return c.NoContent(http.StatusNoContent)
_, submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
event.Record = record

return api.app.OnRecordBeforeConfirmPasswordResetRequest().Trigger(event, func(e *core.RecordConfirmPasswordResetEvent) error {
if err := next(e.Record); err != nil {
return NewBadRequestError("Failed to set new password.", err)
}

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

if submitErr == nil {
api.app.OnRecordAfterConfirmPasswordResetRequest().Trigger(event)
}

return submitErr
}

func (api *recordAuthApi) requestVerification(c echo.Context) error {
Expand All @@ -328,15 +369,39 @@ func (api *recordAuthApi) requestVerification(c echo.Context) error {
return NewBadRequestError("An error occurred while validating the form.", err)
}

// run in background because we don't need to show
// the result to the user (prevents users enumeration)
routine.FireAndForget(func() {
if err := form.Submit(); err != nil && api.app.IsDebug() {
log.Println(err)
event := &core.RecordRequestVerificationEvent{
HttpContext: c,
}

submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
event.Record = record

return api.app.OnRecordBeforeRequestVerificationRequest().Trigger(event, func(e *core.RecordRequestVerificationEvent) error {
// run in background because we don't need to show the result to the client
routine.FireAndForget(func() {
if err := next(e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
})

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

return c.NoContent(http.StatusNoContent)
if submitErr == nil {
api.app.OnRecordAfterRequestVerificationRequest().Trigger(event)
} else if api.app.IsDebug() {
log.Println(submitErr)
}

// don't return the response error to prevent emails enumeration
if !c.Response().Committed {
c.NoContent(http.StatusNoContent)
}

return nil
}

func (api *recordAuthApi) confirmVerification(c echo.Context) error {
Expand All @@ -350,12 +415,29 @@ func (api *recordAuthApi) confirmVerification(c echo.Context) error {
return NewBadRequestError("An error occurred while loading the submitted data.", readErr)
}

_, submitErr := form.Submit()
if submitErr != nil {
return NewBadRequestError("An error occurred while submitting the form.", submitErr)
event := &core.RecordConfirmVerificationEvent{
HttpContext: c,
}

return c.NoContent(http.StatusNoContent)
_, submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
event.Record = record

return api.app.OnRecordBeforeConfirmVerificationRequest().Trigger(event, func(e *core.RecordConfirmVerificationEvent) error {
if err := next(e.Record); err != nil {
return NewBadRequestError("An error occurred while submitting the form.", err)
}

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

if submitErr == nil {
api.app.OnRecordAfterConfirmVerificationRequest().Trigger(event)
}

return submitErr
}

func (api *recordAuthApi) requestEmailChange(c echo.Context) error {
Expand All @@ -369,11 +451,28 @@ func (api *recordAuthApi) requestEmailChange(c echo.Context) error {
return NewBadRequestError("An error occurred while loading the submitted data.", err)
}

if err := form.Submit(); err != nil {
return NewBadRequestError("Failed to request email change.", err)
event := &core.RecordRequestEmailChangeEvent{
HttpContext: c,
Record: record,
}

return c.NoContent(http.StatusNoContent)
submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
return api.app.OnRecordBeforeRequestEmailChangeRequest().Trigger(event, func(e *core.RecordRequestEmailChangeEvent) error {
if err := next(e.Record); err != nil {
return NewBadRequestError("Failed to request email change.", err)
}

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

if submitErr == nil {
api.app.OnRecordAfterRequestEmailChangeRequest().Trigger(event)
}

return submitErr
}

func (api *recordAuthApi) confirmEmailChange(c echo.Context) error {
Expand All @@ -387,12 +486,29 @@ func (api *recordAuthApi) confirmEmailChange(c echo.Context) error {
return NewBadRequestError("An error occurred while loading the submitted data.", readErr)
}

_, submitErr := form.Submit()
if submitErr != nil {
return NewBadRequestError("Failed to confirm email change.", submitErr)
event := &core.RecordConfirmEmailChangeEvent{
HttpContext: c,
}

_, submitErr := form.Submit(func(next forms.InterceptorWithRecordNextFunc) forms.InterceptorWithRecordNextFunc {
return func(record *models.Record) error {
event.Record = record

return api.app.OnRecordBeforeConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeEvent) error {
if err := next(e.Record); err != nil {
return NewBadRequestError("Failed to confirm email change.", err)
}

return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})

if submitErr == nil {
api.app.OnRecordAfterConfirmEmailChangeRequest().Trigger(event)
}

return c.NoContent(http.StatusNoContent)
return submitErr
}

func (api *recordAuthApi) listExternalAuths(c echo.Context) error {
Expand Down
63 changes: 44 additions & 19 deletions apis/record_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,12 @@ func TestRecordAuthRequestPasswordReset(t *testing.T) {
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnMailerBeforeRecordResetPasswordSend": 1,
"OnMailerAfterRecordResetPasswordSend": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnRecordBeforeRequestPasswordResetRequest": 1,
"OnRecordAfterRequestPasswordResetRequest": 1,
"OnMailerBeforeRecordResetPasswordSend": 1,
"OnMailerAfterRecordResetPasswordSend": 1,
},
},
{
Expand Down Expand Up @@ -466,8 +468,10 @@ func TestRecordAuthConfirmPasswordReset(t *testing.T) {
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmPasswordResetRequest": 1,
"OnRecordAfterConfirmPasswordResetRequest": 1,
},
},
}
Expand Down Expand Up @@ -518,6 +522,10 @@ func TestRecordAuthRequestVerification(t *testing.T) {
Body: strings.NewReader(`{"email":"[email protected]"}`),
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnRecordBeforeRequestVerificationRequest": 1,
"OnRecordAfterRequestVerificationRequest": 1,
},
},
{
Name: "existing auth record",
Expand All @@ -527,10 +535,12 @@ func TestRecordAuthRequestVerification(t *testing.T) {
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnMailerBeforeRecordVerificationSend": 1,
"OnMailerAfterRecordVerificationSend": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnRecordBeforeRequestVerificationRequest": 1,
"OnRecordAfterRequestVerificationRequest": 1,
"OnMailerBeforeRecordVerificationSend": 1,
"OnMailerAfterRecordVerificationSend": 1,
},
},
{
Expand All @@ -540,6 +550,10 @@ func TestRecordAuthRequestVerification(t *testing.T) {
Body: strings.NewReader(`{"email":"[email protected]"}`),
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
// "OnRecordBeforeRequestVerificationRequest": 1,
// "OnRecordAfterRequestVerificationRequest": 1,
},
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
// simulate recent verification sent
authRecord, err := app.Dao().FindFirstRecordByData("users", "email", "[email protected]")
Expand Down Expand Up @@ -627,8 +641,10 @@ func TestRecordAuthConfirmVerification(t *testing.T) {
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmVerificationRequest": 1,
"OnRecordAfterConfirmVerificationRequest": 1,
},
},
{
Expand All @@ -639,7 +655,10 @@ func TestRecordAuthConfirmVerification(t *testing.T) {
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJ0eXBlIjoiYXV0aFJlY29yZCIsImV4cCI6MjIwODk4NTI2MX0.PsOABmYUzGbd088g8iIBL4-pf7DUZm0W5Ju6lL5JVRg"
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{},
ExpectedEvents: map[string]int{
"OnRecordBeforeConfirmVerificationRequest": 1,
"OnRecordAfterConfirmVerificationRequest": 1,
},
},
{
Name: "valid verification token from a collection without allowed login",
Expand All @@ -651,8 +670,10 @@ func TestRecordAuthConfirmVerification(t *testing.T) {
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmVerificationRequest": 1,
"OnRecordAfterConfirmVerificationRequest": 1,
},
},
}
Expand Down Expand Up @@ -751,8 +772,10 @@ func TestRecordAuthRequestEmailChange(t *testing.T) {
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnMailerBeforeRecordChangeEmailSend": 1,
"OnMailerAfterRecordChangeEmailSend": 1,
"OnMailerBeforeRecordChangeEmailSend": 1,
"OnMailerAfterRecordChangeEmailSend": 1,
"OnRecordBeforeRequestEmailChangeRequest": 1,
"OnRecordAfterRequestEmailChangeRequest": 1,
},
},
}
Expand Down Expand Up @@ -833,8 +856,10 @@ func TestRecordAuthConfirmEmailChange(t *testing.T) {
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmEmailChangeRequest": 1,
"OnRecordAfterConfirmEmailChangeRequest": 1,
},
},
{
Expand Down
Loading

0 comments on commit 604009b

Please sign in to comment.