summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/admin_user_change_password.go44
-rw-r--r--models/fixtures/email_address.yml8
-rw-r--r--models/fixtures/user.yml39
-rw-r--r--models/user/email_address.go129
-rw-r--r--models/user/email_address_test.go90
-rw-r--r--models/user/error.go15
-rw-r--r--models/user/user.go154
-rw-r--r--models/user/user_test.go67
-rw-r--r--modules/auth/password/password.go17
-rw-r--r--modules/auth/password/pwn.go36
-rw-r--r--modules/auth/password/pwn/pwn.go2
-rw-r--r--modules/auth/password/pwn/pwn_test.go83
-rw-r--r--modules/optional/option.go45
-rw-r--r--modules/optional/option_test.go48
-rw-r--r--routers/api/v1/admin/user.go148
-rw-r--r--routers/api/v1/org/org.go36
-rw-r--r--routers/api/v1/user/email.go43
-rw-r--r--routers/api/v1/user/settings.go43
-rw-r--r--routers/web/admin/users.go184
-rw-r--r--routers/web/auth/auth.go46
-rw-r--r--routers/web/auth/oauth.go40
-rw-r--r--routers/web/auth/password.go117
-rw-r--r--routers/web/org/setting.go73
-rw-r--r--routers/web/repo/middlewares.go10
-rw-r--r--routers/web/user/setting/account.go85
-rw-r--r--routers/web/user/setting/profile.go103
-rw-r--r--services/auth/auth.go8
-rw-r--r--services/auth/source/ldap/source_authenticate.go16
-rw-r--r--services/auth/source/ldap/source_sync.go32
-rw-r--r--services/mailer/mail.go8
-rw-r--r--services/mailer/notify.go4
-rw-r--r--services/user/avatar.go2
-rw-r--r--services/user/email.go166
-rw-r--r--services/user/email_test.go129
-rw-r--r--services/user/update.go212
-rw-r--r--services/user/update_test.go120
-rw-r--r--services/user/user.go5
-rw-r--r--services/user/user_test.go2
-rw-r--r--tests/integration/api_admin_test.go10
-rw-r--r--tests/integration/api_nodeinfo_test.go2
-rw-r--r--tests/integration/api_user_email_test.go10
-rw-r--r--tests/integration/empty_repo_test.go10
42 files changed, 1378 insertions, 1063 deletions
diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index 22764318fd..824d66d112 100644
--- a/cmd/admin_user_change_password.go
+++ b/cmd/admin_user_change_password.go
@@ -4,13 +4,14 @@
package cmd
import (
- "context"
"errors"
"fmt"
user_model "code.gitea.io/gitea/models/user"
- pwd "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2"
)
@@ -50,35 +51,32 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
- if len(c.String("password")) < setting.MinPasswordLength {
- return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
- }
- if !pwd.IsComplexEnough(c.String("password")) {
- return errors.New("Password does not meet complexity requirements")
- }
- pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
- if err != nil {
- return err
- }
- if pwned {
- return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
- }
- uname := c.String("username")
- user, err := user_model.GetUserByName(ctx, uname)
+ user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
- if err = user.SetPassword(c.String("password")); err != nil {
- return err
- }
+ var mustChangePassword optional.Option[bool]
if c.IsSet("must-change-password") {
- user.MustChangePassword = c.Bool("must-change-password")
+ mustChangePassword = optional.Some(c.Bool("must-change-password"))
}
- if err = user_model.UpdateUserCols(ctx, user, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
- return err
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(c.String("password")),
+ MustChangePassword: mustChangePassword,
+ }
+ if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
+ case errors.Is(err, password.ErrComplexity):
+ return errors.New("Password does not meet complexity requirements")
+ case errors.Is(err, password.ErrIsPwned):
+ return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
+ default:
+ return err
+ }
}
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml
index ce4d5208df..67a99f43e2 100644
--- a/models/fixtures/email_address.yml
+++ b/models/fixtures/email_address.yml
@@ -285,3 +285,11 @@
lower_email: abcde@gitea.com
is_activated: true
is_primary: false
+
+-
+ id: 37
+ uid: 37
+ email: user37@example.com
+ lower_email: user37@example.com
+ is_activated: true
+ is_primary: true
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 79fbb981f6..aa0daedd85 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -1095,7 +1095,7 @@
allow_git_hook: false
allow_import_local: false
allow_create_organization: true
- prohibit_login: true
+ prohibit_login: false
avatar: avatar29
avatar_email: user30@example.com
use_custom_avatar: false
@@ -1332,3 +1332,40 @@
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false
+
+-
+ id: 37
+ lower_name: user37
+ name: user37
+ full_name: User 37
+ email: user37@example.com
+ keep_email_private: false
+ email_notifications_preference: enabled
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
+ must_change_password: false
+ login_source: 0
+ login_name: user37
+ type: 0
+ salt: ZogKvWdyEx
+ max_repo_creation: -1
+ is_active: true
+ is_admin: false
+ is_restricted: false
+ allow_git_hook: false
+ allow_import_local: false
+ allow_create_organization: true
+ prohibit_login: true
+ avatar: avatar29
+ avatar_email: user37@example.com
+ use_custom_avatar: false
+ num_followers: 0
+ num_following: 0
+ num_stars: 0
+ num_repos: 0
+ num_teams: 0
+ num_members: 0
+ visibility: 0
+ repo_admin_change_team_access: false
+ theme: ""
+ keep_activity_private: false
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 2af2621f5f..957e72fe89 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -142,12 +142,24 @@ func (email *EmailAddress) BeforeInsert() {
}
}
+func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress, error) {
+ if err := db.Insert(ctx, email); err != nil {
+ return nil, err
+ }
+ return email, nil
+}
+
+func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
+ _, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
+ return err
+}
+
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
// ValidateEmail check if email is a allowed address
func ValidateEmail(email string) error {
if len(email) == 0 {
- return nil
+ return ErrEmailInvalid{email}
}
if !emailRegexp.MatchString(email) {
@@ -177,6 +189,36 @@ func ValidateEmail(email string) error {
return nil
}
+func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
+ ea := &EmailAddress{}
+ if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrEmailAddressNotExist{email}
+ }
+ return ea, nil
+}
+
+func GetEmailAddressOfUser(ctx context.Context, email string, uid int64) (*EmailAddress, error) {
+ ea := &EmailAddress{}
+ if has, err := db.GetEngine(ctx).Where("lower_email=? AND uid=?", strings.ToLower(email), uid).Get(ea); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrEmailAddressNotExist{email}
+ }
+ return ea, nil
+}
+
+func GetPrimaryEmailAddressOfUser(ctx context.Context, uid int64) (*EmailAddress, error) {
+ ea := &EmailAddress{}
+ if has, err := db.GetEngine(ctx).Where("uid=? AND is_primary=?", uid, true).Get(ea); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrEmailAddressNotExist{}
+ }
+ return ea, nil
+}
+
// GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5)
@@ -235,91 +277,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
}
-// AddEmailAddress adds an email address to given user.
-func AddEmailAddress(ctx context.Context, email *EmailAddress) error {
- email.Email = strings.TrimSpace(email.Email)
- used, err := IsEmailUsed(ctx, email.Email)
- if err != nil {
- return err
- } else if used {
- return ErrEmailAlreadyUsed{email.Email}
- }
-
- if err = ValidateEmail(email.Email); err != nil {
- return err
- }
-
- return db.Insert(ctx, email)
-}
-
-// AddEmailAddresses adds an email address to given user.
-func AddEmailAddresses(ctx context.Context, emails []*EmailAddress) error {
- if len(emails) == 0 {
- return nil
- }
-
- // Check if any of them has been used
- for i := range emails {
- emails[i].Email = strings.TrimSpace(emails[i].Email)
- used, err := IsEmailUsed(ctx, emails[i].Email)
- if err != nil {
- return err
- } else if used {
- return ErrEmailAlreadyUsed{emails[i].Email}
- }
- if err = ValidateEmail(emails[i].Email); err != nil {
- return err
- }
- }
-
- if err := db.Insert(ctx, emails); err != nil {
- return fmt.Errorf("Insert: %w", err)
- }
-
- return nil
-}
-
-// DeleteEmailAddress deletes an email address of given user.
-func DeleteEmailAddress(ctx context.Context, email *EmailAddress) (err error) {
- if email.IsPrimary {
- return ErrPrimaryEmailCannotDelete{Email: email.Email}
- }
-
- var deleted int64
- // ask to check UID
- address := EmailAddress{
- UID: email.UID,
- }
- if email.ID > 0 {
- deleted, err = db.GetEngine(ctx).ID(email.ID).Delete(&address)
- } else {
- if email.Email != "" && email.LowerEmail == "" {
- email.LowerEmail = strings.ToLower(email.Email)
- }
- deleted, err = db.GetEngine(ctx).
- Where("lower_email=?", email.LowerEmail).
- Delete(&address)
- }
-
- if err != nil {
- return err
- } else if deleted != 1 {
- return ErrEmailAddressNotExist{Email: email.Email}
- }
- return nil
-}
-
-// DeleteEmailAddresses deletes multiple email addresses
-func DeleteEmailAddresses(ctx context.Context, emails []*EmailAddress) (err error) {
- for i := range emails {
- if err = DeleteEmailAddress(ctx, emails[i]); err != nil {
- return err
- }
- }
-
- return nil
-}
-
// DeleteInactiveEmailAddresses deletes inactive email addresses
func DeleteInactiveEmailAddresses(ctx context.Context) error {
_, err := db.GetEngine(ctx).
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index 7f3ca75cfd..140443f82f 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -42,96 +42,6 @@ func TestIsEmailUsed(t *testing.T) {
assert.False(t, isExist)
}
-func TestAddEmailAddress(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- assert.NoError(t, user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
- Email: "user1234567890@example.com",
- LowerEmail: "user1234567890@example.com",
- IsPrimary: true,
- IsActivated: true,
- }))
-
- // ErrEmailAlreadyUsed
- err := user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
- Email: "user1234567890@example.com",
- LowerEmail: "user1234567890@example.com",
- })
- assert.Error(t, err)
- assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
-}
-
-func TestAddEmailAddresses(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // insert multiple email address
- emails := make([]*user_model.EmailAddress, 2)
- emails[0] = &user_model.EmailAddress{
- Email: "user1234@example.com",
- LowerEmail: "user1234@example.com",
- IsActivated: true,
- }
- emails[1] = &user_model.EmailAddress{
- Email: "user5678@example.com",
- LowerEmail: "user5678@example.com",
- IsActivated: true,
- }
- assert.NoError(t, user_model.AddEmailAddresses(db.DefaultContext, emails))
-
- // ErrEmailAlreadyUsed
- err := user_model.AddEmailAddresses(db.DefaultContext, emails)
- assert.Error(t, err)
- assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
-}
-
-func TestDeleteEmailAddress(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
- UID: int64(1),
- ID: int64(33),
- Email: "user1-2@example.com",
- LowerEmail: "user1-2@example.com",
- }))
-
- assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
- UID: int64(1),
- Email: "user1-3@example.com",
- LowerEmail: "user1-3@example.com",
- }))
-
- // Email address does not exist
- err := user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
- UID: int64(1),
- Email: "user1234567890@example.com",
- LowerEmail: "user1234567890@example.com",
- })
- assert.Error(t, err)
-}
-
-func TestDeleteEmailAddresses(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // delete multiple email address
- emails := make([]*user_model.EmailAddress, 2)
- emails[0] = &user_model.EmailAddress{
- UID: int64(2),
- ID: int64(3),
- Email: "user2@example.com",
- LowerEmail: "user2@example.com",
- }
- emails[1] = &user_model.EmailAddress{
- UID: int64(2),
- Email: "user2-2@example.com",
- LowerEmail: "user2-2@example.com",
- }
- assert.NoError(t, user_model.DeleteEmailAddresses(db.DefaultContext, emails))
-
- // ErrEmailAlreadyUsed
- err := user_model.DeleteEmailAddresses(db.DefaultContext, emails)
- assert.Error(t, err)
-}
-
func TestMakeEmailPrimary(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/models/user/error.go b/models/user/error.go
index f512994169..ef572c178a 100644
--- a/models/user/error.go
+++ b/models/user/error.go
@@ -108,18 +108,3 @@ func IsErrUserIsNotLocal(err error) bool {
_, ok := err.(ErrUserIsNotLocal)
return ok
}
-
-type ErrUsernameNotChanged struct {
- UID int64
- Name string
-}
-
-func (err ErrUsernameNotChanged) Error() string {
- return fmt.Sprintf("username hasn't been changed[uid: %d, name: %s]", err.UID, err.Name)
-}
-
-// IsErrUsernameNotChanged
-func IsErrUsernameNotChanged(err error) bool {
- _, ok := err.(ErrUsernameNotChanged)
- return ok
-}
diff --git a/models/user/user.go b/models/user/user.go
index 269a1be725..e5245dfbb0 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -196,18 +196,6 @@ func (u *User) SetLastLogin() {
u.LastLoginUnix = timeutil.TimeStampNow()
}
-// UpdateUserDiffViewStyle updates the users diff view style
-func UpdateUserDiffViewStyle(ctx context.Context, u *User, style string) error {
- u.DiffViewStyle = style
- return UpdateUserCols(ctx, u, "diff_view_style")
-}
-
-// UpdateUserTheme updates a users' theme irrespective of the site wide theme
-func UpdateUserTheme(ctx context.Context, u *User, themeName string) error {
- u.Theme = themeName
- return UpdateUserCols(ctx, u, "theme")
-}
-
// GetPlaceholderEmail returns an noreply email
func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
@@ -378,13 +366,6 @@ func (u *User) NewGitSig() *git.Signature {
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
// change passwd, salt and passwd_hash_algo fields
func (u *User) SetPassword(passwd string) (err error) {
- if len(passwd) == 0 {
- u.Passwd = ""
- u.Salt = ""
- u.PasswdHashAlgo = ""
- return nil
- }
-
if u.Salt, err = GetUserSalt(); err != nil {
return err
}
@@ -488,21 +469,6 @@ func (u *User) IsMailable() bool {
return u.IsActive
}
-// EmailNotifications returns the User's email notification preference
-func (u *User) EmailNotifications() string {
- return u.EmailNotificationsPreference
-}
-
-// SetEmailNotifications sets the user's email notification preference
-func SetEmailNotifications(ctx context.Context, u *User, set string) error {
- u.EmailNotificationsPreference = set
- if err := UpdateUserCols(ctx, u, "email_notifications_preference"); err != nil {
- log.Error("SetEmailNotifications: %v", err)
- return err
- }
- return nil
-}
-
// IsUserExist checks if given user name exist,
// the user name should be noncased unique.
// If uid is presented, then check will rule out that one,
@@ -705,8 +671,13 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
if u.Rands, err = GetUserSalt(); err != nil {
return err
}
- if err = u.SetPassword(u.Passwd); err != nil {
- return err
+ if u.Passwd != "" {
+ if err = u.SetPassword(u.Passwd); err != nil {
+ return err
+ }
+ } else {
+ u.Salt = ""
+ u.PasswdHashAlgo = ""
}
// save changes to database
@@ -817,24 +788,6 @@ func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
return nil
}
-// checkDupEmail checks whether there are the same email with the user
-func checkDupEmail(ctx context.Context, u *User) error {
- u.Email = strings.ToLower(u.Email)
- has, err := db.GetEngine(ctx).
- Where("id!=?", u.ID).
- And("type=?", u.Type).
- And("email=?", u.Email).
- Get(new(User))
- if err != nil {
- return err
- } else if has {
- return ErrEmailAlreadyUsed{
- Email: u.Email,
- }
- }
- return nil
-}
-
// ValidateUser check if user is valid to insert / update into database
func ValidateUser(u *User, cols ...string) error {
if len(cols) == 0 || util.SliceContainsString(cols, "visibility", true) {
@@ -843,81 +796,9 @@ func ValidateUser(u *User, cols ...string) error {
}
}
- if len(cols) == 0 || util.SliceContainsString(cols, "email", true) {
- u.Email = strings.ToLower(u.Email)
- if err := ValidateEmail(u.Email); err != nil {
- return err
- }
- }
return nil
}
-// UpdateUser updates user's information.
-func UpdateUser(ctx context.Context, u *User, changePrimaryEmail bool, cols ...string) error {
- err := ValidateUser(u, cols...)
- if err != nil {
- return err
- }
-
- e := db.GetEngine(ctx)
-
- if changePrimaryEmail {
- var emailAddress EmailAddress
- has, err := e.Where("lower_email=?", strings.ToLower(u.Email)).Get(&emailAddress)
- if err != nil {
- return err
- }
- if has && emailAddress.UID != u.ID {
- return ErrEmailAlreadyUsed{
- Email: u.Email,
- }
- }
- // 1. Update old primary email
- if _, err = e.Where("uid=? AND is_primary=?", u.ID, true).Cols("is_primary").Update(&EmailAddress{
- IsPrimary: false,
- }); err != nil {
- return err
- }
-
- if !has {
- emailAddress.Email = u.Email
- emailAddress.UID = u.ID
- emailAddress.IsActivated = true
- emailAddress.IsPrimary = true
- if _, err := e.Insert(&emailAddress); err != nil {
- return err
- }
- } else if _, err := e.ID(emailAddress.ID).Cols("is_primary").Update(&EmailAddress{
- IsPrimary: true,
- }); err != nil {
- return err
- }
- } else if !u.IsOrganization() { // check if primary email in email_address table
- primaryEmailExist, err := e.Where("uid=? AND is_primary=?", u.ID, true).Exist(&EmailAddress{})
- if err != nil {
- return err
- }
-
- if !primaryEmailExist {
- if _, err := e.Insert(&EmailAddress{
- Email: u.Email,
- UID: u.ID,
- IsActivated: true,
- IsPrimary: true,
- }); err != nil {
- return err
- }
- }
- }
-
- if len(cols) == 0 {
- _, err = e.ID(u.ID).AllCols().Update(u)
- } else {
- _, err = e.ID(u.ID).Cols(cols...).Update(u)
- }
- return err
-}
-
// UpdateUserCols update user according special columns
func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
if err := ValidateUser(u, cols...); err != nil {
@@ -928,25 +809,6 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
return err
}
-// UpdateUserSetting updates user's settings.
-func UpdateUserSetting(ctx context.Context, u *User) (err error) {
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if !u.IsOrganization() {
- if err = checkDupEmail(ctx, u); err != nil {
- return err
- }
- }
- if err = UpdateUser(ctx, u, false); err != nil {
- return err
- }
- return committer.Commit()
-}
-
// GetInactiveUsers gets all inactive users
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
var cond builder.Cond = builder.Eq{"is_active": false}
@@ -1044,7 +906,7 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string {
if err != nil {
continue
}
- if u.IsMailable() && u.EmailNotifications() != EmailNotificationsDisabled {
+ if u.IsMailable() && u.EmailNotificationsPreference != EmailNotificationsDisabled {
mails = append(mails, u.Email)
}
}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 65aebea43a..f3e5a95b1e 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -101,13 +101,13 @@ func TestSearchUsers(t *testing.T) {
}
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
- []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34})
+ []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
[]int64{9})
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
- []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34})
+ []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
@@ -123,7 +123,7 @@ func TestSearchUsers(t *testing.T) {
[]int64{29})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue},
- []int64{30})
+ []int64{37})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
[]int64{24})
@@ -147,20 +147,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
{user_model.EmailNotificationsOnMention, 9},
} {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID})
- assert.Equal(t, test.expected, user.EmailNotifications())
-
- // Try all possible settings
- assert.NoError(t, user_model.SetEmailNotifications(db.DefaultContext, user, user_model.EmailNotificationsEnabled))
- assert.Equal(t, user_model.EmailNotificationsEnabled, user.EmailNotifications())
-
- assert.NoError(t, user_model.SetEmailNotifications(db.DefaultContext, user, user_model.EmailNotificationsOnMention))
- assert.Equal(t, user_model.EmailNotificationsOnMention, user.EmailNotifications())
-
- assert.NoError(t, user_model.SetEmailNotifications(db.DefaultContext, user, user_model.EmailNotificationsDisabled))
- assert.Equal(t, user_model.EmailNotificationsDisabled, user.EmailNotifications())
-
- assert.NoError(t, user_model.SetEmailNotifications(db.DefaultContext, user, user_model.EmailNotificationsAndYourOwn))
- assert.Equal(t, user_model.EmailNotificationsAndYourOwn, user.EmailNotifications())
+ assert.Equal(t, test.expected, user.EmailNotificationsPreference)
}
}
@@ -343,42 +330,6 @@ func TestGetMaileableUsersByIDs(t *testing.T) {
}
}
-func TestUpdateUser(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-
- user.KeepActivityPrivate = true
- assert.NoError(t, user_model.UpdateUser(db.DefaultContext, user, false))
- user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.True(t, user.KeepActivityPrivate)
-
- setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, false}
- user.KeepActivityPrivate = false
- user.Visibility = structs.VisibleTypePrivate
- assert.Error(t, user_model.UpdateUser(db.DefaultContext, user, false))
- user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.True(t, user.KeepActivityPrivate)
-
- newEmail := "new_" + user.Email
- user.Email = newEmail
- assert.NoError(t, user_model.UpdateUser(db.DefaultContext, user, true))
- user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.Equal(t, newEmail, user.Email)
-
- user.Email = "no mail@mail.org"
- assert.Error(t, user_model.UpdateUser(db.DefaultContext, user, true))
-}
-
-func TestUpdateUserEmailAlreadyUsed(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
-
- user2.Email = org3.Email
- err := user_model.UpdateUser(db.DefaultContext, user2, true)
- assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
-}
-
func TestNewUserRedirect(t *testing.T) {
// redirect to a completely new name
assert.NoError(t, unittest.PrepareTestDatabase())
@@ -534,14 +485,12 @@ func Test_ValidateUser(t *testing.T) {
}()
setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true}
kases := map[*user_model.User]bool{
- {ID: 1, Visibility: structs.VisibleTypePublic}: true,
- {ID: 2, Visibility: structs.VisibleTypeLimited}: false,
- {ID: 2, Visibility: structs.VisibleTypeLimited, Email: "invalid"}: false,
- {ID: 2, Visibility: structs.VisibleTypePrivate, Email: "valid@valid.com"}: true,
+ {ID: 1, Visibility: structs.VisibleTypePublic}: true,
+ {ID: 2, Visibility: structs.VisibleTypeLimited}: false,
+ {ID: 2, Visibility: structs.VisibleTypePrivate}: true,
}
for kase, expected := range kases {
- err := user_model.ValidateUser(kase)
- assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase))
+ assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), fmt.Sprintf("case: %+v", kase))
}
}
diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go
index 2172dc8b44..2c7205b708 100644
--- a/modules/auth/password/password.go
+++ b/modules/auth/password/password.go
@@ -5,8 +5,9 @@ package password
import (
"bytes"
- goContext "context"
+ "context"
"crypto/rand"
+ "errors"
"math/big"
"strings"
"sync"
@@ -15,6 +16,11 @@ import (
"code.gitea.io/gitea/modules/translation"
)
+var (
+ ErrComplexity = errors.New("password not complex enough")
+ ErrMinLength = errors.New("password not long enough")
+)
+
// complexity contains information about a particular kind of password complexity
type complexity struct {
ValidChars string
@@ -101,11 +107,14 @@ func Generate(n int) (string, error) {
}
buffer[j] = validChars[rnd.Int64()]
}
- pwned, err := IsPwned(goContext.Background(), string(buffer))
- if err != nil {
+
+ if err := IsPwned(context.Background(), string(buffer)); err != nil {
+ if errors.Is(err, ErrIsPwned) {
+ continue
+ }
return "", err
}
- if IsComplexEnough(string(buffer)) && !pwned && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
+ if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
return string(buffer), nil
}
}
diff --git a/modules/auth/password/pwn.go b/modules/auth/password/pwn.go
index df425ac659..e00205ea19 100644
--- a/modules/auth/password/pwn.go
+++ b/modules/auth/password/pwn.go
@@ -5,24 +5,48 @@ package password
import (
"context"
+ "errors"
+ "fmt"
"code.gitea.io/gitea/modules/auth/password/pwn"
"code.gitea.io/gitea/modules/setting"
)
+var ErrIsPwned = errors.New("password has been pwned")
+
+type ErrIsPwnedRequest struct {
+ err error
+}
+
+func IsErrIsPwnedRequest(err error) bool {
+ _, ok := err.(ErrIsPwnedRequest)
+ return ok
+}
+
+func (err ErrIsPwnedRequest) Error() string {
+ return fmt.Sprintf("using Have-I-Been-Pwned service failed: %v", err.err)
+}
+
+func (err ErrIsPwnedRequest) Unwrap() error {
+ return err.err
+}
+
// IsPwned checks whether a password has been pwned
-// NOTE: This func returns true if it encounters an error under the assumption that you ALWAYS want to check against
-// HIBP, so not getting a response should block a password until it can be verified.
-func IsPwned(ctx context.Context, password string) (bool, error) {
+// If a password has not been pwned, no error is returned.
+func IsPwned(ctx context.Context, password string) error {
if !setting.PasswordCheckPwn {
- return false, nil
+ return nil
}
client := pwn.New(pwn.WithContext(ctx))
count, err := client.CheckPassword(password, true)
if err != nil {
- return true, err
+ return ErrIsPwnedRequest{err}
+ }
+
+ if count > 0 {
+ return ErrIsPwned
}
- return count > 0, nil
+ return nil
}
diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go
index b5a015fb9c..f77ce9f40b 100644
--- a/modules/auth/password/pwn/pwn.go
+++ b/modules/auth/password/pwn/pwn.go
@@ -73,7 +73,7 @@ func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*h
// because artificial responses will be added to the response
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
- if strings.TrimSpace(pw) == "" {
+ if pw == "" {
return -1, ErrEmptyPassword
}
diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go
index 148208b964..f9deadc8d7 100644
--- a/modules/auth/password/pwn/pwn_test.go
+++ b/modules/auth/password/pwn/pwn_test.go
@@ -4,13 +4,14 @@
package pwn
import (
- "errors"
"math/rand"
"net/http"
"os"
"strings"
"testing"
"time"
+
+ "github.com/stretchr/testify/assert"
)
var client = New(WithHTTP(&http.Client{
@@ -25,78 +26,44 @@ func TestMain(m *testing.M) {
func TestPassword(t *testing.T) {
// Check input error
_, err := client.CheckPassword("", false)
- if err == nil {
- t.Log("blank input should return an error")
- t.Fail()
- }
- if !errors.Is(err, ErrEmptyPassword) {
- t.Log("blank input should return ErrEmptyPassword")
- t.Fail()
- }
+ assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword")
// Should fail
fail := "password1234"
count, err := client.CheckPassword(fail, false)
- if err != nil {
- t.Log(err)
- t.Fail()
- }
- if count == 0 {
- t.Logf("%s should fail as a password\n", fail)
- t.Fail()
- }
+ assert.NotEmpty(t, count, "%s should fail as a password", fail)
+ assert.NoError(t, err)
// Should fail (with padding)
failPad := "administrator"
count, err = client.CheckPassword(failPad, true)
- if err != nil {
- t.Log(err)
- t.Fail()
- }
- if count == 0 {
- t.Logf("%s should fail as a password\n", failPad)
- t.Fail()
- }
+ assert.NotEmpty(t, count, "%s should fail as a password", failPad)
+ assert.NoError(t, err)
// Checking for a "good" password isn't going to be perfect, but we can give it a good try
// with hopefully minimal error. Try five times?
- var good bool
- var pw string
- for idx := 0; idx <= 5; idx++ {
- pw = testPassword()
- count, err = client.CheckPassword(pw, false)
- if err != nil {
- t.Log(err)
- t.Fail()
+ assert.Condition(t, func() bool {
+ for i := 0; i <= 5; i++ {
+ count, err = client.CheckPassword(testPassword(), false)
+ assert.NoError(t, err)
+ if count == 0 {
+ return true
+ }
}
- if count == 0 {
- good = true
- break
- }
- }
- if !good {
- t.Log("no generated passwords passed. there is a chance this is a fluke")
- t.Fail()
- }
+ return false
+ }, "no generated passwords passed. there is a chance this is a fluke")
// Again, but with padded responses
- good = false
- for idx := 0; idx <= 5; idx++ {
- pw = testPassword()
- count, err = client.CheckPassword(pw, true)
- if err != nil {
- t.Log(err)
- t.Fail()
+ assert.Condition(t, func() bool {
+ for i := 0; i <= 5; i++ {
+ count, err = client.CheckPassword(testPassword(), true)
+ assert.NoError(t, err)
+ if count == 0 {
+ return true
+ }
}
- if count == 0 {
- good = true
- break
- }
- }
- if !good {
- t.Log("no generated passwords passed. there is a chance this is a fluke")
- t.Fail()
- }
+ return false
+ }, "no generated passwords passed. there is a chance this is a fluke")
}
// Credit to https://golangbyexample.com/generate-random-password-golang/
diff --git a/modules/optional/option.go b/modules/optional/option.go
new file mode 100644
index 0000000000..af9e5ac852
--- /dev/null
+++ b/modules/optional/option.go
@@ -0,0 +1,45 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional
+
+type Option[T any] []T
+
+func None[T any]() Option[T] {
+ return nil
+}
+
+func Some[T any](v T) Option[T] {
+ return Option[T]{v}
+}
+
+func FromPtr[T any](v *T) Option[T] {
+ if v == nil {
+ return None[T]()
+ }
+ return Some(*v)
+}
+
+func FromNonDefault[T comparable](v T) Option[T] {
+ var zero T
+ if v == zero {
+ return None[T]()
+ }
+ return Some(v)
+}
+
+func (o Option[T]) Has() bool {
+ return o != nil
+}
+
+func (o Option[T]) Value() T {
+ var zero T
+ return o.ValueOrDefault(zero)
+}
+
+func (o Option[T]) ValueOrDefault(v T) T {
+ if o.Has() {
+ return o[0]
+ }
+ return v
+}
diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
new file mode 100644
index 0000000000..7ec345b6ba
--- /dev/null
+++ b/modules/optional/option_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestOption(t *testing.T) {
+ var uninitialized Option[int]
+ assert.False(t, uninitialized.Has())
+ assert.Equal(t, int(0), uninitialized.Value())
+ assert.Equal(t, int(1), uninitialized.ValueOrDefault(1))
+
+ none := None[int]()
+ assert.False(t, none.Has())
+ assert.Equal(t, int(0), none.Value())
+ assert.Equal(t, int(1), none.ValueOrDefault(1))
+
+ some := Some[int](1)
+ assert.True(t, some.Has())
+ assert.Equal(t, int(1), some.Value())
+ assert.Equal(t, int(1), some.ValueOrDefault(2))
+
+ var ptr *int
+ assert.False(t, FromPtr(ptr).Has())
+
+ opt1 := FromPtr(util.ToPointer(1))
+ assert.True(t, opt1.Has())
+ assert.Equal(t, int(1), opt1.Value())
+
+ assert.False(t, FromNonDefault("").Has())
+
+ opt2 := FromNonDefault("test")
+ assert.True(t, opt2.Has())
+ assert.Equal(t, "test", opt2.Value())
+
+ assert.False(t, FromNonDefault(0).Has())
+
+ opt3 := FromNonDefault(1)
+ assert.True(t, opt3.Has())
+ assert.Equal(t, int(1), opt3.Value())
+}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index b4cc42ea5d..272996f43d 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
- "strings"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -18,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -107,9 +107,8 @@ func CreateUser(ctx *context.APIContext) {
return
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
- if err != nil {
+ if err := password.IsPwned(ctx, form.Password); err != nil {
+ if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
@@ -192,115 +191,65 @@ func EditUser(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditUserOption)
- parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
- if ctx.Written() {
- return
+ authOpts := &user_service.UpdateAuthOptions{
+ LoginSource: optional.FromNonDefault(form.SourceID),
+ LoginName: optional.Some(form.LoginName),
+ Password: optional.FromNonDefault(form.Password),
+ MustChangePassword: optional.FromPtr(form.MustChangePassword),
+ ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
-
- if len(form.Password) != 0 {
- if len(form.Password) < setting.MinPasswordLength {
+ if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
- return
- }
- if !password.IsComplexEnough(form.Password) {
- err := errors.New("PasswordComplexity")
+ case errors.Is(err, password.ErrComplexity):
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
- return
- }
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
- if err != nil {
- log.Error(err.Error())
- }
- ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
- return
- }
- if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
- return
- }
- if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
- ctx.InternalServerError(err)
- return
+ case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
+ ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
+ default:
+ ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
}
+ return
}
- if form.MustChangePassword != nil {
- ctx.ContextUser.MustChangePassword = *form.MustChangePassword
- }
-
- ctx.ContextUser.LoginName = form.LoginName
-
- if form.FullName != nil {
- ctx.ContextUser.FullName = *form.FullName
- }
- var emailChanged bool
if form.Email != nil {
- email := strings.TrimSpace(*form.Email)
- if len(email) == 0 {
- ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
- return
- }
-
- if err := user_model.ValidateEmail(email); err != nil {
- ctx.InternalServerError(err)
- return
- }
-
- emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
- ctx.ContextUser.Email = email
- }
- if form.Website != nil {
- ctx.ContextUser.Website = *form.Website
- }
- if form.Location != nil {
- ctx.ContextUser.Location = *form.Location
- }
- if form.Description != nil {
- ctx.ContextUser.Description = *form.Description
- }
- if form.Active != nil {
- ctx.ContextUser.IsActive = *form.Active
- }
- if len(form.Visibility) != 0 {
- ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
- }
- if form.Admin != nil {
- if !*form.Admin && user_model.IsLastAdminUser(ctx, ctx.ContextUser) {
- ctx.Error(http.StatusBadRequest, "LastAdmin", ctx.Tr("auth.last_admin"))
+ if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
+ switch {
+ case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
+ ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
+ case user_model.IsErrEmailAlreadyUsed(err):
+ ctx.Error(http.StatusBadRequest, "EmailUsed", err)
+ default:
+ ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err)
+ }
return
}
- ctx.ContextUser.IsAdmin = *form.Admin
- }
- if form.AllowGitHook != nil {
- ctx.ContextUser.AllowGitHook = *form.AllowGitHook
- }
- if form.AllowImportLocal != nil {
- ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
- }
- if form.MaxRepoCreation != nil {
- ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
- }
- if form.AllowCreateOrganization != nil {
- ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
- }
- if form.ProhibitLogin != nil {
- ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
- }
- if form.Restricted != nil {
- ctx.ContextUser.IsRestricted = *form.Restricted
}
- if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
- if user_model.IsErrEmailAlreadyUsed(err) ||
- user_model.IsErrEmailCharIsNotSupported(err) ||
- user_model.IsErrEmailInvalid(err) {
- ctx.Error(http.StatusUnprocessableEntity, "", err)
+ opts := &user_service.UpdateOptions{
+ FullName: optional.FromPtr(form.FullName),
+ Website: optional.FromPtr(form.Website),
+ Location: optional.FromPtr(form.Location),
+ Description: optional.FromPtr(form.Description),
+ IsActive: optional.FromPtr(form.Active),
+ IsAdmin: optional.FromPtr(form.Admin),
+ Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
+ AllowGitHook: optional.FromPtr(form.AllowGitHook),
+ AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
+ MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
+ AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
+ IsRestricted: optional.FromPtr(form.Restricted),
+ }
+
+ if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
+ if models.IsErrDeleteLastAdminUser(err) {
+ ctx.Error(http.StatusBadRequest, "LastAdmin", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
}
return
}
+
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
@@ -527,9 +476,6 @@ func RenameUser(ctx *context.APIContext) {
// Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch {
- case user_model.IsErrUsernameNotChanged(err):
- // Noop as username is not changed
- ctx.Status(http.StatusNoContent)
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
@@ -545,5 +491,5 @@ func RenameUser(ctx *context.APIContext) {
}
log.Trace("User name changed: %s -> %s", oldName, newName)
- ctx.Status(http.StatusOK)
+ ctx.Status(http.StatusNoContent)
}
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index d5fac1e5b8..255e28c706 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -13,12 +13,14 @@ import (
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
+ user_service "code.gitea.io/gitea/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@@ -337,28 +339,30 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization"
// "404":
// "$ref": "#/responses/notFound"
+
form := web.GetForm(ctx).(*api.EditOrgOption)
- org := ctx.Org.Organization
- org.FullName = form.FullName
- org.Email = form.Email
- org.Description = form.Description
- org.Website = form.Website
- org.Location = form.Location
- if form.Visibility != "" {
- org.Visibility = api.VisibilityModes[form.Visibility]
+
+ if form.Email != "" {
+ if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
+ return
+ }
}
- if form.RepoAdminChangeTeamAccess != nil {
- org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
+
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Description: optional.Some(form.Description),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
+ RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
- if err := user_model.UpdateUserCols(ctx, org.AsUser(),
- "full_name", "description", "website", "location",
- "visibility", "repo_admin_change_team_access",
- ); err != nil {
- ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
+ if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
+ ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
}
// Delete an organization
diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go
index 68f6c974a5..3dcea9083c 100644
--- a/routers/api/v1/user/email.go
+++ b/routers/api/v1/user/email.go
@@ -9,10 +9,10 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
+ user_service "code.gitea.io/gitea/services/user"
)
// ListEmails list all of the authenticated user's email addresses
@@ -56,22 +56,14 @@ func AddEmail(ctx *context.APIContext) {
// "$ref": "#/responses/EmailList"
// "422":
// "$ref": "#/responses/validationError"
+
form := web.GetForm(ctx).(*api.CreateEmailOption)
if len(form.Emails) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty")
return
}
- emails := make([]*user_model.EmailAddress, len(form.Emails))
- for i := range form.Emails {
- emails[i] = &user_model.EmailAddress{
- UID: ctx.Doer.ID,
- Email: form.Emails[i],
- IsActivated: !setting.Service.RegisterEmailConfirm,
- }
- }
-
- if err := user_model.AddEmailAddresses(ctx, emails); err != nil {
+ if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
@@ -91,11 +83,17 @@ func AddEmail(ctx *context.APIContext) {
return
}
- apiEmails := make([]*api.Email, len(emails))
- for i := range emails {
- apiEmails[i] = convert.ToEmail(emails[i])
+ emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err)
+ return
}
- ctx.JSON(http.StatusCreated, &apiEmails)
+
+ apiEmails := make([]*api.Email, 0, len(emails))
+ for _, email := range emails {
+ apiEmails = append(apiEmails, convert.ToEmail(email))
+ }
+ ctx.JSON(http.StatusCreated, apiEmails)
}
// DeleteEmail delete email
@@ -115,26 +113,19 @@ func DeleteEmail(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+
form := web.GetForm(ctx).(*api.DeleteEmailOption)
if len(form.Emails) == 0 {
ctx.Status(http.StatusNoContent)
return
}
- emails := make([]*user_model.EmailAddress, len(form.Emails))
- for i := range form.Emails {
- emails[i] = &user_model.EmailAddress{
- Email: form.Emails[i],
- UID: ctx.Doer.ID,
- }
- }
-
- if err := user_model.DeleteEmailAddresses(ctx, emails); err != nil {
+ if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAddressNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err)
- return
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
}
- ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go
index 53794c82f8..062df1ca43 100644
--- a/routers/api/v1/user/settings.go
+++ b/routers/api/v1/user/settings.go
@@ -6,11 +6,12 @@ package user
import (
"net/http"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
+ user_service "code.gitea.io/gitea/services/user"
)
// GetUserSettings returns user settings
@@ -44,36 +45,18 @@ func UpdateUserSettings(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.UserSettingsOptions)
- if form.FullName != nil {
- ctx.Doer.FullName = *form.FullName
+ opts := &user_service.UpdateOptions{
+ FullName: optional.FromPtr(form.FullName),
+ Description: optional.FromPtr(form.Description),
+ Website: optional.FromPtr(form.Website),
+ Location: optional.FromPtr(form.Location),
+ Language: optional.FromPtr(form.Language),
+ Theme: optional.FromPtr(form.Theme),
+ DiffViewStyle: optional.FromPtr(form.DiffViewStyle),
+ KeepEmailPrivate: optional.FromPtr(form.HideEmail),
+ KeepActivityPrivate: optional.FromPtr(form.HideActivity),
}
- if form.Description != nil {
- ctx.Doer.Description = *form.Description
- }
- if form.Website != nil {
- ctx.Doer.Website = *form.Website
- }
- if form.Location != nil {
- ctx.Doer.Location = *form.Location
- }
- if form.Language != nil {
- ctx.Doer.Language = *form.Language
- }
- if form.Theme != nil {
- ctx.Doer.Theme = *form.Theme
- }
- if form.DiffViewStyle != nil {
- ctx.Doer.DiffViewStyle = *form.DiffViewStyle
- }
-
- if form.HideEmail != nil {
- ctx.Doer.KeepEmailPrivate = *form.HideEmail
- }
- if form.HideActivity != nil {
- ctx.Doer.KeepActivityPrivate = *form.HideActivity
- }
-
- if err := user_model.UpdateUser(ctx, ctx.Doer, false); err != nil {
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.InternalServerError(err)
return
}
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 8f6995b96f..af184fa9eb 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -5,6 +5,7 @@
package admin
import (
+ "errors"
"net/http"
"net/url"
"strconv"
@@ -20,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -162,11 +164,10 @@ func NewUserPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
return
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ if err := password.IsPwned(ctx, form.Password); err != nil {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
+ if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -184,10 +185,7 @@ func NewUserPost(ctx *context.Context) {
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
- case user_model.IsErrEmailCharIsNotSupported(err):
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
- case user_model.IsErrEmailInvalid(err):
+ case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case db.IsErrNameReserved(err):
@@ -348,67 +346,110 @@ func EditUserPost(ctx *context.Context) {
return
}
+ if form.UserName != "" {
+ if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
+ switch {
+ case user_model.IsErrUserIsNotLocal(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
+ case db.IsErrNameReserved(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
+ default:
+ ctx.ServerError("RenameUser", err)
+ }
+ return
+ }
+ }
+
+ authOpts := &user_service.UpdateAuthOptions{
+ Password: optional.FromNonDefault(form.Password),
+ LoginName: optional.Some(form.LoginName),
+ }
+
+ // skip self Prohibit Login
+ if ctx.Doer.ID == u.ID {
+ authOpts.ProhibitLogin = optional.Some(false)
+ } else {
+ authOpts.ProhibitLogin = optional.Some(form.ProhibitLogin)
+ }
+
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
- loginType, _ := strconv.ParseInt(fields[0], 10, 0)
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
- if u.LoginSource != authSource {
- u.LoginSource = authSource
- u.LoginType = auth.Type(loginType)
- }
+ authOpts.LoginSource = optional.Some(authSource)
}
- if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
- var err error
- if len(form.Password) < setting.MinPasswordLength {
+ if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
- return
- }
- if !password.IsComplexEnough(form.Password) {
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
- return
- }
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ case errors.Is(err, password.ErrIsPwned):
ctx.Data["Err_Password"] = true
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.RenderWithErr(errMsg, tplUserEdit, &form)
- return
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
+ default:
+ ctx.ServerError("UpdateUser", err)
}
+ return
+ }
- if err := user_model.ValidateEmail(form.Email); err != nil {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_error"), tplUserEdit, &form)
+ if form.Email != "" {
+ if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
+ switch {
+ case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
+ case user_model.IsErrEmailAlreadyUsed(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
+ default:
+ ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
+ }
return
}
+ }
- if u.Salt, err = user_model.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- if err = u.SetPassword(form.Password); err != nil {
- ctx.ServerError("SetPassword", err)
- return
- }
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ IsActive: optional.Some(form.Active),
+ IsAdmin: optional.Some(form.Admin),
+ AllowGitHook: optional.Some(form.AllowGitHook),
+ AllowImportLocal: optional.Some(form.AllowImportLocal),
+ MaxRepoCreation: optional.Some(form.MaxRepoCreation),
+ AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
+ IsRestricted: optional.Some(form.Restricted),
+ Visibility: optional.Some(form.Visibility),
}
- if len(form.UserName) != 0 && u.Name != form.UserName {
- if err := user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
- if ctx.Written() {
- return
- }
- ctx.RenderWithErr(ctx.Flash.ErrorMsg, tplUserEdit, &form)
- return
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ if models.IsErrDeleteLastAdminUser(err) {
+ ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
+ } else {
+ ctx.ServerError("UpdateUser", err)
}
- u.Name = form.UserName
- u.LowerName = strings.ToLower(form.UserName)
+ return
}
+ log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
if form.Reset2FA {
tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
@@ -433,52 +474,7 @@ func EditUserPost(ctx *context.Context) {
return
}
}
-
- }
-
- // Check whether user is the last admin
- if !form.Admin && user_model.IsLastAdminUser(ctx, u) {
- ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
- return
- }
-
- u.LoginName = form.LoginName
- u.FullName = form.FullName
- emailChanged := !strings.EqualFold(u.Email, form.Email)
- u.Email = form.Email
- u.Website = form.Website
- u.Location = form.Location
- u.MaxRepoCreation = form.MaxRepoCreation
- u.IsActive = form.Active
- u.IsAdmin = form.Admin
- u.IsRestricted = form.Restricted
- u.AllowGitHook = form.AllowGitHook
- u.AllowImportLocal = form.AllowImportLocal
- u.AllowCreateOrganization = form.AllowCreateOrganization
-
- u.Visibility = form.Visibility
-
- // skip self Prohibit Login
- if ctx.Doer.ID == u.ID {
- u.ProhibitLogin = false
- } else {
- u.ProhibitLogin = form.ProhibitLogin
- }
-
- if err := user_model.UpdateUser(ctx, u, emailChanged); err != nil {
- if user_model.IsErrEmailAlreadyUsed(err) {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
- } else if user_model.IsErrEmailCharIsNotSupported(err) ||
- user_model.IsErrEmailInvalid(err) {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
- } else {
- ctx.ServerError("UpdateUser", err)
- }
- return
}
- log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 474bae98e4..3de1f3373d 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -30,6 +31,7 @@ import (
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/markbates/goth"
)
@@ -104,9 +106,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
func resetLocale(ctx *context.Context, u *user_model.User) error {
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
- if len(u.Language) == 0 {
- u.Language = ctx.Locale.Language()
- if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
+ if u.Language == "" {
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(ctx.Locale.Language()),
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
return err
}
}
@@ -330,10 +334,12 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
- if len(u.Language) == 0 {
- u.Language = ctx.Locale.Language()
- if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
- ctx.ServerError("UpdateUserCols Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
+ if u.Language == "" {
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(ctx.Locale.Language()),
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
return setting.AppSubURL + "/"
}
}
@@ -348,9 +354,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Csrf.DeleteCookie(ctx)
// Register last login
- u.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, u, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
+ ctx.ServerError("UpdateUser", err)
return setting.AppSubURL + "/"
}
@@ -482,10 +487,9 @@ func SignUpPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
return
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ if err := password.IsPwned(ctx, form.Password); err != nil {
errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
+ if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -589,10 +593,12 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
if user_model.CountUsers(ctx, nil) == 1 {
- u.IsAdmin = true
- u.IsActive = true
- u.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_active", "last_login_unix"); err != nil {
+ opts := &user_service.UpdateOptions{
+ IsActive: optional.Some(true),
+ IsAdmin: optional.Some(true),
+ SetLastLogin: true,
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return false
}
@@ -752,10 +758,8 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
- // Register last login
- user.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 00305a36ee..07140b6674 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -990,7 +991,9 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source)
- setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
+ isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
+ u.IsAdmin = isAdmin.ValueOrDefault(false)
+ u.IsRestricted = isRestricted.ValueOrDefault(false)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@@ -1054,19 +1057,17 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
-func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
+func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
- wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
-
if source.AdminGroup != "" {
- u.IsAdmin = groups.Contains(source.AdminGroup)
+ isAdmin = optional.Some(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
- u.IsRestricted = groups.Contains(source.RestrictedGroup)
+ isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
}
- return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
+ return isAdmin, isRestricted
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
@@ -1133,18 +1134,12 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
- // Register last login
- u.SetLastLogin()
-
- // Update GroupClaims
- changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
- cols := []string{"last_login_unix"}
- if changed {
- cols = append(cols, "is_admin", "is_restricted")
+ opts := &user_service.UpdateOptions{
+ SetLastLogin: true,
}
-
- if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
@@ -1177,10 +1172,11 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
- changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
- if changed {
- if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ opts := &user_service.UpdateOptions{}
+ opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
+ if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
}
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index def9c2bcaa..5af1696a64 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
@@ -21,6 +22,7 @@ import (
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/services/user"
)
var (
@@ -165,30 +167,6 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
- // Validate password length.
- passwd := ctx.FormString("password")
- if len(passwd) < setting.MinPasswordLength {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
- return
- } else if !password.IsComplexEnough(passwd) {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
- return
- } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(errMsg, tplResetPassword, nil)
- return
- }
-
// Handle two-factor
regenerateScratchToken := false
if twofa != nil {
@@ -221,18 +199,27 @@ func ResetPasswdPost(ctx *context.Context) {
}
}
}
- var err error
- if u.Rands, err = user_model.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- if err = u.SetPassword(passwd); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
+
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(ctx.FormString("password")),
+ MustChangePassword: optional.Some(false),
}
- u.MustChangePassword = false
- if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
+ if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
+ default:
+ ctx.ServerError("UpdateAuth", err)
+ }
return
}
@@ -242,7 +229,7 @@ func ResetPasswdPost(ctx *context.Context) {
if regenerateScratchToken {
// Invalidate the scratch token.
- _, err = twofa.GenerateScratchToken()
+ _, err := twofa.GenerateScratchToken()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -282,11 +269,11 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMustChangePassword)
return
}
- u := ctx.Doer
+
// Make sure only requests for users who are eligible to change their password via
// this method passes through
- if !u.MustChangePassword {
- ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
+ if !ctx.Doer.MustChangePassword {
+ ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
return
}
@@ -296,44 +283,34 @@ func MustChangePasswordPost(ctx *context.Context) {
return
}
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
- return
- }
-
- if !password.IsComplexEnough(form.Password) {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
- return
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(form.Password),
+ MustChangePassword: optional.Some(false),
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
- ctx.Data["Err_Password"] = true
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
+ if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
+ default:
+ ctx.ServerError("UpdateAuth", err)
}
- ctx.RenderWithErr(errMsg, tplMustChangePassword, &form)
- return
- }
-
- if err = u.SetPassword(form.Password); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- u.MustChangePassword = false
-
- if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
- log.Trace("User updated password: %s", u.Name)
+ log.Trace("User updated password: %s", ctx.Doer.Name)
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp)
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index f0d9259d3f..47d0063f76 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -7,7 +7,6 @@ package org
import (
"net/http"
"net/url"
- "strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@@ -71,53 +71,50 @@ func SettingsPost(ctx *context.Context) {
}
org := ctx.Org.Organization
- nameChanged := org.Name != form.Name
-
- // Check if organization name has been changed.
- if nameChanged {
- err := user_service.RenameUser(ctx, org.AsUser(), form.Name)
- switch {
- case user_model.IsErrUserAlreadyExist(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
- return
- case db.IsErrNameReserved(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- return
- case db.IsErrNamePatternNotAllowed(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- return
- case err != nil:
- ctx.ServerError("org_service.RenameOrganization", err)
+
+ if org.Name != form.Name {
+ if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
+ } else if db.IsErrNameReserved(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ } else {
+ ctx.ServerError("RenameUser", err)
+ }
return
}
- // reset ctx.org.OrgLink with new name
- ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
- log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
+ ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
}
- // In case it's just a case change.
- org.Name = form.Name
- org.LowerName = strings.ToLower(form.Name)
+ if form.Email != "" {
+ if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
+ return
+ }
+ }
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Description: optional.Some(form.Description),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ Visibility: optional.Some(form.Visibility),
+ RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
+ }
if ctx.Doer.IsAdmin {
- org.MaxRepoCreation = form.MaxRepoCreation
+ opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
}
- org.FullName = form.FullName
- org.Email = form.Email
- org.Description = form.Description
- org.Website = form.Website
- org.Location = form.Location
- org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
-
- visibilityChanged := form.Visibility != org.Visibility
- org.Visibility = form.Visibility
+ visibilityChanged := org.Visibility != form.Visibility
- if err := user_model.UpdateUser(ctx, org.AsUser(), false); err != nil {
+ if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index 5f4a219aa3..ee49649654 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -11,6 +11,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/optional"
+ user_service "code.gitea.io/gitea/services/user"
)
// SetEditorconfigIfExists set editor config as render variable
@@ -55,8 +57,12 @@ func SetDiffViewStyle(ctx *context.Context) {
}
ctx.Data["IsSplitStyle"] = style == "split"
- if err := user_model.UpdateUserDiffViewStyle(ctx, ctx.Doer, style); err != nil {
- ctx.ServerError("ErrUpdateDiffViewStyle", err)
+
+ opts := &user_service.UpdateOptions{
+ DiffViewStyle: optional.Some(style),
+ }
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
}
}
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 7a306636e0..c7f194a3b5 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
@@ -53,33 +54,33 @@ func AccountPost(ctx *context.Context) {
return
}
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
- } else if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
+ if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
- } else if !password.IsComplexEnough(form.Password) {
- ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
- } else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.Flash.Error(errMsg)
} else {
- var err error
- if err = ctx.Doer.SetPassword(form.Password); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
+ opts := &user.UpdateAuthOptions{
+ Password: optional.Some(form.Password),
+ MustChangePassword: optional.Some(false),
}
- if err := user_model.UpdateUserCols(ctx, ctx.Doer, "salt", "passwd_hash_algo", "passwd"); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
+ if err := user.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
+ default:
+ ctx.ServerError("UpdateAuth", err)
+ return
+ }
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
- log.Trace("User password updated: %s", ctx.Doer.Name)
- ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
@@ -137,7 +138,7 @@ func EmailPost(ctx *context.Context) {
// Only fired when the primary email is inactive (Wrong state)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
} else {
- mailer.SendActivateEmailMail(ctx.Doer, email)
+ mailer.SendActivateEmailMail(ctx.Doer, email.Email)
}
address = email.Email
@@ -160,9 +161,12 @@ func EmailPost(ctx *context.Context) {
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return
}
- if err := user_model.SetEmailNotifications(ctx, ctx.Doer, preference); err != nil {
+ opts := &user.UpdateOptions{
+ EmailNotificationsPreference: optional.Some(preference),
+ }
+ if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil {
log.Error("Set Email Notifications failed: %v", err)
- ctx.ServerError("SetEmailNotifications", err)
+ ctx.ServerError("UpdateUser", err)
return
}
log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name)
@@ -178,48 +182,47 @@ func EmailPost(ctx *context.Context) {
return
}
- email := &user_model.EmailAddress{
- UID: ctx.Doer.ID,
- Email: form.Email,
- IsActivated: !setting.Service.RegisterEmailConfirm,
- }
- if err := user_model.AddEmailAddress(ctx, email); err != nil {
+ if err := user.AddEmailAddresses(ctx, ctx.Doer, []string{form.Email}); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
- return
- } else if user_model.IsErrEmailCharIsNotSupported(err) ||
- user_model.IsErrEmailInvalid(err) {
+ } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
- return
+ } else {
+ ctx.ServerError("AddEmailAddresses", err)
}
- ctx.ServerError("AddEmailAddress", err)
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
- mailer.SendActivateEmailMail(ctx.Doer, email)
+ mailer.SendActivateEmailMail(ctx.Doer, form.Email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
- ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
+ ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", form.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
- log.Trace("Email address added: %s", email.Email)
+ log.Trace("Email address added: %s", form.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
- if err := user_model.DeleteEmailAddress(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
- ctx.ServerError("DeleteEmail", err)
+ email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
+ if err != nil || email == nil {
+ ctx.ServerError("GetEmailAddressByID", err)
+ return
+ }
+
+ if err := user.DeleteEmailAddresses(ctx, ctx.Doer, []string{email.Email}); err != nil {
+ ctx.ServerError("DeleteEmailAddresses", err)
return
}
log.Trace("Email address deleted: %s", ctx.Doer.Name)
@@ -293,7 +296,7 @@ func loadAccountData(ctx *context.Context) {
emails[i] = &email
}
ctx.Data["Emails"] = emails
- ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotifications()
+ ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 00614565d2..95b350528c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer"
@@ -49,40 +50,8 @@ func Profile(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
-// HandleUsernameChange handle username changes from user settings and admin interface
-func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName string) error {
- oldName := user.Name
- // rename user
- if err := user_service.RenameUser(ctx, user, newName); err != nil {
- switch {
- // Noop as username is not changed
- case user_model.IsErrUsernameNotChanged(err):
- ctx.Flash.Error(ctx.Tr("form.username_has_not_been_changed"))
- // Non-local users are not allowed to change their username.
- case user_model.IsErrUserIsNotLocal(err):
- ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
- case user_model.IsErrUserAlreadyExist(err):
- ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
- case user_model.IsErrEmailAlreadyUsed(err):
- ctx.Flash.Error(ctx.Tr("form.email_been_used"))
- case db.IsErrNameReserved(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
- case db.IsErrNamePatternNotAllowed(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
- case db.IsErrNameCharsNotAllowed(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
- default:
- ctx.ServerError("ChangeUserName", err)
- }
- return err
- }
- log.Trace("User name changed: %s -> %s", oldName, newName)
- return nil
-}
-
// ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.UpdateProfileForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
@@ -93,29 +62,40 @@ func ProfilePost(ctx *context.Context) {
return
}
- if len(form.Name) != 0 && ctx.Doer.Name != form.Name {
- log.Debug("Changing name for %s to %s", ctx.Doer.Name, form.Name)
- if err := HandleUsernameChange(ctx, ctx.Doer, form.Name); err != nil {
+ form := web.GetForm(ctx).(*forms.UpdateProfileForm)
+
+ if form.Name != "" {
+ if err := user_service.RenameUser(ctx, ctx.Doer, form.Name); err != nil {
+ switch {
+ case user_model.IsErrUserIsNotLocal(err):
+ ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
+ case db.IsErrNameReserved(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_reserved", form.Name))
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", form.Name))
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", form.Name))
+ default:
+ ctx.ServerError("RenameUser", err)
+ return
+ }
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
- ctx.Doer.Name = form.Name
- ctx.Doer.LowerName = strings.ToLower(form.Name)
}
- ctx.Doer.FullName = form.FullName
- ctx.Doer.KeepEmailPrivate = form.KeepEmailPrivate
- ctx.Doer.Website = form.Website
- ctx.Doer.Location = form.Location
- ctx.Doer.Description = form.Description
- ctx.Doer.KeepActivityPrivate = form.KeepActivityPrivate
- ctx.Doer.Visibility = form.Visibility
- if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil {
- if _, ok := err.(user_model.ErrEmailAlreadyUsed); ok {
- ctx.Flash.Error(ctx.Tr("form.email_been_used"))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- return
- }
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
+ Description: optional.Some(form.Description),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ Visibility: optional.Some(form.Visibility),
+ KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
+ }
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@@ -170,7 +150,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
}
if err := user_model.UpdateUserCols(ctx, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
- return fmt.Errorf("UpdateUser: %w", err)
+ return fmt.Errorf("UpdateUserCols: %w", err)
}
return nil
@@ -371,14 +351,15 @@ func UpdateUIThemePost(ctx *context.Context) {
return
}
- if err := user_model.UpdateUserTheme(ctx, ctx.Doer, form.Theme); err != nil {
+ opts := &user_service.UpdateOptions{
+ Theme: optional.Some(form.Theme),
+ }
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
- return
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
}
- log.Trace("Update user theme: %s", ctx.Doer.Name)
- ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
@@ -388,17 +369,19 @@ func UpdateUserLang(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAppearance"] = true
- if len(form.Language) != 0 {
+ if form.Language != "" {
if !util.SliceContainsString(setting.Langs, form.Language) {
ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
}
- ctx.Doer.Language = form.Language
}
- if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil {
- ctx.ServerError("UpdateUserSetting", err)
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(form.Language),
+ }
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 713463a3d4..6746dc2a54 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -14,9 +14,11 @@ import (
"code.gitea.io/gitea/modules/auth/webauthn"
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
+ user_service "code.gitea.io/gitea/services/user"
)
// Init should be called exactly once when the application starts to allow plugins
@@ -85,8 +87,10 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
// If the user does not have a locale set, we save the current one.
if len(user.Language) == 0 {
lc := middleware.Locale(resp, req)
- user.Language = lc.Language()
- if err := user_model.UpdateUserCols(req.Context(), user, "language"); err != nil {
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(lc.Language()),
+ }
+ if err := user_service.UpdateUser(req.Context(), user, opts); err != nil {
log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language))
return
}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index a7ea61b81c..8f641ed541 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
@@ -49,20 +50,17 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
}
if user != nil && !user.ProhibitLogin {
- cols := make([]string, 0)
+ opts := &user_service.UpdateOptions{}
if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
// Change existing admin flag only if AdminFilter option is set
- user.IsAdmin = sr.IsAdmin
- cols = append(cols, "is_admin")
+ opts.IsAdmin = optional.Some(sr.IsAdmin)
}
- if !user.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
+ if !sr.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
// Change existing restricted flag only if RestrictedFilter option is set
- user.IsRestricted = sr.IsRestricted
- cols = append(cols, "is_restricted")
+ opts.IsRestricted = optional.Some(sr.IsRestricted)
}
- if len(cols) > 0 {
- err = user_model.UpdateUserCols(ctx, user, cols...)
- if err != nil {
+ if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
+ if err := user_service.UpdateUser(ctx, user, opts); err != nil {
return nil, err
}
}
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 5c65ca8dc2..eee7bb585a 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -15,6 +15,7 @@ import (
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
@@ -158,23 +159,25 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name)
- usr.FullName = fullName
- emailChanged := usr.Email != su.Mail
- usr.Email = su.Mail
- // Change existing admin flag only if AdminFilter option is set
- if len(source.AdminFilter) > 0 {
- usr.IsAdmin = su.IsAdmin
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(fullName),
+ IsActive: optional.Some(true),
+ }
+ if source.AdminFilter != "" {
+ opts.IsAdmin = optional.Some(su.IsAdmin)
}
// Change existing restricted flag only if RestrictedFilter option is set
- if !usr.IsAdmin && len(source.RestrictedFilter) > 0 {
- usr.IsRestricted = su.IsRestricted
+ if !su.IsAdmin && source.RestrictedFilter != "" {
+ opts.IsRestricted = optional.Some(su.IsRestricted)
}
- usr.IsActive = true
- err = user_model.UpdateUser(ctx, usr, emailChanged, "full_name", "email", "is_admin", "is_restricted", "is_active")
- if err != nil {
+ if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err)
}
+
+ if err := user_service.ReplacePrimaryEmailAddress(ctx, usr, su.Mail); err != nil {
+ log.Error("SyncExternalUsers[%s]: Error updating user %s primary email %s: %v", source.authSource.Name, usr.Name, su.Mail, err)
+ }
}
if usr.IsUploadAvatarChanged(su.Avatar) {
@@ -215,9 +218,10 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
- usr.IsActive = false
- err = user_model.UpdateUserCols(ctx, usr, "is_active")
- if err != nil {
+ opts := &user_service.UpdateOptions{
+ IsActive: optional.Some(false),
+ }
+ if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
}
}
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 16c30088cd..ca27336f92 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -108,7 +108,7 @@ func SendResetPasswordMail(u *user_model.User) {
}
// SendActivateEmailMail sends confirmation email to confirm new email address
-func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
+func SendActivateEmailMail(u *user_model.User, email string) {
if setting.MailService == nil {
// No mail service configured
return
@@ -118,8 +118,8 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
"locale": locale,
"DisplayName": u.DisplayName(),
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
- "Code": u.GenerateEmailActivateCode(email.Email),
- "Email": email.Email,
+ "Code": u.GenerateEmailActivateCode(email),
+ "Email": email,
"Language": locale.Language(),
}
@@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
return
}
- msg := NewMessage(email.Email, locale.Tr("mail.activate_email"), content.String())
+ msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String())
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
SendAsync(msg)
diff --git a/services/mailer/notify.go b/services/mailer/notify.go
index cc4e6baf0b..e48b5d399d 100644
--- a/services/mailer/notify.go
+++ b/services/mailer/notify.go
@@ -114,7 +114,7 @@ func (m *mailNotifier) PullRequestCodeComment(ctx context.Context, pr *issues_mo
func (m *mailNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
// mail only sent to added assignees and not self-assignee
- if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() != user_model.EmailNotificationsDisabled {
+ if !removed && doer.ID != assignee.ID && assignee.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
ct := fmt.Sprintf("Assigned #%d.", issue.Index)
if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{assignee}); err != nil {
log.Error("Error in SendIssueAssignedMail for issue[%d] to assignee[%d]: %v", issue.ID, assignee.ID, err)
@@ -123,7 +123,7 @@ func (m *mailNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model
}
func (m *mailNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
- if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() != user_model.EmailNotificationsDisabled {
+ if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL())
if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil {
log.Error("Error in SendIssueAssignedMail for issue[%d] to reviewer[%d]: %v", issue.ID, reviewer.ID, err)
diff --git a/services/user/avatar.go b/services/user/avatar.go
index 4130d07c38..2d6c3faf9a 100644
--- a/services/user/avatar.go
+++ b/services/user/avatar.go
@@ -57,7 +57,7 @@ func DeleteAvatar(ctx context.Context, u *user_model.User) error {
u.UseCustomAvatar = false
u.Avatar = ""
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil {
- return fmt.Errorf("UpdateUser: %w", err)
+ return fmt.Errorf("DeleteAvatar: %w", err)
}
return nil
}
diff --git a/services/user/email.go b/services/user/email.go
new file mode 100644
index 0000000000..07e19bc688
--- /dev/null
+++ b/services/user/email.go
@@ -0,0 +1,166 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+ "errors"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+)
+
+func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
+ if strings.EqualFold(u.Email, emailStr) {
+ return nil
+ }
+
+ if err := user_model.ValidateEmail(emailStr); err != nil {
+ return err
+ }
+
+ // Check if address exists already
+ email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ return err
+ }
+ if email != nil && email.UID != u.ID {
+ return user_model.ErrEmailAlreadyUsed{Email: emailStr}
+ }
+
+ // Update old primary address
+ primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
+ if err != nil {
+ return err
+ }
+
+ primary.IsPrimary = false
+ if err := user_model.UpdateEmailAddress(ctx, primary); err != nil {
+ return err
+ }
+
+ // Insert new or update existing address
+ if email != nil {
+ email.IsPrimary = true
+ email.IsActivated = true
+ if err := user_model.UpdateEmailAddress(ctx, email); err != nil {
+ return err
+ }
+ } else {
+ email = &user_model.EmailAddress{
+ UID: u.ID,
+ Email: emailStr,
+ IsActivated: true,
+ IsPrimary: true,
+ }
+ if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
+ return err
+ }
+ }
+
+ u.Email = emailStr
+
+ return user_model.UpdateUserCols(ctx, u, "email")
+}
+
+func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
+ if strings.EqualFold(u.Email, emailStr) {
+ return nil
+ }
+
+ if err := user_model.ValidateEmail(emailStr); err != nil {
+ return err
+ }
+
+ if !u.IsOrganization() {
+ // Check if address exists already
+ email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ return err
+ }
+ if email != nil {
+ if email.IsPrimary && email.UID == u.ID {
+ return nil
+ }
+ return user_model.ErrEmailAlreadyUsed{Email: emailStr}
+ }
+
+ // Remove old primary address
+ primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
+ if err != nil {
+ return err
+ }
+ if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil {
+ return err
+ }
+
+ // Insert new primary address
+ email = &user_model.EmailAddress{
+ UID: u.ID,
+ Email: emailStr,
+ IsActivated: true,
+ IsPrimary: true,
+ }
+ if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
+ return err
+ }
+ }
+
+ u.Email = emailStr
+
+ return user_model.UpdateUserCols(ctx, u, "email")
+}
+
+func AddEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error {
+ for _, emailStr := range emails {
+ if err := user_model.ValidateEmail(emailStr); err != nil {
+ return err
+ }
+
+ // Check if address exists already
+ email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ return err
+ }
+ if email != nil {
+ return user_model.ErrEmailAlreadyUsed{Email: emailStr}
+ }
+
+ // Insert new address
+ email = &user_model.EmailAddress{
+ UID: u.ID,
+ Email: emailStr,
+ IsActivated: !setting.Service.RegisterEmailConfirm,
+ IsPrimary: false,
+ }
+ if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func DeleteEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error {
+ for _, emailStr := range emails {
+ // Check if address exists
+ email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
+ if err != nil {
+ return err
+ }
+ if email.IsPrimary {
+ return user_model.ErrPrimaryEmailCannotDelete{Email: emailStr}
+ }
+
+ // Remove address
+ if _, err := db.DeleteByID[user_model.EmailAddress](ctx, email.ID); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/services/user/email_test.go b/services/user/email_test.go
new file mode 100644
index 0000000000..8f419b69f9
--- /dev/null
+++ b/services/user/email_test.go
@@ -0,0 +1,129 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ organization_model "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27})
+
+ emails, err := user_model.GetEmailAddresses(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Len(t, emails, 1)
+
+ primary, err := user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.NotEqual(t, "new-primary@example.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
+
+ primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, "new-primary@example.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Len(t, emails, 2)
+
+ assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
+
+ primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, "user27@example.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Len(t, emails, 2)
+}
+
+func TestReplacePrimaryEmailAddress(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ t.Run("User", func(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
+
+ emails, err := user_model.GetEmailAddresses(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Len(t, emails, 1)
+
+ primary, err := user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.NotEqual(t, "primary-13@example.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ assert.NoError(t, ReplacePrimaryEmailAddress(db.DefaultContext, user, "primary-13@example.com"))
+
+ primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, "primary-13@example.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Len(t, emails, 1)
+
+ assert.NoError(t, ReplacePrimaryEmailAddress(db.DefaultContext, user, "primary-13@example.com"))
+ })
+
+ t.Run("Organization", func(t *testing.T) {
+ org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3})
+
+ assert.Equal(t, "org3@example.com", org.Email)
+
+ assert.NoError(t, ReplacePrimaryEmailAddress(db.DefaultContext, org.AsUser(), "primary-org@example.com"))
+
+ assert.Equal(t, "primary-org@example.com", org.Email)
+ })
+}
+
+func TestAddEmailAddresses(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ assert.Error(t, AddEmailAddresses(db.DefaultContext, user, []string{" invalid email "}))
+
+ emails := []string{"user1234@example.com", "user5678@example.com"}
+
+ assert.NoError(t, AddEmailAddresses(db.DefaultContext, user, emails))
+
+ err := AddEmailAddresses(db.DefaultContext, user, emails)
+ assert.Error(t, err)
+ assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
+}
+
+func TestDeleteEmailAddresses(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ emails := []string{"user2-2@example.com"}
+
+ err := DeleteEmailAddresses(db.DefaultContext, user, emails)
+ assert.NoError(t, err)
+
+ err = DeleteEmailAddresses(db.DefaultContext, user, emails)
+ assert.Error(t, err)
+ assert.True(t, user_model.IsErrEmailAddressNotExist(err))
+
+ emails = []string{"user2@example.com"}
+
+ err = DeleteEmailAddresses(db.DefaultContext, user, emails)
+ assert.Error(t, err)
+ assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err))
+}
diff --git a/services/user/update.go b/services/user/update.go
new file mode 100644
index 0000000000..849757c8b0
--- /dev/null
+++ b/services/user/update.go
@@ -0,0 +1,212 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
+ password_module "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+)
+
+type UpdateOptions struct {
+ KeepEmailPrivate optional.Option[bool]
+ FullName optional.Option[string]
+ Website optional.Option[string]
+ Location optional.Option[string]
+ Description optional.Option[string]
+ AllowGitHook optional.Option[bool]
+ AllowImportLocal optional.Option[bool]
+ MaxRepoCreation optional.Option[int]
+ IsRestricted optional.Option[bool]
+ Visibility optional.Option[structs.VisibleType]
+ KeepActivityPrivate optional.Option[bool]
+ Language optional.Option[string]
+ Theme optional.Option[string]
+ DiffViewStyle optional.Option[string]
+ AllowCreateOrganization optional.Option[bool]
+ IsActive optional.Option[bool]
+ IsAdmin optional.Option[bool]
+ EmailNotificationsPreference optional.Option[string]
+ SetLastLogin bool
+ RepoAdminChangeTeamAccess optional.Option[bool]
+}
+
+func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error {
+ cols := make([]string, 0, 20)
+
+ if opts.KeepEmailPrivate.Has() {
+ u.KeepEmailPrivate = opts.KeepEmailPrivate.Value()
+
+ cols = append(cols, "keep_email_private")
+ }
+
+ if opts.FullName.Has() {
+ u.FullName = opts.FullName.Value()
+
+ cols = append(cols, "full_name")
+ }
+ if opts.Website.Has() {
+ u.Website = opts.Website.Value()
+
+ cols = append(cols, "website")
+ }
+ if opts.Location.Has() {
+ u.Location = opts.Location.Value()
+
+ cols = append(cols, "location")
+ }
+ if opts.Description.Has() {
+ u.Description = opts.Description.Value()
+
+ cols = append(cols, "description")
+ }
+ if opts.Language.Has() {
+ u.Language = opts.Language.Value()
+
+ cols = append(cols, "language")
+ }
+ if opts.Theme.Has() {
+ u.Theme = opts.Theme.Value()
+
+ cols = append(cols, "theme")
+ }
+ if opts.DiffViewStyle.Has() {
+ u.DiffViewStyle = opts.DiffViewStyle.Value()
+
+ cols = append(cols, "diff_view_style")
+ }
+
+ if opts.AllowGitHook.Has() {
+ u.AllowGitHook = opts.AllowGitHook.Value()
+
+ cols = append(cols, "allow_git_hook")
+ }
+ if opts.AllowImportLocal.Has() {
+ u.AllowImportLocal = opts.AllowImportLocal.Value()
+
+ cols = append(cols, "allow_import_local")
+ }
+
+ if opts.MaxRepoCreation.Has() {
+ u.MaxRepoCreation = opts.MaxRepoCreation.Value()
+
+ cols = append(cols, "max_repo_creation")
+ }
+
+ if opts.IsActive.Has() {
+ u.IsActive = opts.IsActive.Value()
+
+ cols = append(cols, "is_active")
+ }
+ if opts.IsRestricted.Has() {
+ u.IsRestricted = opts.IsRestricted.Value()
+
+ cols = append(cols, "is_restricted")
+ }
+ if opts.IsAdmin.Has() {
+ if !opts.IsAdmin.Value() && user_model.IsLastAdminUser(ctx, u) {
+ return models.ErrDeleteLastAdminUser{UID: u.ID}
+ }
+
+ u.IsAdmin = opts.IsAdmin.Value()
+
+ cols = append(cols, "is_admin")
+ }
+
+ if opts.Visibility.Has() {
+ if !u.IsOrganization() && !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) {
+ return fmt.Errorf("visibility mode not allowed: %s", opts.Visibility.Value().String())
+ }
+ u.Visibility = opts.Visibility.Value()
+
+ cols = append(cols, "visibility")
+ }
+ if opts.KeepActivityPrivate.Has() {
+ u.KeepActivityPrivate = opts.KeepActivityPrivate.Value()
+
+ cols = append(cols, "keep_activity_private")
+ }
+
+ if opts.AllowCreateOrganization.Has() {
+ u.AllowCreateOrganization = opts.AllowCreateOrganization.Value()
+
+ cols = append(cols, "allow_create_organization")
+ }
+ if opts.RepoAdminChangeTeamAccess.Has() {
+ u.RepoAdminChangeTeamAccess = opts.RepoAdminChangeTeamAccess.Value()
+
+ cols = append(cols, "repo_admin_change_team_access")
+ }
+
+ if opts.EmailNotificationsPreference.Has() {
+ u.EmailNotificationsPreference = opts.EmailNotificationsPreference.Value()
+
+ cols = append(cols, "email_notifications_preference")
+ }
+
+ if opts.SetLastLogin {
+ u.SetLastLogin()
+
+ cols = append(cols, "last_login_unix")
+ }
+
+ return user_model.UpdateUserCols(ctx, u, cols...)
+}
+
+type UpdateAuthOptions struct {
+ LoginSource optional.Option[int64]
+ LoginName optional.Option[string]
+ Password optional.Option[string]
+ MustChangePassword optional.Option[bool]
+ ProhibitLogin optional.Option[bool]
+}
+
+func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions) error {
+ if opts.LoginSource.Has() {
+ source, err := auth_model.GetSourceByID(ctx, opts.LoginSource.Value())
+ if err != nil {
+ return err
+ }
+
+ u.LoginType = source.Type
+ u.LoginSource = source.ID
+ }
+ if opts.LoginName.Has() {
+ u.LoginName = opts.LoginName.Value()
+ }
+
+ if opts.Password.Has() && (u.IsLocal() || u.IsOAuth2()) {
+ password := opts.Password.Value()
+
+ if len(password) < setting.MinPasswordLength {
+ return password_module.ErrMinLength
+ }
+ if !password_module.IsComplexEnough(password) {
+ return password_module.ErrComplexity
+ }
+ if err := password_module.IsPwned(ctx, password); err != nil {
+ return err
+ }
+
+ if err := u.SetPassword(password); err != nil {
+ return err
+ }
+ }
+
+ if opts.MustChangePassword.Has() {
+ u.MustChangePassword = opts.MustChangePassword.Value()
+ }
+ if opts.ProhibitLogin.Has() {
+ 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")
+}
diff --git a/services/user/update_test.go b/services/user/update_test.go
new file mode 100644
index 0000000000..7ed764b539
--- /dev/null
+++ b/services/user/update_test.go
@@ -0,0 +1,120 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ password_module "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUpdateUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ assert.Error(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{
+ IsAdmin: optional.Some(false),
+ }))
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+
+ opts := &UpdateOptions{
+ KeepEmailPrivate: optional.Some(false),
+ FullName: optional.Some("Changed Name"),
+ Website: optional.Some("https://gitea.com/"),
+ Location: optional.Some("location"),
+ Description: optional.Some("description"),
+ AllowGitHook: optional.Some(true),
+ AllowImportLocal: optional.Some(true),
+ MaxRepoCreation: optional.Some[int](10),
+ IsRestricted: optional.Some(true),
+ IsActive: optional.Some(false),
+ IsAdmin: optional.Some(true),
+ Visibility: optional.Some(structs.VisibleTypePrivate),
+ KeepActivityPrivate: optional.Some(true),
+ Language: optional.Some("lang"),
+ Theme: optional.Some("theme"),
+ DiffViewStyle: optional.Some("split"),
+ AllowCreateOrganization: optional.Some(false),
+ EmailNotificationsPreference: optional.Some("disabled"),
+ SetLastLogin: true,
+ }
+ assert.NoError(t, UpdateUser(db.DefaultContext, user, opts))
+
+ assert.Equal(t, opts.KeepEmailPrivate.Value(), user.KeepEmailPrivate)
+ assert.Equal(t, opts.FullName.Value(), user.FullName)
+ assert.Equal(t, opts.Website.Value(), user.Website)
+ assert.Equal(t, opts.Location.Value(), user.Location)
+ assert.Equal(t, opts.Description.Value(), user.Description)
+ assert.Equal(t, opts.AllowGitHook.Value(), user.AllowGitHook)
+ assert.Equal(t, opts.AllowImportLocal.Value(), user.AllowImportLocal)
+ assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
+ assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
+ assert.Equal(t, opts.IsActive.Value(), user.IsActive)
+ assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
+ assert.Equal(t, opts.Visibility.Value(), user.Visibility)
+ assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
+ assert.Equal(t, opts.Language.Value(), user.Language)
+ assert.Equal(t, opts.Theme.Value(), user.Theme)
+ assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle)
+ assert.Equal(t, opts.AllowCreateOrganization.Value(), user.AllowCreateOrganization)
+ assert.Equal(t, opts.EmailNotificationsPreference.Value(), user.EmailNotificationsPreference)
+
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+ assert.Equal(t, opts.KeepEmailPrivate.Value(), user.KeepEmailPrivate)
+ assert.Equal(t, opts.FullName.Value(), user.FullName)
+ assert.Equal(t, opts.Website.Value(), user.Website)
+ assert.Equal(t, opts.Location.Value(), user.Location)
+ assert.Equal(t, opts.Description.Value(), user.Description)
+ assert.Equal(t, opts.AllowGitHook.Value(), user.AllowGitHook)
+ assert.Equal(t, opts.AllowImportLocal.Value(), user.AllowImportLocal)
+ assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
+ assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
+ assert.Equal(t, opts.IsActive.Value(), user.IsActive)
+ assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
+ assert.Equal(t, opts.Visibility.Value(), user.Visibility)
+ assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
+ assert.Equal(t, opts.Language.Value(), user.Language)
+ assert.Equal(t, opts.Theme.Value(), user.Theme)
+ assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle)
+ assert.Equal(t, opts.AllowCreateOrganization.Value(), user.AllowCreateOrganization)
+ assert.Equal(t, opts.EmailNotificationsPreference.Value(), user.EmailNotificationsPreference)
+}
+
+func TestUpdateAuth(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+ copy := *user
+
+ assert.NoError(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{
+ LoginName: optional.Some("new-login"),
+ }))
+ assert.Equal(t, "new-login", user.LoginName)
+
+ assert.NoError(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{
+ Password: optional.Some("%$DRZUVB576tfzgu"),
+ MustChangePassword: optional.Some(true),
+ }))
+ assert.True(t, user.MustChangePassword)
+ assert.NotEqual(t, copy.Passwd, user.Passwd)
+ assert.NotEqual(t, copy.Salt, user.Salt)
+
+ assert.NoError(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{
+ ProhibitLogin: optional.Some(true),
+ }))
+ assert.True(t, user.ProhibitLogin)
+
+ assert.ErrorIs(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{
+ Password: optional.Some("aaaa"),
+ }), password_module.ErrMinLength)
+}
diff --git a/services/user/user.go b/services/user/user.go
index 8bf083192f..f2648db409 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -41,10 +41,7 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string) err
}
if newUserName == u.Name {
- return user_model.ErrUsernameNotChanged{
- UID: u.ID,
- Name: u.Name,
- }
+ return nil
}
if err := user_model.IsUsableUsername(newUserName); err != nil {
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 73f1491c12..2ebcded925 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -107,7 +107,7 @@ func TestRenameUser(t *testing.T) {
})
t.Run("Same username", func(t *testing.T) {
- assert.ErrorIs(t, RenameUser(db.DefaultContext, user, user.Name), user_model.ErrUsernameNotChanged{UID: user.ID, Name: user.Name})
+ assert.NoError(t, RenameUser(db.DefaultContext, user, user.Name))
})
t.Run("Non usable username", func(t *testing.T) {
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index ff7c2ddca3..0748a75ba4 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -208,11 +208,11 @@ func TestAPIEditUser(t *testing.T) {
SourceID: 0,
Email: &empty,
}).AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusUnprocessableEntity)
+ resp := MakeRequest(t, req, http.StatusBadRequest)
errMap := make(map[string]any)
json.Unmarshal(resp.Body.Bytes(), &errMap)
- assert.EqualValues(t, "email is not allowed to be empty string", errMap["message"].(string))
+ assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.False(t, user2.IsRestricted)
@@ -254,14 +254,14 @@ func TestAPIRenameUser(t *testing.T) {
// required
"new_name": "User2",
}).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusOK)
+ MakeRequest(t, req, http.StatusNoContent)
urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2")
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
// required
"new_name": "User2-2-2",
}).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusOK)
+ MakeRequest(t, req, http.StatusNoContent)
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
// required
@@ -281,7 +281,7 @@ func TestAPIRenameUser(t *testing.T) {
// required
"new_name": "user2",
}).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusOK)
+ MakeRequest(t, req, http.StatusNoContent)
}
func TestAPICron(t *testing.T) {
diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go
index a727aea3ce..876fb5ac13 100644
--- a/tests/integration/api_nodeinfo_test.go
+++ b/tests/integration/api_nodeinfo_test.go
@@ -32,7 +32,7 @@ func TestNodeinfo(t *testing.T) {
DecodeJSON(t, resp, &nodeinfo)
assert.True(t, nodeinfo.OpenRegistrations)
assert.Equal(t, "gitea", nodeinfo.Software.Name)
- assert.Equal(t, 25, nodeinfo.Usage.Users.Total)
+ assert.Equal(t, 26, nodeinfo.Usage.Users.Total)
assert.Equal(t, 20, nodeinfo.Usage.LocalPosts)
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
})
diff --git a/tests/integration/api_user_email_test.go b/tests/integration/api_user_email_test.go
index 6eeb547444..6441e2ed8e 100644
--- a/tests/integration/api_user_email_test.go
+++ b/tests/integration/api_user_email_test.go
@@ -68,6 +68,16 @@ func TestAPIAddEmail(t *testing.T) {
DecodeJSON(t, resp, &emails)
assert.EqualValues(t, []*api.Email{
{
+ Email: "user2@example.com",
+ Verified: true,
+ Primary: true,
+ },
+ {
+ Email: "user2-2@example.com",
+ Verified: false,
+ Primary: false,
+ },
+ {
Email: "user2-3@example.com",
Verified: true,
Primary: false,
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index 8842de5f6f..ea393a6061 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -12,7 +12,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -45,9 +44,6 @@ func TestEmptyRepo(t *testing.T) {
func TestEmptyRepoAddFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- err := user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 30, ProhibitLogin: false}, "prohibit_login")
- assert.NoError(t, err)
-
session := loginUser(t, "user30")
req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -72,9 +68,6 @@ func TestEmptyRepoAddFile(t *testing.T) {
func TestEmptyRepoUploadFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- err := user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 30, ProhibitLogin: false}, "prohibit_login")
- assert.NoError(t, err)
-
session := loginUser(t, "user30")
req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -112,9 +105,6 @@ func TestEmptyRepoUploadFile(t *testing.T) {
func TestEmptyRepoAddFileByAPI(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- err := user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 30, ProhibitLogin: false}, "prohibit_login")
- assert.NoError(t, err)
-
session := loginUser(t, "user30")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)