diff --git a/internal/api/anonymous.go b/internal/api/anonymous.go index 51ad187d8..a1b445791 100644 --- a/internal/api/anonymous.go +++ b/internal/api/anonymous.go @@ -53,6 +53,9 @@ func (a *API) SignupAnonymously(w http.ResponseWriter, r *http.Request) error { if err != nil { return apierrors.NewInternalServerError("Database error creating anonymous user").WithInternalError(err) } + if err := a.triggerAfterUserCreated(r, db, newUser); err != nil { + return err + } metering.RecordLogin(metering.LoginTypeAnonymous, newUser.ID, nil) return sendJSON(w, http.StatusOK, token) diff --git a/internal/api/apitask/apitask.go b/internal/api/apitask/apitask.go index 592948d21..b5e2b5731 100644 --- a/internal/api/apitask/apitask.go +++ b/internal/api/apitask/apitask.go @@ -39,6 +39,19 @@ type Task interface { Run(context.Context) error } +type taskFunc struct { + typ string + fn func(context.Context) error +} + +func (o *taskFunc) Type() string { return o.typ } + +func (o *taskFunc) Run(ctx context.Context) error { return o.fn(ctx) } + +func Func(typ string, fn func(context.Context) error) Task { + return &taskFunc{typ: typ, fn: fn} +} + // Run will run a request-scoped background task in a separate goroutine // immediately if the current context supports it. Otherwise it makes an // immediate blocking call to task.Run(ctx). diff --git a/internal/api/apitask/apitask_test.go b/internal/api/apitask/apitask_test.go index eca1cff39..e0ede661f 100644 --- a/internal/api/apitask/apitask_test.go +++ b/internal/api/apitask/apitask_test.go @@ -12,19 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -type taskFunc struct { - typ string - fn func(context.Context) error -} - -func (o *taskFunc) Type() string { return o.typ } - -func (o *taskFunc) Run(ctx context.Context) error { return o.fn(ctx) } - -func taskFn(typ string, fn func(context.Context) error) Task { - return &taskFunc{typ: typ, fn: fn} -} - func TestContext(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) defer cancel() @@ -59,7 +46,7 @@ func TestRun(t *testing.T) { expCalls := 0 for range 16 { expCalls++ - task := taskFn("test.run", func(ctx context.Context) error { + task := Func("test.run", func(ctx context.Context) error { calls.Add(1) return nil }) @@ -85,7 +72,7 @@ func TestRun(t *testing.T) { sentinel := errors.New("sentinel") for range 16 { expCalls++ - task := taskFn("test.run", func(ctx context.Context) error { + task := Func("test.run", func(ctx context.Context) error { calls.Add(1) return sentinel }) @@ -110,7 +97,7 @@ func TestRun(t *testing.T) { sentinel := errors.New("sentinel") for range 16 { expCalls++ - task := taskFn("test.run", func(ctx context.Context) error { + task := Func("test.run", func(ctx context.Context) error { calls.Add(1) return sentinel }) @@ -137,7 +124,7 @@ func TestMiddleware(t *testing.T) { hrFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for i := range 10 { typ := fmt.Sprintf("test-task-%v", i) - task := taskFn(typ, func(ctx context.Context) error { + task := Func(typ, func(ctx context.Context) error { return nil }) @@ -164,7 +151,7 @@ func TestMiddleware(t *testing.T) { hrFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for i := range 10 { typ := fmt.Sprintf("test-task-%v", i) - task := taskFn(typ, func(ctx context.Context) error { + task := Func(typ, func(ctx context.Context) error { return nil }) err := Run(r.Context(), task) diff --git a/internal/api/e2e_test.go b/internal/api/e2e_test.go index 207491fe2..2ac322111 100644 --- a/internal/api/e2e_test.go +++ b/internal/api/e2e_test.go @@ -87,6 +87,43 @@ func runVerifyBeforeUserCreatedHook( return latest } +func runVerifyAfterUserCreatedHook( + t *testing.T, + inst *e2ehooks.Instance, + expUser *models.User, +) *models.User { + var latest *models.User + t.Run("VerifyAfterUserCreatedHook", func(t *testing.T) { + defer inst.HookRecorder.AfterUserCreated.ClearCalls() + + calls := inst.HookRecorder.AfterUserCreated.GetCalls() + require.Equal(t, 1, len(calls)) + call := calls[0] + + hookReq := &v0hooks.AfterUserCreatedInput{} + err := call.Unmarshal(hookReq) + require.NoError(t, err) + require.Equal(t, v0hooks.AfterUserCreated, hookReq.Metadata.Name) + + u := hookReq.User + require.Equal(t, expUser.ID, u.ID) + require.Equal(t, expUser.Aud, u.Aud) + require.Equal(t, expUser.Email, u.Email) + require.Equal(t, expUser.AppMetaData, u.AppMetaData) + + require.False(t, u.CreatedAt.IsZero()) + require.False(t, u.UpdatedAt.IsZero()) + + err = expUser.Confirm(inst.Conn) + require.NoError(t, err) + + latest, err = models.FindUserByID(inst.Conn, expUser.ID) + require.NoError(t, err) + require.NotNil(t, latest) + }) + return latest +} + func getAccessToken( ctx context.Context, t *testing.T, @@ -208,6 +245,7 @@ func TestE2EHooks(t *testing.T) { require.Equal(t, email, res.Email.String()) runVerifyBeforeUserCreatedHook(t, inst, res) + runVerifyAfterUserCreatedHook(t, inst, res) }) t.Run("SignupPhone", func(t *testing.T) { @@ -224,6 +262,8 @@ func TestE2EHooks(t *testing.T) { require.Equal(t, phone, res.Phone.String()) runVerifyBeforeUserCreatedHook(t, inst, res) + runVerifyAfterUserCreatedHook(t, inst, res) + }) t.Run("SignupAnonymously", func(t *testing.T) { @@ -235,6 +275,8 @@ func TestE2EHooks(t *testing.T) { require.NoError(t, err) runVerifyBeforeUserCreatedHook(t, inst, res.User) + runVerifyAfterUserCreatedHook(t, inst, res.User) + }) t.Run("ExternalCallback", func(t *testing.T) { @@ -246,6 +288,8 @@ func TestE2EHooks(t *testing.T) { require.NoError(t, err) runVerifyBeforeUserCreatedHook(t, inst, res.User) + runVerifyAfterUserCreatedHook(t, inst, res.User) + }) t.Run("AdminEndpoints", func(t *testing.T) { @@ -273,6 +317,8 @@ func TestE2EHooks(t *testing.T) { require.NoError(t, err) runVerifyBeforeUserCreatedHook(t, inst, res) + runVerifyAfterUserCreatedHook(t, inst, res) + }) t.Run("AdminGenerateLink", func(t *testing.T) { @@ -304,6 +350,7 @@ func TestE2EHooks(t *testing.T) { require.NoError(t, err) runVerifyBeforeUserCreatedHook(t, inst, &res.User) + runVerifyAfterUserCreatedHook(t, inst, &res.User) }) t.Run("InviteVerification", func(t *testing.T) { @@ -332,6 +379,7 @@ func TestE2EHooks(t *testing.T) { require.NoError(t, err) runVerifyBeforeUserCreatedHook(t, inst, &res.User) + runVerifyAfterUserCreatedHook(t, inst, &res.User) }) }) }) @@ -372,6 +420,7 @@ func TestE2EHooks(t *testing.T) { require.Equal(t, email, mfaUser.Email.String()) mfaUser = runVerifyBeforeUserCreatedHook(t, inst, mfaUser) + runVerifyAfterUserCreatedHook(t, inst, mfaUser) require.NotNil(t, mfaUser) mfaUserAccessToken = getAccessToken( ctx, t, inst, string(mfaUser.Email), defaultPassword) @@ -562,6 +611,7 @@ func TestE2EHooks(t *testing.T) { require.Equal(t, email, res.Email.String()) currentUser = runVerifyBeforeUserCreatedHook(t, inst, res) + runVerifyAfterUserCreatedHook(t, inst, res) require.NotNil(t, currentUser) inst.HookRecorder.CustomizeAccessToken.ClearCalls() } diff --git a/internal/api/external.go b/internal/api/external.go index ae61bc86b..9611c24c2 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -218,6 +218,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } } + var createdUser bool var user *models.User var token *AccessTokenResponse err = db.Transaction(func(tx *storage.Connection) error { @@ -231,7 +232,8 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re return terr } } else { - if user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil { + createdUser = true + if _, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil { return terr } } @@ -253,10 +255,14 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } return nil }) - if err != nil { return err } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } // Record login for analytics - only when token is issued (not during pkce authorize) if token != nil { @@ -290,7 +296,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re return nil } -func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.Request, userData *provider.UserProvidedData, providerType string, emailOptional bool) (*models.User, error) { +func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.Request, userData *provider.UserProvidedData, providerType string, emailOptional bool) (models.AccountLinkingDecision, *models.User, error) { ctx := r.Context() aud := a.requestAud(ctx, r) config := a.config @@ -304,7 +310,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. decision, terr := models.DetermineAccountLinking(tx, config, userData.Emails, aud, providerType, userData.Metadata.Subject) if terr != nil { - return nil, terr + return 0, nil, terr } switch decision.Decision { @@ -312,20 +318,20 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. user = decision.User if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil { - return nil, terr + return 0, nil, terr } if terr = user.UpdateUserMetaData(tx, identityData); terr != nil { - return nil, terr + return 0, nil, terr } if terr = user.UpdateAppMetaDataProviders(tx); terr != nil { - return nil, terr + return 0, nil, terr } case models.CreateAccount: if config.DisableSignup { - return nil, apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeSignupDisabled, "Signups not allowed for this instance") + return 0, nil, apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeSignupDisabled, "Signups not allowed for this instance") } params := &SignupParams{ @@ -352,15 +358,15 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. // transaction user, terr = params.ToUserModel(isSSOUser) if terr != nil { - return nil, terr + return 0, nil, terr } if user, terr = a.signupNewUser(tx, user); terr != nil { - return nil, terr + return 0, nil, terr } if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil { - return nil, terr + return 0, nil, terr } user.Identities = append(user.Identities, *identity) @@ -370,24 +376,24 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. identity.IdentityData = identityData if terr = tx.UpdateOnly(identity, "identity_data", "last_sign_in_at"); terr != nil { - return nil, terr + return 0, nil, terr } if terr = user.UpdateUserMetaData(tx, identityData); terr != nil { - return nil, terr + return 0, nil, terr } if terr = user.UpdateAppMetaDataProviders(tx); terr != nil { - return nil, terr + return 0, nil, terr } case models.MultipleAccounts: - return nil, apierrors.NewInternalServerError("Multiple accounts with the same email address in the same linking domain detected: %v", decision.LinkingDomain) + return 0, nil, apierrors.NewInternalServerError("Multiple accounts with the same email address in the same linking domain detected: %v", decision.LinkingDomain) default: - return nil, apierrors.NewInternalServerError("Unknown automatic linking decision: %v", decision.Decision) + return 0, nil, apierrors.NewInternalServerError("Unknown automatic linking decision: %v", decision.Decision) } if user.IsBanned() { - return nil, apierrors.NewForbiddenError(apierrors.ErrorCodeUserBanned, "User is banned") + return 0, nil, apierrors.NewForbiddenError(apierrors.ErrorCodeUserBanned, "User is banned") } hasEmails := providerType != "web3" && !(emailOptional && decision.CandidateEmail.Email == "") @@ -398,44 +404,44 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. // need to be removed when a new oauth identity is being added // to prevent pre-account takeover attacks from happening. if terr = user.RemoveUnconfirmedIdentities(tx, identity); terr != nil { - return nil, apierrors.NewInternalServerError("Error updating user").WithInternalError(terr) + return 0, nil, apierrors.NewInternalServerError("Error updating user").WithInternalError(terr) } if decision.CandidateEmail.Verified || config.Mailer.Autoconfirm { if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": providerType, }); terr != nil { - return nil, terr + return 0, nil, terr } // fall through to auto-confirm and issue token if terr = user.Confirm(tx); terr != nil { - return nil, apierrors.NewInternalServerError("Error updating user").WithInternalError(terr) + return 0, nil, apierrors.NewInternalServerError("Error updating user").WithInternalError(terr) } } else { emailConfirmationSent := false if decision.CandidateEmail.Email != "" { if terr = a.sendConfirmation(r, tx, user, models.ImplicitFlow); terr != nil { - return nil, terr + return 0, nil, terr } emailConfirmationSent = true } if !config.Mailer.AllowUnverifiedEmailSignIns { if emailConfirmationSent { - return nil, storage.NewCommitWithError(apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. A confirmation email has been sent to your %v email", providerType, providerType))) + return 0, nil, storage.NewCommitWithError(apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. A confirmation email has been sent to your %v email", providerType, providerType))) } - return nil, storage.NewCommitWithError(apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. Verify the email with %v in order to sign in", providerType, providerType))) + return 0, nil, storage.NewCommitWithError(apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. Verify the email with %v in order to sign in", providerType, providerType))) } } } else { if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.LoginAction, "", map[string]interface{}{ "provider": providerType, }); terr != nil { - return nil, terr + return 0, nil, terr } } - return user, nil + return decision.Decision, user, nil } func (a *API) processInvite(r *http.Request, tx *storage.Connection, userData *provider.UserProvidedData, inviteToken, providerType string) (*models.User, error) { diff --git a/internal/api/hooks.go b/internal/api/hooks.go index 95c2367c8..7b02f4889 100644 --- a/internal/api/hooks.go +++ b/internal/api/hooks.go @@ -12,6 +12,26 @@ import ( "github.com/supabase/auth/internal/storage" ) +func (a *API) triggerAfterUserCreated( + r *http.Request, + conn *storage.Connection, + user *models.User, +) error { + if !a.hooksMgr.Enabled(v0hooks.AfterUserCreated) { + return nil + } + + // We still check tx because we want to make sure we aren't calling this + // trigger in code paths that haven't actually created the user yet. + if err := checkTX(conn); err != nil { + return err + } + + req := v0hooks.NewAfterUserCreatedInput(r, user) + res := new(v0hooks.AfterUserCreatedOutput) + return a.hooksMgr.InvokeHook(conn, r, req, res) +} + func (a *API) triggerBeforeUserCreated( r *http.Request, db *storage.Connection, diff --git a/internal/api/invite.go b/internal/api/invite.go index 03ad2d7ba..fd2629770 100644 --- a/internal/api/invite.go +++ b/internal/api/invite.go @@ -99,5 +99,8 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { return err } + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } return sendJSON(w, http.StatusOK, user) } diff --git a/internal/api/mail.go b/internal/api/mail.go index 189c12a6e..70856bce4 100644 --- a/internal/api/mail.go +++ b/internal/api/mail.go @@ -94,8 +94,9 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { hashedToken := crypto.GenerateTokenHash(params.Email, otp) var ( - signupUser *models.User - inviteUser *models.User + createdUser bool + signupUser *models.User + inviteUser *models.User ) switch { case params.Type == mail.SignupVerification && user == nil: @@ -162,6 +163,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailExists, DuplicateEmailMsg) } } else { + createdUser = true user, terr = a.signupNewUser(tx, inviteUser) if terr != nil { return terr @@ -207,6 +209,7 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { // password here to generate a new user, use // signupUser which is a model generated from // SignupParams above + createdUser = true user, terr = a.signupNewUser(tx, signupUser) if terr != nil { return terr @@ -288,11 +291,16 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { } return nil }) - if err != nil { return err } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } + resp := GenerateLinkResponse{ User: *user, ActionLink: url, @@ -301,7 +309,6 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error { VerificationType: params.Type, RedirectTo: referrer, } - return sendJSON(w, http.StatusOK, resp) } diff --git a/internal/api/samlacs.go b/internal/api/samlacs.go index e15c79674..263c3895f 100644 --- a/internal/api/samlacs.go +++ b/internal/api/samlacs.go @@ -300,14 +300,18 @@ func (a *API) handleSamlAcs(w http.ResponseWriter, r *http.Request) error { return err } + var createdUser bool + var user *models.User if err := db.Transaction(func(tx *storage.Connection) error { var terr error - var user *models.User // accounts potentially created via SAML can contain non-unique email addresses in the auth.users table - if user, terr = a.createAccountFromExternalIdentity(tx, r, &userProvidedData, providerType, false); terr != nil { + var decision models.AccountLinkingDecision + if decision, user, terr = a.createAccountFromExternalIdentity(tx, r, &userProvidedData, providerType, false); terr != nil { return terr } + createdUser = decision == models.CreateAccount + if flowState != nil { // This means that the callback is using PKCE flowState.UserID = &(user.ID) @@ -326,6 +330,11 @@ func (a *API) handleSamlAcs(w http.ResponseWriter, r *http.Request) error { }); err != nil { return err } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } if !utilities.IsRedirectURLValid(config, redirectTo) { redirectTo = config.SiteURL diff --git a/internal/api/signup.go b/internal/api/signup.go index aa5d427fa..89c79f889 100644 --- a/internal/api/signup.go +++ b/internal/api/signup.go @@ -336,6 +336,11 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { user.UserMetaData = map[string]interface{}{} user.Identities = []models.Identity{} } + + // Trigger the after user created hook. + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return apierrors.NewInternalServerError("Error invoking hook").WithInternalError(err) + } return sendJSON(w, http.StatusOK, user) } @@ -395,6 +400,5 @@ func (a *API) signupNewUser(conn *storage.Connection, user *models.User) (*model if err := conn.Reload(user); err != nil { return nil, apierrors.NewInternalServerError("Database error loading user after sign-up").WithInternalError(err) } - return user, nil } diff --git a/internal/api/token_oidc.go b/internal/api/token_oidc.go index ee1e529d8..e62940abc 100644 --- a/internal/api/token_oidc.go +++ b/internal/api/token_oidc.go @@ -266,6 +266,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } } + var createdUser bool var token *AccessTokenResponse var grantParams models.GrantParams @@ -277,15 +278,17 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } } + var user *models.User if err := db.Transaction(func(tx *storage.Connection) error { - var user *models.User var terr error + var decision models.AccountLinkingDecision if params.LinkIdentity { user, terr = a.linkIdentityToUser(r, ctx, tx, userData, providerType) } else { - user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional) + decision, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional) } + createdUser = decision == models.CreateAccount if terr != nil { return terr } @@ -306,6 +309,11 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return apierrors.NewOAuthError("server_error", "Internal Server Error").WithInternalError(err) } } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } metering.RecordLogin(metering.LoginTypeOIDC, token.User.ID, &metering.LoginData{ Provider: providerType, diff --git a/internal/api/web3.go b/internal/api/web3.go index 8b1d1881b..3928b1a25 100644 --- a/internal/api/web3.go +++ b/internal/api/web3.go @@ -135,6 +135,7 @@ func (a *API) web3GrantSolana(ctx context.Context, w http.ResponseWriter, r *htt Emails: []provider.Email{}, } + var createdUser bool var token *AccessTokenResponse var grantParams models.GrantParams grantParams.FillGrantParams(r) @@ -143,11 +144,15 @@ func (a *API) web3GrantSolana(ctx context.Context, w http.ResponseWriter, r *htt return err } + var user *models.User err = db.Transaction(func(tx *storage.Connection) error { - user, terr := a.createAccountFromExternalIdentity(tx, r, &userData, providerType, true) + var terr error + var decision models.AccountLinkingDecision + decision, user, terr = a.createAccountFromExternalIdentity(tx, r, &userData, providerType, true) if terr != nil { return terr } + createdUser = decision == models.CreateAccount if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.LoginAction, "", map[string]interface{}{ "provider": providerType, @@ -178,6 +183,11 @@ func (a *API) web3GrantSolana(ctx context.Context, w http.ResponseWriter, r *htt return apierrors.NewOAuthError("server_error", "Internal Server Error").WithInternalError(err) } } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } // Record login for analytics with Web3 context metering.RecordLogin(metering.LoginTypeWeb3, token.User.ID, &metering.LoginData{ @@ -271,6 +281,7 @@ func (a *API) web3GrantEthereum(ctx context.Context, w http.ResponseWriter, r *h Emails: []provider.Email{}, } + var createdUser bool var token *AccessTokenResponse var grantParams models.GrantParams grantParams.FillGrantParams(r) @@ -279,11 +290,15 @@ func (a *API) web3GrantEthereum(ctx context.Context, w http.ResponseWriter, r *h return err } + var user *models.User err = db.Transaction(func(tx *storage.Connection) error { - user, terr := a.createAccountFromExternalIdentity(tx, r, &userData, providerType, true) + var terr error + var decision models.AccountLinkingDecision + decision, user, terr = a.createAccountFromExternalIdentity(tx, r, &userData, providerType, true) if terr != nil { return terr } + createdUser = decision == models.CreateAccount if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.LoginAction, "", map[string]interface{}{ "provider": providerType, @@ -315,5 +330,10 @@ func (a *API) web3GrantEthereum(ctx context.Context, w http.ResponseWriter, r *h } } + if createdUser { + if err := a.triggerAfterUserCreated(r, db, user); err != nil { + return err + } + } return sendJSON(w, http.StatusOK, token) }