diff --git a/README.md b/README.md index 32e9922d7..2ec67cbde 100644 --- a/README.md +++ b/README.md @@ -740,6 +740,47 @@ Default Content (if template is unavailable): Whether to send a notification email when a user's phone number is changed. Defaults to `false`. +`GOTRUE_MAILER_TEMPLATES_IDENTITY_LINKED_NOTIFICATION` - `string` + +URL path to an email template to use when notifying a user that a new identity has been linked to their account. (e.g. `https://www.example.com/path-to-email-template.html`) +`Email` and `Provider` variables are available. + +Default Content (if template is unavailable): + +```html +

A new identity has been linked

+ +

+ A new identity ({{ .Provider }}) has been linked to your account {{ .Email }}. +

+

If you did not make this change, please contact support immediately.

+``` + +`GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_LINKED_ENABLED` - `bool` + +Whether to send a notification email when a new identity is linked to a user's account. Defaults to `false`. + +`GOTRUE_MAILER_TEMPLATES_IDENTITY_UNLINKED_NOTIFICATION` - `string` + +URL path to an email template to use when notifying a user that an identity has been unlinked from their account. (e.g. `https://www.example.com/path-to-email-template.html`) +`Email` and `Provider` variables are available. + +Default Content (if template is unavailable): + +```html +

An identity has been unlinked

+ +

+ An identity ({{ .Provider }}) has been unlinked from your account {{ .Email + }}. +

+

If you did not make this change, please contact support immediately.

+``` + +`GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_UNLINKED_ENABLED` - `bool` + +Whether to send a notification email when an identity is unlinked from a user's account. Defaults to `false`. + `GOTRUE_MAILER_TEMPLATES_MFA_FACTOR_ENROLLED_NOTIFICATION` - `string` URL path to an email template to use when notifying a user that they have enrolled in a new MFA factor. (e.g. `https://www.example.com/path-to-email-template.html`) diff --git a/example.env b/example.env index 190d45b1b..5f658fcb7 100644 --- a/example.env +++ b/example.env @@ -38,6 +38,8 @@ GOTRUE_MAILER_SUBJECTS_INVITE="You have been invited" GOTRUE_MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION="Your password has been changed" GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION="Your email address has been changed" GOTRUE_MAILER_SUBJECTS_PHONE_CHANGED_NOTIFICATION="Your phone number has been changed" +GOTRUE_MAILER_SUBJECTS_IDENTITY_LINKED_NOTIFICATION="A new identity has been linked" +GOTRUE_MAILER_SUBJECTS_IDENTITY_UNLINKED_NOTIFICATION="An identity has been unlinked" GOTRUE_MAILER_SUBJECTS_MFA_FACTOR_ENROLLED_NOTIFICATION="MFA factor enrolled" GOTRUE_MAILER_SUBJECTS_MFA_FACTOR_UNENROLLED_NOTIFICATION="MFA factor unenrolled" GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED="true" @@ -51,6 +53,8 @@ GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE="" GOTRUE_MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION="" GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGED_NOTIFICATION="" GOTRUE_MAILER_TEMPLATES_PHONE_CHANGED_NOTIFICATION="" +GOTRUE_MAILER_TEMPLATES_IDENTITY_LINKED_NOTIFICATION="" +GOTRUE_MAILER_TEMPLATES_IDENTITY_UNLINKED_NOTIFICATION="" GOTRUE_MAILER_TEMPLATES_MFA_FACTOR_ENROLLED_NOTIFICATION="" GOTRUE_MAILER_TEMPLATES_MFA_FACTOR_UNENROLLED_NOTIFICATION="" @@ -58,6 +62,8 @@ GOTRUE_MAILER_TEMPLATES_MFA_FACTOR_UNENROLLED_NOTIFICATION="" GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED="false" GOTRUE_MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED="false" GOTRUE_MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED="false" +GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_LINKED_ENABLED="false" +GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_UNLINKED_ENABLED="false" GOTRUE_MAILER_NOTIFICATIONS_MFA_FACTOR_ENROLLED_ENABLED="false" GOTRUE_MAILER_NOTIFICATIONS_MFA_FACTOR_UNENROLLED_ENABLED="false" diff --git a/internal/api/identity.go b/internal/api/identity.go index 5bfa2a767..c125aff1b 100644 --- a/internal/api/identity.go +++ b/internal/api/identity.go @@ -7,6 +7,7 @@ import ( "github.com/fatih/structs" "github.com/go-chi/chi/v5" "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" "github.com/supabase/auth/internal/api/apierrors" "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/models" @@ -50,6 +51,7 @@ func (a *API) DeleteIdentity(w http.ResponseWriter, r *http.Request) error { return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeIdentityNotFound, "Identity doesn't exist") } + provider := identityToBeDeleted.Provider err = db.Transaction(func(tx *storage.Connection) error { if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.IdentityUnlinkAction, "", map[string]interface{}{ "identity_id": identityToBeDeleted.ID, @@ -88,6 +90,14 @@ func (a *API) DeleteIdentity(w http.ResponseWriter, r *http.Request) error { return err } + // Send identity unlinked notification email if enabled and user has an email + if config.Mailer.Notifications.IdentityUnlinkedEnabled && user.GetEmail() != "" { + if err := a.sendIdentityUnlinkedNotification(r, db, user, provider); err != nil { + // Log the error but don't fail the unlinking + logrus.WithError(err).Warn("Unable to send identity unlinked notification email") + } + } + return sendJSON(w, http.StatusOK, map[string]interface{}{}) } @@ -154,5 +164,14 @@ func (a *API) linkIdentityToUser(r *http.Request, ctx context.Context, tx *stora if terr := targetUser.UpdateAppMetaDataProviders(tx); terr != nil { return nil, terr } + + // Send identity linked notification email if enabled and user has an email + if a.config.Mailer.Notifications.IdentityLinkedEnabled && targetUser.GetEmail() != "" { + if terr := a.sendIdentityLinkedNotification(r, tx, targetUser, providerType); terr != nil { + // Log the error but don't fail the linking + logrus.WithError(terr).Warn("Unable to send identity linked notification email") + } + } + return targetUser, nil } diff --git a/internal/api/identity_test.go b/internal/api/identity_test.go index 6258860cc..f0dd45680 100644 --- a/internal/api/identity_test.go +++ b/internal/api/identity_test.go @@ -14,6 +14,8 @@ import ( "github.com/supabase/auth/internal/api/apierrors" "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/conf" + mail "github.com/supabase/auth/internal/mailer" + "github.com/supabase/auth/internal/mailer/mockclient" "github.com/supabase/auth/internal/models" ) @@ -21,14 +23,17 @@ type IdentityTestSuite struct { suite.Suite API *API Config *conf.GlobalConfiguration + Mailer mail.Mailer } func TestIdentity(t *testing.T) { - api, config, err := setupAPIForTest() + mockMailer := &mockclient.MockMailer{} + api, config, err := setupAPIForTest(WithMailer(mockMailer)) require.NoError(t, err) ts := &IdentityTestSuite{ API: api, Config: config, + Mailer: mockMailer, } defer api.db.Close() suite.Run(t, ts) @@ -226,3 +231,115 @@ func (ts *IdentityTestSuite) generateAccessTokenAndSession(u *models.User) strin return token } + +func (ts *IdentityTestSuite) TestLinkIdentitySendsNotificationEmailEnabled() { + ts.Config.Mailer.Notifications.IdentityLinkedEnabled = true + + u, err := models.FindUserByEmailAndAudience(ts.API.db, "one@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + ctx := withTargetUser(context.Background(), u) + + // Get the mock mailer and reset it + mockMailer, ok := ts.Mailer.(*mockclient.MockMailer) + require.True(ts.T(), ok, "Mailer is not of type *MockMailer") + mockMailer.Reset() + + // Link a new identity + testValidUserData := &provider.UserProvidedData{ + Metadata: &provider.Claims{ + Subject: "test_subject", + }, + } + r := httptest.NewRequest(http.MethodGet, "/identities", nil) + u, err = ts.API.linkIdentityToUser(r, ctx, ts.API.db, testValidUserData, "google") + require.NoError(ts.T(), err) + + // Assert that identity linked notification email was sent + require.Len(ts.T(), mockMailer.IdentityLinkedMailCalls, 1, "Expected 1 identity linked notification email(s) to be sent") + require.Equal(ts.T(), u.ID, mockMailer.IdentityLinkedMailCalls[0].User.ID, "Email should be sent to the correct user") + require.Equal(ts.T(), "google", mockMailer.IdentityLinkedMailCalls[0].Provider, "Provider should match") + require.Equal(ts.T(), "one@example.com", mockMailer.IdentityLinkedMailCalls[0].User.GetEmail(), "Email should be sent to the correct email address") +} + +func (ts *IdentityTestSuite) TestLinkIdentitySendsNotificationEmailDisabled() { + ts.Config.Mailer.Notifications.IdentityLinkedEnabled = false + + u, err := models.FindUserByEmailAndAudience(ts.API.db, "one@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + ctx := withTargetUser(context.Background(), u) + + // Get the mock mailer and reset it + mockMailer, ok := ts.Mailer.(*mockclient.MockMailer) + require.True(ts.T(), ok, "Mailer is not of type *MockMailer") + mockMailer.Reset() + + // Link a new identity + testValidUserData := &provider.UserProvidedData{ + Metadata: &provider.Claims{ + Subject: "test_subject_disabled", + }, + } + r := httptest.NewRequest(http.MethodGet, "/identities", nil) + _, err = ts.API.linkIdentityToUser(r, ctx, ts.API.db, testValidUserData, "facebook") + require.NoError(ts.T(), err) + + // Assert that identity linked notification email was not sent + require.Len(ts.T(), mockMailer.IdentityLinkedMailCalls, 0, "Expected 0 identity linked notification email(s) to be sent") +} + +func (ts *IdentityTestSuite) TestUnlinkIdentitySendsNotificationEmailEnabled() { + ts.Config.Mailer.Notifications.IdentityUnlinkedEnabled = true + ts.Config.Security.ManualLinkingEnabled = true + + u, err := models.FindUserByEmailAndAudience(ts.API.db, "two@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + identity, err := models.FindIdentityByIdAndProvider(ts.API.db, u.ID.String(), "phone") + require.NoError(ts.T(), err) + + // Get the mock mailer and reset it + mockMailer, ok := ts.Mailer.(*mockclient.MockMailer) + require.True(ts.T(), ok, "Mailer is not of type *MockMailer") + mockMailer.Reset() + + token := ts.generateAccessTokenAndSession(u) + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/user/identities/%s", identity.ID), nil) + require.NoError(ts.T(), err) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + // Assert that identity unlinked notification email was sent + require.Len(ts.T(), mockMailer.IdentityUnlinkedMailCalls, 1, "Expected 1 identity unlinked notification email(s) to be sent") + require.Equal(ts.T(), u.ID, mockMailer.IdentityUnlinkedMailCalls[0].User.ID, "Email should be sent to the correct user") + require.Equal(ts.T(), "phone", mockMailer.IdentityUnlinkedMailCalls[0].Provider, "Provider should match") + require.Equal(ts.T(), "two@example.com", mockMailer.IdentityUnlinkedMailCalls[0].User.GetEmail(), "Email should be sent to the correct email address") +} + +func (ts *IdentityTestSuite) TestUnlinkIdentitySendsNotificationEmailDisabled() { + ts.Config.Mailer.Notifications.IdentityUnlinkedEnabled = false + ts.Config.Security.ManualLinkingEnabled = true + + u, err := models.FindUserByEmailAndAudience(ts.API.db, "two@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + identity, err := models.FindIdentityByIdAndProvider(ts.API.db, u.ID.String(), "phone") + require.NoError(ts.T(), err) + + // Get the mock mailer and reset it + mockMailer, ok := ts.Mailer.(*mockclient.MockMailer) + require.True(ts.T(), ok, "Mailer is not of type *MockMailer") + mockMailer.Reset() + + token := ts.generateAccessTokenAndSession(u) + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/user/identities/%s", identity.ID), nil) + require.NoError(ts.T(), err) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + // Assert that identity unlinked notification email was not sent + require.Len(ts.T(), mockMailer.IdentityUnlinkedMailCalls, 0, "Expected 0 identity unlinked notification email(s) to be sent") +} diff --git a/internal/api/mail.go b/internal/api/mail.go index dbc3d6d22..c32e3e597 100644 --- a/internal/api/mail.go +++ b/internal/api/mail.go @@ -634,6 +634,40 @@ func (a *API) sendPhoneChangedNotification(r *http.Request, tx *storage.Connecti return nil } +func (a *API) sendIdentityLinkedNotification(r *http.Request, tx *storage.Connection, u *models.User, provider string) error { + err := a.sendEmail(r, tx, u, sendEmailParams{ + emailActionType: mail.IdentityLinkedNotification, + provider: provider, + }) + if err != nil { + if errors.Is(err, EmailRateLimitExceeded) { + return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error()) + } else if herr, ok := err.(*HTTPError); ok { + return herr + } + return apierrors.NewInternalServerError("Error sending identity linked notification email").WithInternalError(err) + } + + return nil +} + +func (a *API) sendIdentityUnlinkedNotification(r *http.Request, tx *storage.Connection, u *models.User, provider string) error { + err := a.sendEmail(r, tx, u, sendEmailParams{ + emailActionType: mail.IdentityUnlinkedNotification, + provider: provider, + }) + if err != nil { + if errors.Is(err, EmailRateLimitExceeded) { + return apierrors.NewTooManyRequestsError(apierrors.ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error()) + } else if herr, ok := err.(*HTTPError); ok { + return herr + } + return apierrors.NewInternalServerError("Error sending identity unlinked notification email").WithInternalError(err) + } + + return nil +} + func (a *API) sendMFAFactorEnrolledNotification(r *http.Request, tx *storage.Connection, u *models.User, factorType string) error { err := a.sendEmail(r, tx, u, sendEmailParams{ emailActionType: mail.MFAFactorEnrolledNotification, @@ -715,6 +749,7 @@ type sendEmailParams struct { tokenHashWithPrefix string oldEmail string oldPhone string + provider string factorType string } @@ -838,6 +873,10 @@ func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User, err = mr.EmailChangedNotificationMail(r, u, params.oldEmail) case mail.PhoneChangedNotification: err = mr.PhoneChangedNotificationMail(r, u, params.oldPhone) + case mail.IdentityLinkedNotification: + err = mr.IdentityLinkedNotificationMail(r, u, params.provider) + case mail.IdentityUnlinkedNotification: + err = mr.IdentityUnlinkedNotificationMail(r, u, params.provider) case mail.MFAFactorEnrolledNotification: err = mr.MFAFactorEnrolledNotificationMail(r, u, params.factorType) case mail.MFAFactorUnenrolledNotification: diff --git a/internal/api/verify.go b/internal/api/verify.go index f9eb98687..6209774bb 100644 --- a/internal/api/verify.go +++ b/internal/api/verify.go @@ -403,6 +403,7 @@ func (a *API) smsVerify(r *http.Request, conn *storage.Connection, user *models. config := a.config oldPhone := user.GetPhone() + phoneIdentityWasCreated := false err := conn.Transaction(func(tx *storage.Connection) error { if params.Type == smsVerification { @@ -430,6 +431,7 @@ func (a *API) smsVerify(r *http.Request, conn *storage.Connection, user *models. })); terr != nil { return terr } + phoneIdentityWasCreated = true } else { if terr := identity.UpdateIdentityData(tx, map[string]interface{}{ "phone": params.Phone, @@ -467,6 +469,14 @@ func (a *API) smsVerify(r *http.Request, conn *storage.Connection, user *models. } } + // Send identity linked notification email if a new phone identity was created + if phoneIdentityWasCreated && config.Mailer.Notifications.IdentityLinkedEnabled && user.GetEmail() != "" { + if err := a.sendIdentityLinkedNotification(r, conn, user, "phone"); err != nil { + // Log the error but don't fail the verification + logrus.WithError(err).Warn("Unable to send identity linked notification email") + } + } + return user, nil } diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index b7024e7c5..84ec2ee16 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -394,6 +394,8 @@ type EmailContentConfiguration struct { PasswordChangedNotification string `json:"password_changed_notification" split_words:"true"` EmailChangedNotification string `json:"email_changed_notification" split_words:"true"` PhoneChangedNotification string `json:"phone_changed_notification" split_words:"true"` + IdentityLinkedNotification string `json:"identity_linked_notification" split_words:"true"` + IdentityUnlinkedNotification string `json:"identity_unlinked_notification" split_words:"true"` MFAFactorEnrolledNotification string `json:"mfa_factor_enrolled_notification" split_words:"true"` MFAFactorUnenrolledNotification string `json:"mfa_factor_unenrolled_notification" split_words:"true"` } @@ -403,6 +405,8 @@ type NotificationsConfiguration struct { PasswordChangedEnabled bool `json:"password_changed_enabled" split_words:"true" default:"false"` EmailChangedEnabled bool `json:"email_changed_enabled" split_words:"true" default:"false"` PhoneChangedEnabled bool `json:"phone_changed_enabled" split_words:"true" default:"false"` + IdentityLinkedEnabled bool `json:"identity_linked_enabled" split_words:"true" default:"false"` + IdentityUnlinkedEnabled bool `json:"identity_unlinked_enabled" split_words:"true" default:"false"` MFAFactorEnrolledEnabled bool `json:"mfa_factor_enrolled_enabled" split_words:"true" default:"false"` MFAFactorUnenrolledEnabled bool `json:"mfa_factor_unenrolled_enabled" split_words:"true" default:"false"` } diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index cb889a2c4..ccbaff82c 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -23,6 +23,8 @@ const ( PasswordChangedNotification = "password_changed_notification" EmailChangedNotification = "email_changed_notification" PhoneChangedNotification = "phone_changed_notification" + IdentityLinkedNotification = "identity_linked_notification" + IdentityUnlinkedNotification = "identity_unlinked_notification" MFAFactorEnrolledNotification = "mfa_factor_enrolled_notification" MFAFactorUnenrolledNotification = "mfa_factor_unenrolled_notification" ) @@ -41,6 +43,8 @@ type Mailer interface { PasswordChangedNotificationMail(r *http.Request, user *models.User) error EmailChangedNotificationMail(r *http.Request, user *models.User, oldEmail string) error PhoneChangedNotificationMail(r *http.Request, user *models.User, oldPhone string) error + IdentityLinkedNotificationMail(r *http.Request, user *models.User, provider string) error + IdentityUnlinkedNotificationMail(r *http.Request, user *models.User, provider string) error MFAFactorEnrolledNotificationMail(r *http.Request, user *models.User, factorType string) error MFAFactorUnenrolledNotificationMail(r *http.Request, user *models.User, factorType string) error } diff --git a/internal/mailer/mockclient/mockclient.go b/internal/mailer/mockclient/mockclient.go index f1d09996f..18feab826 100644 --- a/internal/mailer/mockclient/mockclient.go +++ b/internal/mailer/mockclient/mockclient.go @@ -20,6 +20,8 @@ type MockMailer struct { PasswordChangedMailCalls []PasswordChangedMailCall EmailChangedMailCalls []EmailChangedMailCall PhoneChangedMailCalls []PhoneChangedMailCall + IdentityLinkedMailCalls []IdentityLinkedMailCall + IdentityUnlinkedMailCalls []IdentityUnlinkedMailCall MFAFactorEnrolledMailCalls []MFAFactorEnrolledMailCall MFAFactorUnenrolledMailCalls []MFAFactorUnenrolledMailCall } @@ -88,6 +90,16 @@ type PhoneChangedMailCall struct { OldPhone string } +type IdentityLinkedMailCall struct { + User *models.User + Provider string +} + +type IdentityUnlinkedMailCall struct { + User *models.User + Provider string +} + type MFAFactorEnrolledMailCall struct { User *models.User FactorType string @@ -193,6 +205,22 @@ func (m *MockMailer) PhoneChangedNotificationMail(r *http.Request, user *models. return nil } +func (m *MockMailer) IdentityLinkedNotificationMail(r *http.Request, user *models.User, provider string) error { + m.IdentityLinkedMailCalls = append(m.IdentityLinkedMailCalls, IdentityLinkedMailCall{ + User: user, + Provider: provider, + }) + return nil +} + +func (m *MockMailer) IdentityUnlinkedNotificationMail(r *http.Request, user *models.User, provider string) error { + m.IdentityUnlinkedMailCalls = append(m.IdentityUnlinkedMailCalls, IdentityUnlinkedMailCall{ + User: user, + Provider: provider, + }) + return nil +} + func (m *MockMailer) MFAFactorEnrolledNotificationMail(r *http.Request, user *models.User, factorType string) error { m.MFAFactorEnrolledMailCalls = append(m.MFAFactorEnrolledMailCalls, MFAFactorEnrolledMailCall{ User: user, @@ -221,6 +249,8 @@ func (m *MockMailer) Reset() { m.PasswordChangedMailCalls = nil m.EmailChangedMailCalls = nil m.PhoneChangedMailCalls = nil + m.IdentityLinkedMailCalls = nil + m.IdentityUnlinkedMailCalls = nil m.MFAFactorEnrolledMailCalls = nil m.MFAFactorUnenrolledMailCalls = nil } diff --git a/internal/mailer/templatemailer/template.go b/internal/mailer/templatemailer/template.go index f31f0084d..a0de7f4f2 100644 --- a/internal/mailer/templatemailer/template.go +++ b/internal/mailer/templatemailer/template.go @@ -541,6 +541,10 @@ func lookupEmailContentConfig( return cfg.EmailChangedNotification, true case PhoneChangedNotificationTemplate: return cfg.PhoneChangedNotification, true + case IdentityLinkedNotificationTemplate: + return cfg.IdentityLinkedNotification, true + case IdentityUnlinkedNotificationTemplate: + return cfg.IdentityUnlinkedNotification, true case MFAFactorEnrolledNotificationTemplate: return cfg.MFAFactorEnrolledNotification, true case MFAFactorUnenrolledNotificationTemplate: diff --git a/internal/mailer/templatemailer/templatemailer.go b/internal/mailer/templatemailer/templatemailer.go index 05726baad..7cde7eff0 100644 --- a/internal/mailer/templatemailer/templatemailer.go +++ b/internal/mailer/templatemailer/templatemailer.go @@ -23,6 +23,8 @@ const ( PasswordChangedNotificationTemplate = "password_changed_notification" EmailChangedNotificationTemplate = "email_changed_notification" PhoneChangedNotificationTemplate = "phone_changed_notification" + IdentityLinkedNotificationTemplate = "identity_linked_notification" + IdentityUnlinkedNotificationTemplate = "identity_unlinked_notification" MFAFactorEnrolledNotificationTemplate = "mfa_factor_enrolled_notification" MFAFactorUnenrolledNotificationTemplate = "mfa_factor_unenrolled_notification" ) @@ -82,6 +84,18 @@ const defaultPhoneChangedNotificationMail = `

Your phone number has been chan

If you did not make this change, please contact support immediately.

` +const defaultIdentityLinkedNotificationMail = `

A new identity has been linked

+ +

A new identity ({{ .Provider }}) has been linked to your account {{ .Email }}.

+

If you did not make this change, please contact support immediately.

+` + +const defaultIdentityUnlinkedNotificationMail = `

An identity has been unlinked

+ +

An identity ({{ .Provider }}) has been unlinked from your account {{ .Email }}.

+

If you did not make this change, please contact support immediately.

+` + const defaultMFAFactorEnrolledNotificationMail = `

MFA factor has been enrolled

A new factor ({{ .FactorType }}) has been enrolled for your account {{ .Email }}.

@@ -107,6 +121,8 @@ var ( PasswordChangedNotificationTemplate, EmailChangedNotificationTemplate, PhoneChangedNotificationTemplate, + IdentityLinkedNotificationTemplate, + IdentityUnlinkedNotificationTemplate, MFAFactorEnrolledNotificationTemplate, MFAFactorUnenrolledNotificationTemplate, } @@ -122,6 +138,8 @@ var ( PasswordChangedNotification: "Your password has been changed", EmailChangedNotification: "Your email address has been changed", PhoneChangedNotification: "Your phone number has been changed", + IdentityLinkedNotification: "A new identity has been linked", + IdentityUnlinkedNotification: "An identity has been unlinked", MFAFactorEnrolledNotification: "MFA factor enrolled", MFAFactorUnenrolledNotification: "MFA factor unenrolled", } @@ -137,6 +155,8 @@ var ( PasswordChangedNotification: defaultPasswordChangedNotificationMail, EmailChangedNotification: defaultEmailChangedNotificationMail, PhoneChangedNotification: defaultPhoneChangedNotificationMail, + IdentityLinkedNotification: defaultIdentityLinkedNotificationMail, + IdentityUnlinkedNotification: defaultIdentityUnlinkedNotificationMail, MFAFactorEnrolledNotification: defaultMFAFactorEnrolledNotificationMail, MFAFactorUnenrolledNotification: defaultMFAFactorUnenrolledNotificationMail, } @@ -430,6 +450,24 @@ func (m *Mailer) PhoneChangedNotificationMail(r *http.Request, user *models.User return m.mail(r.Context(), m.cfg, PhoneChangedNotificationTemplate, user.GetEmail(), data) } +func (m *Mailer) IdentityLinkedNotificationMail(r *http.Request, user *models.User, provider string) error { + data := map[string]any{ + "Email": user.GetEmail(), + "Provider": provider, // the provider of the newly linked identity + "Data": user.UserMetaData, + } + return m.mail(r.Context(), m.cfg, IdentityLinkedNotificationTemplate, user.GetEmail(), data) +} + +func (m *Mailer) IdentityUnlinkedNotificationMail(r *http.Request, user *models.User, provider string) error { + data := map[string]any{ + "Email": user.GetEmail(), + "Provider": provider, // the provider of the unlinked identity + "Data": user.UserMetaData, + } + return m.mail(r.Context(), m.cfg, IdentityUnlinkedNotificationTemplate, user.GetEmail(), data) +} + func (m *Mailer) MFAFactorEnrolledNotificationMail(r *http.Request, user *models.User, factorType string) error { data := map[string]any{ "Email": user.GetEmail(),