mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-29 21:41:02 +00:00 
			
		
		
		
	[SECURITY] Notify users about account security changes
- Currently if the password, primary mail, TOTP or security keys are changed, no notification is made of that and makes compromising an account a bit easier as it's essentially undetectable until the original person tries to log in. Although other changes should be made as well (re-authing before allowing a password change), this should go a long way of improving the account security in Forgejo. - Adds a mail notification for password and primary mail changes. For the primary mail change, a mail notification is sent to the old primary mail. - Add a mail notification when TOTP or a security keys is removed, if no other 2FA method is configured the mail will also contain that 2FA is no longer needed to log into their account. - `MakeEmailAddressPrimary` is refactored to the user service package, as it now involves calling the mailer service. - Unit tests added. - Integration tests added.
This commit is contained in:
		
					parent
					
						
							
								ded237ee77
							
						
					
				
			
			
				commit
				
					
						4383da91bd
					
				
			
		
					 24 changed files with 543 additions and 116 deletions
				
			
		|  | @ -17,6 +17,7 @@ import ( | |||
| 	"time" | ||||
| 
 | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	auth_model "code.gitea.io/gitea/models/auth" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
|  | @ -35,10 +36,14 @@ import ( | |||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	mailAuthActivate       base.TplName = "auth/activate" | ||||
| 	mailAuthActivateEmail  base.TplName = "auth/activate_email" | ||||
| 	mailAuthResetPassword  base.TplName = "auth/reset_passwd" | ||||
| 	mailAuthRegisterNotify base.TplName = "auth/register_notify" | ||||
| 	mailAuthActivate           base.TplName = "auth/activate" | ||||
| 	mailAuthActivateEmail      base.TplName = "auth/activate_email" | ||||
| 	mailAuthResetPassword      base.TplName = "auth/reset_passwd" | ||||
| 	mailAuthRegisterNotify     base.TplName = "auth/register_notify" | ||||
| 	mailAuthPasswordChange     base.TplName = "auth/password_change" | ||||
| 	mailAuthPrimaryMailChange  base.TplName = "auth/primary_mail_change" | ||||
| 	mailAuth2faDisabled        base.TplName = "auth/2fa_disabled" | ||||
| 	mailAuthRemovedSecurityKey base.TplName = "auth/removed_security_key" | ||||
| 
 | ||||
| 	mailNotifyCollaborator base.TplName = "notify/collaborator" | ||||
| 
 | ||||
|  | @ -561,3 +566,133 @@ func fromDisplayName(u *user_model.User) string { | |||
| 	} | ||||
| 	return u.GetCompleteName() | ||||
| } | ||||
| 
 | ||||
| // SendPasswordChange informs the user on their primary email address that | ||||
| // their password was changed. | ||||
| func SendPasswordChange(u *user_model.User) error { | ||||
| 	if setting.MailService == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 
 | ||||
| 	data := map[string]any{ | ||||
| 		"locale":      locale, | ||||
| 		"DisplayName": u.DisplayName(), | ||||
| 		"Username":    u.Name, | ||||
| 		"Language":    locale.Language(), | ||||
| 	} | ||||
| 
 | ||||
| 	var content bytes.Buffer | ||||
| 
 | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthPasswordChange), data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), locale.TrString("mail.password_change.subject"), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, password change notification", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // SendPrimaryMailChange informs the user on their old primary email address | ||||
| // that it's no longer used as primary mail and will no longer receive | ||||
| // notification on that email address. | ||||
| func SendPrimaryMailChange(u *user_model.User, oldPrimaryEmail string) error { | ||||
| 	if setting.MailService == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 
 | ||||
| 	data := map[string]any{ | ||||
| 		"locale":         locale, | ||||
| 		"NewPrimaryMail": u.Email, | ||||
| 		"DisplayName":    u.DisplayName(), | ||||
| 		"Username":       u.Name, | ||||
| 		"Language":       locale.Language(), | ||||
| 	} | ||||
| 
 | ||||
| 	var content bytes.Buffer | ||||
| 
 | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthPrimaryMailChange), data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(oldPrimaryEmail), locale.TrString("mail.primary_mail_change.subject"), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, primary email change notification", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // SendDisabledTOTP informs the user that their totp has been disabled. | ||||
| func SendDisabledTOTP(ctx context.Context, u *user_model.User) error { | ||||
| 	if setting.MailService == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 
 | ||||
| 	hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	data := map[string]any{ | ||||
| 		"locale":      locale, | ||||
| 		"HasWebAuthn": hasWebAuthn, | ||||
| 		"DisplayName": u.DisplayName(), | ||||
| 		"Username":    u.Name, | ||||
| 		"Language":    locale.Language(), | ||||
| 	} | ||||
| 
 | ||||
| 	var content bytes.Buffer | ||||
| 
 | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuth2faDisabled), data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), locale.TrString("mail.totp_disabled.subject"), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, 2fa disabled notification", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // SendRemovedWebAuthn informs the user that one of their security keys has been removed. | ||||
| func SendRemovedSecurityKey(ctx context.Context, u *user_model.User, securityKeyName string) error { | ||||
| 	if setting.MailService == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 
 | ||||
| 	hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	hasTOTP, err := auth_model.HasTwoFactorByUID(ctx, u.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	data := map[string]any{ | ||||
| 		"locale":          locale, | ||||
| 		"HasWebAuthn":     hasWebAuthn, | ||||
| 		"HasTOTP":         hasTOTP, | ||||
| 		"SecurityKeyName": securityKeyName, | ||||
| 		"DisplayName":     u.DisplayName(), | ||||
| 		"Username":        u.Name, | ||||
| 		"Language":        locale.Language(), | ||||
| 	} | ||||
| 
 | ||||
| 	var content bytes.Buffer | ||||
| 
 | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRemovedSecurityKey), data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	msg := NewMessage(u.EmailTo(), locale.TrString("mail.removed_security_key.subject"), content.String()) | ||||
| 	msg.Info = fmt.Sprintf("UID: %d, security key removed notification", u.ID) | ||||
| 
 | ||||
| 	SendAsync(msg) | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -55,14 +55,14 @@ func TestAdminNotificationMail_test(t *testing.T) { | |||
| 		defer test.MockVariableValue(&setting.Admin.SendNotificationEmailOnNewUser, true)() | ||||
| 
 | ||||
| 		called := false | ||||
| 		defer mockMailSettings(func(msgs ...*Message) { | ||||
| 		defer MockMailSettings(func(msgs ...*Message) { | ||||
| 			assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent") | ||||
| 			assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance") | ||||
| 			manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(users[1].ID, 10) | ||||
| 			assert.Contains(t, msgs[0].Body, manageUserURL) | ||||
| 			assert.Contains(t, msgs[0].Body, users[1].HTMLURL()) | ||||
| 			assert.Contains(t, msgs[0].Body, users[1].Name, "user name of the newly created user") | ||||
| 			assertTranslatedLocale(t, msgs[0].Body, "mail.admin", "admin.users") | ||||
| 			AssertTranslatedLocale(t, msgs[0].Body, "mail.admin", "admin.users") | ||||
| 			called = true | ||||
| 		})() | ||||
| 		MailNewUser(ctx, users[1]) | ||||
|  | @ -71,7 +71,7 @@ func TestAdminNotificationMail_test(t *testing.T) { | |||
| 
 | ||||
| 	t.Run("SendNotificationEmailOnNewUser_false", func(t *testing.T) { | ||||
| 		defer test.MockVariableValue(&setting.Admin.SendNotificationEmailOnNewUser, false)() | ||||
| 		defer mockMailSettings(func(msgs ...*Message) { | ||||
| 		defer MockMailSettings(func(msgs ...*Message) { | ||||
| 			assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since SEND_NOTIFICATION_EMAIL_ON_NEW_USER is disabled") | ||||
| 		})() | ||||
| 		MailNewUser(ctx, users[1]) | ||||
|  |  | |||
							
								
								
									
										60
									
								
								services/mailer/mail_auth_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								services/mailer/mail_auth_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| // Copyright 2024 The Forgejo Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package mailer_test | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	"code.gitea.io/gitea/services/mailer" | ||||
| 	user_service "code.gitea.io/gitea/services/user" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func TestPasswordChangeMail(t *testing.T) { | ||||
| 	defer require.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 	called := false | ||||
| 	defer mailer.MockMailSettings(func(msgs ...*mailer.Message) { | ||||
| 		assert.Len(t, msgs, 1) | ||||
| 		assert.Equal(t, user.EmailTo(), msgs[0].To) | ||||
| 		assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.password_change.subject"), msgs[0].Subject) | ||||
| 		mailer.AssertTranslatedLocale(t, msgs[0].Body, "mail.password_change.text_1", "mail.password_change.text_2", "mail.password_change.text_3") | ||||
| 		called = true | ||||
| 	})() | ||||
| 
 | ||||
| 	require.NoError(t, user_service.UpdateAuth(db.DefaultContext, user, &user_service.UpdateAuthOptions{Password: optional.Some("NewPasswordYolo!")})) | ||||
| 	assert.True(t, called) | ||||
| } | ||||
| 
 | ||||
| func TestPrimaryMailChange(t *testing.T) { | ||||
| 	defer require.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 	firstEmail := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 3, UID: user.ID, IsPrimary: true}) | ||||
| 	secondEmail := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}, "is_primary = false") | ||||
| 
 | ||||
| 	called := false | ||||
| 	defer mailer.MockMailSettings(func(msgs ...*mailer.Message) { | ||||
| 		assert.False(t, called) | ||||
| 		assert.Len(t, msgs, 1) | ||||
| 		assert.Equal(t, user.EmailTo(firstEmail.Email), msgs[0].To) | ||||
| 		assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.primary_mail_change.subject"), msgs[0].Subject) | ||||
| 		assert.Contains(t, msgs[0].Body, secondEmail.Email) | ||||
| 		mailer.AssertTranslatedLocale(t, msgs[0].Body, "mail.primary_mail_change.text_1", "mail.primary_mail_change.text_2", "mail.primary_mail_change.text_3") | ||||
| 		called = true | ||||
| 	})() | ||||
| 
 | ||||
| 	require.NoError(t, user_service.MakeEmailAddressPrimary(db.DefaultContext, user, secondEmail, true)) | ||||
| 	assert.True(t, called) | ||||
| 
 | ||||
| 	require.NoError(t, user_service.MakeEmailAddressPrimary(db.DefaultContext, user, firstEmail, false)) | ||||
| } | ||||
|  | @ -62,7 +62,7 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re | |||
| } | ||||
| 
 | ||||
| func TestComposeIssueCommentMessage(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	doer, _, issue, comment := prepareMailerTest(t) | ||||
| 
 | ||||
| 	markup.Init(&markup.ProcessorHelper{ | ||||
|  | @ -117,7 +117,7 @@ func TestComposeIssueCommentMessage(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestComposeIssueMessage(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	doer, _, issue, _ := prepareMailerTest(t) | ||||
| 
 | ||||
| 	recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}} | ||||
|  | @ -146,7 +146,7 @@ func TestComposeIssueMessage(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestMailerIssueTemplate(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
|  | @ -160,7 +160,7 @@ func TestMailerIssueTemplate(t *testing.T) { | |||
| 		for _, s := range expected { | ||||
| 			assert.Contains(t, wholemsg, s) | ||||
| 		} | ||||
| 		assertTranslatedLocale(t, wholemsg, "mail.issue") | ||||
| 		AssertTranslatedLocale(t, wholemsg, "mail.issue") | ||||
| 	} | ||||
| 
 | ||||
| 	testCompose := func(t *testing.T, ctx *mailCommentContext) *Message { | ||||
|  | @ -241,7 +241,7 @@ func TestMailerIssueTemplate(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestTemplateSelection(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	doer, repo, issue, comment := prepareMailerTest(t) | ||||
| 	recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}} | ||||
| 
 | ||||
|  | @ -296,7 +296,7 @@ func TestTemplateSelection(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestTemplateServices(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	doer, _, issue, comment := prepareMailerTest(t) | ||||
| 	assert.NoError(t, issue.LoadRepo(db.DefaultContext)) | ||||
| 
 | ||||
|  | @ -349,7 +349,7 @@ func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recip | |||
| } | ||||
| 
 | ||||
| func TestGenerateAdditionalHeaders(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	doer, _, issue, _ := prepareMailerTest(t) | ||||
| 
 | ||||
| 	ctx := &mailCommentContext{Context: context.TODO() /* TODO: use a correct context */, Issue: issue, Doer: doer} | ||||
|  | @ -382,7 +382,7 @@ func TestGenerateAdditionalHeaders(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func Test_createReference(t *testing.T) { | ||||
| 	defer mockMailSettings(nil)() | ||||
| 	defer MockMailSettings(nil)() | ||||
| 	_, _, issue, comment := prepareMailerTest(t) | ||||
| 	_, _, pullIssue, _ := prepareMailerTest(t) | ||||
| 	pullIssue.IsPull = true | ||||
|  |  | |||
|  | @ -22,14 +22,14 @@ func TestMain(m *testing.M) { | |||
| 	unittest.MainTest(m) | ||||
| } | ||||
| 
 | ||||
| func assertTranslatedLocale(t *testing.T, message string, prefixes ...string) { | ||||
| func AssertTranslatedLocale(t *testing.T, message string, prefixes ...string) { | ||||
| 	t.Helper() | ||||
| 	for _, prefix := range prefixes { | ||||
| 		assert.NotContains(t, message, prefix, "there is an untranslated locale prefix") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func mockMailSettings(send func(msgs ...*Message)) func() { | ||||
| func MockMailSettings(send func(msgs ...*Message)) func() { | ||||
| 	translation.InitLocales(context.Background()) | ||||
| 	subjectTemplates, bodyTemplates = templates.Mailer(context.Background()) | ||||
| 	mailService := setting.Mailer{ | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import ( | |||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/services/mailer" | ||||
| ) | ||||
| 
 | ||||
| // AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address | ||||
|  | @ -163,7 +164,7 @@ func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *us | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = user_model.MakeEmailPrimaryWithUser(ctx, user, email) | ||||
| 	err = MakeEmailAddressPrimary(ctx, user, email, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -190,3 +191,42 @@ func DeleteEmailAddresses(ctx context.Context, u *user_model.User, emails []stri | |||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func MakeEmailAddressPrimary(ctx context.Context, u *user_model.User, newPrimaryEmail *user_model.EmailAddress, notify bool) error { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	oldPrimaryEmail := u.Email | ||||
| 
 | ||||
| 	// 1. Update user table | ||||
| 	u.Email = newPrimaryEmail.Email | ||||
| 	if _, err = sess.ID(u.ID).Cols("email").Update(u); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// 2. Update old primary email | ||||
| 	if _, err = sess.Where("uid=? AND is_primary=?", u.ID, true).Cols("is_primary").Update(&user_model.EmailAddress{ | ||||
| 		IsPrimary: false, | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// 3. update new primary email | ||||
| 	newPrimaryEmail.IsPrimary = true | ||||
| 	if _, err = sess.ID(newPrimaryEmail.ID).Cols("is_primary").Update(newPrimaryEmail); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := committer.Commit(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if notify { | ||||
| 		return mailer.SendPrimaryMailChange(u, oldPrimaryEmail) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/gobwas/glob" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) { | ||||
|  | @ -163,3 +164,15 @@ func TestDeleteEmailAddresses(t *testing.T) { | |||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) | ||||
| } | ||||
| 
 | ||||
| func TestMakeEmailAddressPrimary(t *testing.T) { | ||||
| 	require.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 	newPrimaryEmail := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}, "is_primary = false") | ||||
| 
 | ||||
| 	require.NoError(t, MakeEmailAddressPrimary(db.DefaultContext, user, newPrimaryEmail, false)) | ||||
| 
 | ||||
| 	unittest.AssertExistsIf(t, true, &user_model.User{ID: 2, Email: newPrimaryEmail.Email}) | ||||
| 	unittest.AssertExistsIf(t, true, &user_model.EmailAddress{ID: 3, UID: user.ID}, "is_primary = false") | ||||
| 	unittest.AssertExistsIf(t, true, &user_model.EmailAddress{ID: 35, UID: user.ID, IsPrimary: true}) | ||||
| } | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/services/mailer" | ||||
| ) | ||||
| 
 | ||||
| type UpdateOptions struct { | ||||
|  | @ -220,5 +221,13 @@ func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions | |||
| 		u.ProhibitLogin = opts.ProhibitLogin.Value() | ||||
| 	} | ||||
| 
 | ||||
| 	return user_model.UpdateUserCols(ctx, u, "login_type", "login_source", "login_name", "passwd", "passwd_hash_algo", "salt", "must_change_password", "prohibit_login") | ||||
| 	if err := user_model.UpdateUserCols(ctx, u, "login_type", "login_source", "login_name", "passwd", "passwd_hash_algo", "salt", "must_change_password", "prohibit_login"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Password.Has() { | ||||
| 		return mailer.SendPasswordChange(u) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue