Unify user update methods (#28733)

Fixes #28660
Fixes an admin api bug related to `user.LoginSource`
Fixed `/user/emails` response not identical to GitHub api

This PR unifies the user update methods. The goal is to keep the logic
only at one place (having audit logs in mind). For example, do the
password checks only in one method not everywhere a password is updated.

After that PR is merged, the user creation should be next.
This commit is contained in:
KN4CK3R 2024-02-04 14:29:09 +01:00 committed by GitHub
parent b4513f48ce
commit f8b471ace1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1383 additions and 1068 deletions

View file

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
user_model "code.gitea.io/gitea/models/user" 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" "code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -50,35 +51,32 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err 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")) { user, err := user_model.GetUserByName(ctx, c.String("username"))
return errors.New("Password does not meet complexity requirements")
}
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
if err != nil { if err != nil {
return err 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)
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") { 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 { opts := &user_service.UpdateAuthOptions{
return err 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) fmt.Printf("%s's password has been successfully updated!\n", user.Name)

View file

@ -285,3 +285,11 @@
lower_email: abcde@gitea.com lower_email: abcde@gitea.com
is_activated: true is_activated: true
is_primary: false is_primary: false
-
id: 37
uid: 37
email: user37@example.com
lower_email: user37@example.com
is_activated: true
is_primary: true

View file

@ -1095,7 +1095,7 @@
allow_git_hook: false allow_git_hook: false
allow_import_local: false allow_import_local: false
allow_create_organization: true allow_create_organization: true
prohibit_login: true prohibit_login: false
avatar: avatar29 avatar: avatar29
avatar_email: user30@example.com avatar_email: user30@example.com
use_custom_avatar: false use_custom_avatar: false
@ -1332,3 +1332,40 @@
repo_admin_change_team_access: false repo_admin_change_team_access: false
theme: "" theme: ""
keep_activity_private: false 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

View file

@ -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])?)*$") 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 // ValidateEmail check if email is a allowed address
func ValidateEmail(email string) error { func ValidateEmail(email string) error {
if len(email) == 0 { if len(email) == 0 {
return nil return ErrEmailInvalid{email}
} }
if !emailRegexp.MatchString(email) { if !emailRegexp.MatchString(email) {
@ -177,6 +189,36 @@ func ValidateEmail(email string) error {
return nil 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. // GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) { func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5) 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{}) 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 // DeleteInactiveEmailAddresses deletes inactive email addresses
func DeleteInactiveEmailAddresses(ctx context.Context) error { func DeleteInactiveEmailAddresses(ctx context.Context) error {
_, err := db.GetEngine(ctx). _, err := db.GetEngine(ctx).

View file

@ -42,96 +42,6 @@ func TestIsEmailUsed(t *testing.T) {
assert.False(t, isExist) 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) { func TestMakeEmailPrimary(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -108,18 +108,3 @@ func IsErrUserIsNotLocal(err error) bool {
_, ok := err.(ErrUserIsNotLocal) _, ok := err.(ErrUserIsNotLocal)
return ok 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
}

View file

@ -196,18 +196,6 @@ func (u *User) SetLastLogin() {
u.LastLoginUnix = timeutil.TimeStampNow() 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 // GetPlaceholderEmail returns an noreply email
func (u *User) GetPlaceholderEmail() string { func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress) 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 // SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
// change passwd, salt and passwd_hash_algo fields // change passwd, salt and passwd_hash_algo fields
func (u *User) SetPassword(passwd string) (err error) { 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 { if u.Salt, err = GetUserSalt(); err != nil {
return err return err
} }
@ -488,21 +469,6 @@ func (u *User) IsMailable() bool {
return u.IsActive 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, // IsUserExist checks if given user name exist,
// the user name should be noncased unique. // the user name should be noncased unique.
// If uid is presented, then check will rule out that one, // 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 { if u.Rands, err = GetUserSalt(); err != nil {
return err return err
} }
if err = u.SetPassword(u.Passwd); err != nil { if u.Passwd != "" {
return err if err = u.SetPassword(u.Passwd); err != nil {
return err
}
} else {
u.Salt = ""
u.PasswdHashAlgo = ""
} }
// save changes to database // save changes to database
@ -817,24 +788,6 @@ func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
return nil 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 // ValidateUser check if user is valid to insert / update into database
func ValidateUser(u *User, cols ...string) error { func ValidateUser(u *User, cols ...string) error {
if len(cols) == 0 || util.SliceContainsString(cols, "visibility", true) { 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 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 // UpdateUserCols update user according special columns
func UpdateUserCols(ctx context.Context, u *User, cols ...string) error { func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
if err := ValidateUser(u, cols...); err != nil { if err := ValidateUser(u, cols...); err != nil {
@ -928,25 +809,6 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
return err 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 // GetInactiveUsers gets all inactive users
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) { func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
var cond builder.Cond = builder.Eq{"is_active": false} var cond builder.Cond = builder.Eq{"is_active": false}
@ -1044,7 +906,7 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string {
if err != nil { if err != nil {
continue continue
} }
if u.IsMailable() && u.EmailNotifications() != EmailNotificationsDisabled { if u.IsMailable() && u.EmailNotificationsPreference != EmailNotificationsDisabled {
mails = append(mails, u.Email) mails = append(mails, u.Email)
} }
} }

View file

@ -101,13 +101,13 @@ func TestSearchUsers(t *testing.T) {
} }
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, 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}, testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
[]int64{9}) []int64{9})
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, 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}, 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}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
@ -123,7 +123,7 @@ func TestSearchUsers(t *testing.T) {
[]int64{29}) []int64{29})
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue}, 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}, testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
[]int64{24}) []int64{24})
@ -147,20 +147,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
{user_model.EmailNotificationsOnMention, 9}, {user_model.EmailNotificationsOnMention, 9},
} { } {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID})
assert.Equal(t, test.expected, user.EmailNotifications()) assert.Equal(t, test.expected, user.EmailNotificationsPreference)
// 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())
} }
} }
@ -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) { func TestNewUserRedirect(t *testing.T) {
// redirect to a completely new name // redirect to a completely new name
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
@ -534,14 +485,12 @@ func Test_ValidateUser(t *testing.T) {
}() }()
setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true} setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true}
kases := map[*user_model.User]bool{ kases := map[*user_model.User]bool{
{ID: 1, Visibility: structs.VisibleTypePublic}: true, {ID: 1, Visibility: structs.VisibleTypePublic}: true,
{ID: 2, Visibility: structs.VisibleTypeLimited}: false, {ID: 2, Visibility: structs.VisibleTypeLimited}: false,
{ID: 2, Visibility: structs.VisibleTypeLimited, Email: "invalid"}: false, {ID: 2, Visibility: structs.VisibleTypePrivate}: true,
{ID: 2, Visibility: structs.VisibleTypePrivate, Email: "valid@valid.com"}: true,
} }
for kase, expected := range kases { for kase, expected := range kases {
err := user_model.ValidateUser(kase) assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), fmt.Sprintf("case: %+v", kase))
assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase))
} }
} }

View file

@ -5,8 +5,9 @@ package password
import ( import (
"bytes" "bytes"
goContext "context" "context"
"crypto/rand" "crypto/rand"
"errors"
"math/big" "math/big"
"strings" "strings"
"sync" "sync"
@ -15,6 +16,11 @@ import (
"code.gitea.io/gitea/modules/translation" "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 // complexity contains information about a particular kind of password complexity
type complexity struct { type complexity struct {
ValidChars string ValidChars string
@ -101,11 +107,14 @@ func Generate(n int) (string, error) {
} }
buffer[j] = validChars[rnd.Int64()] 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 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 return string(buffer), nil
} }
} }

View file

@ -5,24 +5,48 @@ package password
import ( import (
"context" "context"
"errors"
"fmt"
"code.gitea.io/gitea/modules/auth/password/pwn" "code.gitea.io/gitea/modules/auth/password/pwn"
"code.gitea.io/gitea/modules/setting" "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 // 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 // If a password has not been pwned, no error is returned.
// HIBP, so not getting a response should block a password until it can be verified. func IsPwned(ctx context.Context, password string) error {
func IsPwned(ctx context.Context, password string) (bool, error) {
if !setting.PasswordCheckPwn { if !setting.PasswordCheckPwn {
return false, nil return nil
} }
client := pwn.New(pwn.WithContext(ctx)) client := pwn.New(pwn.WithContext(ctx))
count, err := client.CheckPassword(password, true) count, err := client.CheckPassword(password, true)
if err != nil { if err != nil {
return true, err return ErrIsPwnedRequest{err}
} }
return count > 0, nil if count > 0 {
return ErrIsPwned
}
return nil
} }

View file

@ -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 // because artificial responses will be added to the response
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ // For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
func (c *Client) CheckPassword(pw string, padding bool) (int, error) { func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
if strings.TrimSpace(pw) == "" { if pw == "" {
return -1, ErrEmptyPassword return -1, ErrEmptyPassword
} }

View file

@ -4,13 +4,14 @@
package pwn package pwn
import ( import (
"errors"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
var client = New(WithHTTP(&http.Client{ var client = New(WithHTTP(&http.Client{
@ -25,78 +26,44 @@ func TestMain(m *testing.M) {
func TestPassword(t *testing.T) { func TestPassword(t *testing.T) {
// Check input error // Check input error
_, err := client.CheckPassword("", false) _, err := client.CheckPassword("", false)
if err == nil { assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword")
t.Log("blank input should return an error")
t.Fail()
}
if !errors.Is(err, ErrEmptyPassword) {
t.Log("blank input should return ErrEmptyPassword")
t.Fail()
}
// Should fail // Should fail
fail := "password1234" fail := "password1234"
count, err := client.CheckPassword(fail, false) count, err := client.CheckPassword(fail, false)
if err != nil { assert.NotEmpty(t, count, "%s should fail as a password", fail)
t.Log(err) assert.NoError(t, err)
t.Fail()
}
if count == 0 {
t.Logf("%s should fail as a password\n", fail)
t.Fail()
}
// Should fail (with padding) // Should fail (with padding)
failPad := "administrator" failPad := "administrator"
count, err = client.CheckPassword(failPad, true) count, err = client.CheckPassword(failPad, true)
if err != nil { assert.NotEmpty(t, count, "%s should fail as a password", failPad)
t.Log(err) assert.NoError(t, err)
t.Fail()
}
if count == 0 {
t.Logf("%s should fail as a password\n", failPad)
t.Fail()
}
// Checking for a "good" password isn't going to be perfect, but we can give it a good try // 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? // with hopefully minimal error. Try five times?
var good bool assert.Condition(t, func() bool {
var pw string for i := 0; i <= 5; i++ {
for idx := 0; idx <= 5; idx++ { count, err = client.CheckPassword(testPassword(), false)
pw = testPassword() assert.NoError(t, err)
count, err = client.CheckPassword(pw, false) if count == 0 {
if err != nil { return true
t.Log(err) }
t.Fail()
} }
if count == 0 { return false
good = true }, "no generated passwords passed. there is a chance this is a fluke")
break
}
}
if !good {
t.Log("no generated passwords passed. there is a chance this is a fluke")
t.Fail()
}
// Again, but with padded responses // Again, but with padded responses
good = false assert.Condition(t, func() bool {
for idx := 0; idx <= 5; idx++ { for i := 0; i <= 5; i++ {
pw = testPassword() count, err = client.CheckPassword(testPassword(), true)
count, err = client.CheckPassword(pw, true) assert.NoError(t, err)
if err != nil { if count == 0 {
t.Log(err) return true
t.Fail() }
} }
if count == 0 { return false
good = true }, "no generated passwords passed. there is a chance this is a fluke")
break
}
}
if !good {
t.Log("no generated passwords passed. there is a chance this is a fluke")
t.Fail()
}
} }
// Credit to https://golangbyexample.com/generate-random-password-golang/ // Credit to https://golangbyexample.com/generate-random-password-golang/

View file

@ -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
}

View file

@ -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())
}

View file

@ -8,7 +8,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey" 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/auth/password"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -107,9 +107,8 @@ func CreateUser(ctx *context.APIContext) {
return return
} }
pwned, err := password.IsPwned(ctx, form.Password) if err := password.IsPwned(ctx, form.Password); err != nil {
if pwned { if password.IsErrIsPwnedRequest(err) {
if err != nil {
log.Error(err.Error()) log.Error(err.Error())
} }
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
@ -192,115 +191,65 @@ func EditUser(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditUserOption) form := web.GetForm(ctx).(*api.EditUserOption)
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName) authOpts := &user_service.UpdateAuthOptions{
if ctx.Written() { 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 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))
case errors.Is(err, password.ErrComplexity):
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
default:
ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
}
return return
} }
if len(form.Password) != 0 {
if len(form.Password) < setting.MinPasswordLength {
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")
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
}
}
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 { if form.Email != nil {
email := strings.TrimSpace(*form.Email) if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
if len(email) == 0 { switch {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string")) 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 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"))
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 { opts := &user_service.UpdateOptions{
if user_model.IsErrEmailAlreadyUsed(err) || FullName: optional.FromPtr(form.FullName),
user_model.IsErrEmailCharIsNotSupported(err) || Website: optional.FromPtr(form.Website),
user_model.IsErrEmailInvalid(err) { Location: optional.FromPtr(form.Location),
ctx.Error(http.StatusUnprocessableEntity, "", err) 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 { } else {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err) ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
} }
return return
} }
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name) 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)) 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 // Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil { if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch { switch {
case user_model.IsErrUsernameNotChanged(err):
// Noop as username is not changed
ctx.Status(http.StatusNoContent)
case user_model.IsErrUserAlreadyExist(err): case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken")) ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err): case db.IsErrNameReserved(err):
@ -545,5 +491,5 @@ func RenameUser(ctx *context.APIContext) {
} }
log.Trace("User name changed: %s -> %s", oldName, newName) log.Trace("User name changed: %s -> %s", oldName, newName)
ctx.Status(http.StatusOK) ctx.Status(http.StatusNoContent)
} }

View file

@ -13,12 +13,14 @@ import (
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org" "code.gitea.io/gitea/services/org"
user_service "code.gitea.io/gitea/services/user"
) )
func listUserOrgs(ctx *context.APIContext, u *user_model.User) { func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@ -337,28 +339,30 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization" // "$ref": "#/responses/Organization"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditOrgOption) form := web.GetForm(ctx).(*api.EditOrgOption)
org := ctx.Org.Organization
org.FullName = form.FullName if form.Email != "" {
org.Email = form.Email if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
org.Description = form.Description ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
org.Website = form.Website return
org.Location = form.Location }
if form.Visibility != "" {
org.Visibility = api.VisibilityModes[form.Visibility]
} }
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(), if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
"full_name", "description", "website", "location", ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
"visibility", "repo_admin_change_team_access",
); err != nil {
ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
return return
} }
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org)) ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
} }
// Delete an organization // Delete an organization

View file

@ -9,10 +9,10 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
) )
// ListEmails list all of the authenticated user's email addresses // ListEmails list all of the authenticated user's email addresses
@ -56,22 +56,14 @@ func AddEmail(ctx *context.APIContext) {
// "$ref": "#/responses/EmailList" // "$ref": "#/responses/EmailList"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateEmailOption) form := web.GetForm(ctx).(*api.CreateEmailOption)
if len(form.Emails) == 0 { if len(form.Emails) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty") ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty")
return return
} }
emails := make([]*user_model.EmailAddress, len(form.Emails)) if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
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 user_model.IsErrEmailAlreadyUsed(err) { if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
@ -91,11 +83,17 @@ func AddEmail(ctx *context.APIContext) {
return return
} }
apiEmails := make([]*api.Email, len(emails)) emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID)
for i := range emails { if err != nil {
apiEmails[i] = convert.ToEmail(emails[i]) 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 // DeleteEmail delete email
@ -115,26 +113,19 @@ func DeleteEmail(ctx *context.APIContext) {
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.DeleteEmailOption) form := web.GetForm(ctx).(*api.DeleteEmailOption)
if len(form.Emails) == 0 { if len(form.Emails) == 0 {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
return return
} }
emails := make([]*user_model.EmailAddress, len(form.Emails)) if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
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 user_model.IsErrEmailAddressNotExist(err) { if user_model.IsErrEmailAddressNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err) ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err)
return } else {
ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
} }
ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
return return
} }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)

View file

@ -6,11 +6,12 @@ package user
import ( import (
"net/http" "net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
) )
// GetUserSettings returns user settings // GetUserSettings returns user settings
@ -44,36 +45,18 @@ func UpdateUserSettings(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.UserSettingsOptions) form := web.GetForm(ctx).(*api.UserSettingsOptions)
if form.FullName != nil { opts := &user_service.UpdateOptions{
ctx.Doer.FullName = *form.FullName 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 { if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != 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 {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return
} }

View file

@ -5,6 +5,7 @@
package admin package admin
import ( import (
"errors"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -20,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -162,11 +164,10 @@ func NewUserPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
return return
} }
pwned, err := password.IsPwned(ctx, form.Password) if err := password.IsPwned(ctx, form.Password); err != nil {
if pwned {
ctx.Data["Err_Password"] = true ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned") errMsg := ctx.Tr("auth.password_pwned")
if err != nil { if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error()) log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err") errMsg = ctx.Tr("auth.password_pwned_err")
} }
@ -184,10 +185,7 @@ func NewUserPost(ctx *context.Context) {
case user_model.IsErrEmailAlreadyUsed(err): case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form) ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
case user_model.IsErrEmailCharIsNotSupported(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 user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case db.IsErrNameReserved(err): case db.IsErrNameReserved(err):
@ -348,68 +346,111 @@ func EditUserPost(ctx *context.Context) {
return 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, "-") fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 { if len(fields) == 2 {
loginType, _ := strconv.ParseInt(fields[0], 10, 0)
authSource, _ := strconv.ParseInt(fields[1], 10, 64) authSource, _ := strconv.ParseInt(fields[1], 10, 64)
if u.LoginSource != authSource { authOpts.LoginSource = optional.Some(authSource)
u.LoginSource = authSource
u.LoginType = auth.Type(loginType)
}
} }
if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) { if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
var err error switch {
if len(form.Password) < setting.MinPasswordLength { case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form) ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
return case errors.Is(err, password.ErrComplexity):
}
if !password.IsComplexEnough(form.Password) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
ctx.Data["Err_Password"] = true ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned") ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
if err != nil { case errors.Is(err, password.ErrIsPwned):
log.Error(err.Error()) ctx.Data["Err_Password"] = true
errMsg = ctx.Tr("auth.password_pwned_err") ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
} case password.IsErrIsPwnedRequest(err):
ctx.RenderWithErr(errMsg, tplUserEdit, &form) log.Error("%s", err.Error())
return ctx.Data["Err_Password"] = true
} ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
default:
if err := user_model.ValidateEmail(form.Email); err != nil {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_error"), tplUserEdit, &form)
return
}
if u.Salt, err = user_model.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return
} }
if err = u.SetPassword(form.Password); err != nil { return
ctx.ServerError("SetPassword", err) }
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 return
} }
} }
if len(form.UserName) != 0 && u.Name != form.UserName { opts := &user_service.UpdateOptions{
if err := user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil { FullName: optional.Some(form.FullName),
if ctx.Written() { Website: optional.Some(form.Website),
return Location: optional.Some(form.Location),
} IsActive: optional.Some(form.Active),
ctx.RenderWithErr(ctx.Flash.ErrorMsg, tplUserEdit, &form) IsAdmin: optional.Some(form.Admin),
return AllowGitHook: optional.Some(form.AllowGitHook),
} AllowImportLocal: optional.Some(form.AllowImportLocal),
u.Name = form.UserName MaxRepoCreation: optional.Some(form.MaxRepoCreation),
u.LowerName = strings.ToLower(form.UserName) AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
IsRestricted: optional.Some(form.Restricted),
Visibility: optional.Some(form.Visibility),
} }
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)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
if form.Reset2FA { if form.Reset2FA {
tf, err := auth.GetTwoFactorByUID(ctx, u.ID) tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
@ -433,53 +474,8 @@ func EditUserPost(ctx *context.Context) {
return 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.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
} }

View file

@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -30,6 +31,7 @@ import (
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
"github.com/markbates/goth" "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 { func resetLocale(ctx *context.Context, u *user_model.User) error {
// Language setting of the user overwrites the one previously set // 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 the user does not have a locale set, we save the current one.
if len(u.Language) == 0 { if u.Language == "" {
u.Language = ctx.Locale.Language() opts := &user_service.UpdateOptions{
if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil { Language: optional.Some(ctx.Locale.Language()),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
return err 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 // 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 the user does not have a locale set, we save the current one.
if len(u.Language) == 0 { if u.Language == "" {
u.Language = ctx.Locale.Language() opts := &user_service.UpdateOptions{
if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil { Language: optional.Some(ctx.Locale.Language()),
ctx.ServerError("UpdateUserCols Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, u.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 + "/" return setting.AppSubURL + "/"
} }
} }
@ -348,9 +354,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Csrf.DeleteCookie(ctx) ctx.Csrf.DeleteCookie(ctx)
// Register last login // Register last login
u.SetLastLogin() if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
if err := user_model.UpdateUserCols(ctx, u, "last_login_unix"); err != nil { ctx.ServerError("UpdateUser", err)
ctx.ServerError("UpdateUserCols", err)
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
} }
@ -482,10 +487,9 @@ func SignUpPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
return return
} }
pwned, err := password.IsPwned(ctx, form.Password) if err := password.IsPwned(ctx, form.Password); err != nil {
if pwned {
errMsg := ctx.Tr("auth.password_pwned") errMsg := ctx.Tr("auth.password_pwned")
if err != nil { if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error()) log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err") 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) { func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user. // Auto-set admin for the only user.
if user_model.CountUsers(ctx, nil) == 1 { if user_model.CountUsers(ctx, nil) == 1 {
u.IsAdmin = true opts := &user_service.UpdateOptions{
u.IsActive = true IsActive: optional.Some(true),
u.SetLastLogin() IsAdmin: optional.Some(true),
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_active", "last_login_unix"); err != nil { SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return false return false
} }
@ -752,10 +758,8 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return return
} }
// Register last login if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
user.SetLastLogin() ctx.ServerError("UpdateUser", err)
if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err)
return return
} }

View file

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -990,7 +991,9 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source) 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) { if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled // error already handled
@ -1054,19 +1057,17 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims) 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) groups := getClaimedGroups(source, gothUser)
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
if source.AdminGroup != "" { if source.AdminGroup != "" {
u.IsAdmin = groups.Contains(source.AdminGroup) isAdmin = optional.Some(groups.Contains(source.AdminGroup))
} }
if source.RestrictedGroup != "" { 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) { 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 // Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx) ctx.Csrf.DeleteCookie(ctx)
// Register last login opts := &user_service.UpdateOptions{
u.SetLastLogin() SetLastLogin: true,
// Update GroupClaims
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
cols := []string{"last_login_unix"}
if changed {
cols = append(cols, "is_admin", "is_restricted")
} }
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil { if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUserCols", err) ctx.ServerError("UpdateUser", err)
return return
} }
@ -1177,10 +1172,11 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser) opts := &user_service.UpdateOptions{}
if changed { opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil { if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
ctx.ServerError("UpdateUserCols", err) if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return return
} }
} }

View file

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -21,6 +22,7 @@ import (
"code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
) )
var ( var (
@ -165,30 +167,6 @@ func ResetPasswdPost(ctx *context.Context) {
return 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 // Handle two-factor
regenerateScratchToken := false regenerateScratchToken := false
if twofa != nil { if twofa != nil {
@ -221,18 +199,27 @@ func ResetPasswdPost(ctx *context.Context) {
} }
} }
} }
var err error
if u.Rands, err = user_model.GetUserSalt(); err != nil { opts := &user_service.UpdateAuthOptions{
ctx.ServerError("UpdateUser", err) Password: optional.Some(ctx.FormString("password")),
return MustChangePassword: optional.Some(false),
} }
if err = u.SetPassword(passwd); err != nil { if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err) ctx.Data["IsResetForm"] = true
return ctx.Data["Err_Password"] = true
} switch {
u.MustChangePassword = false case errors.Is(err, password.ErrMinLength):
if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil { ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
ctx.ServerError("UpdateUser", err) 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 return
} }
@ -242,7 +229,7 @@ func ResetPasswdPost(ctx *context.Context) {
if regenerateScratchToken { if regenerateScratchToken {
// Invalidate the scratch token. // Invalidate the scratch token.
_, err = twofa.GenerateScratchToken() _, err := twofa.GenerateScratchToken()
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
@ -282,11 +269,11 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMustChangePassword) ctx.HTML(http.StatusOK, tplMustChangePassword)
return return
} }
u := ctx.Doer
// Make sure only requests for users who are eligible to change their password via // Make sure only requests for users who are eligible to change their password via
// this method passes through // this method passes through
if !u.MustChangePassword { if !ctx.Doer.MustChangePassword {
ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page")) ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
return return
} }
@ -296,44 +283,34 @@ func MustChangePasswordPost(ctx *context.Context) {
return return
} }
if len(form.Password) < setting.MinPasswordLength { opts := &user_service.UpdateAuthOptions{
ctx.Data["Err_Password"] = true Password: optional.Some(form.Password),
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) MustChangePassword: optional.Some(false),
return
} }
if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
if !password.IsComplexEnough(form.Password) { switch {
ctx.Data["Err_Password"] = true case errors.Is(err, password.ErrMinLength):
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) ctx.Data["Err_Password"] = true
return ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
} case errors.Is(err, password.ErrComplexity):
pwned, err := password.IsPwned(ctx, form.Password) ctx.Data["Err_Password"] = true
if pwned { ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
ctx.Data["Err_Password"] = true case errors.Is(err, password.ErrIsPwned):
errMsg := ctx.Tr("auth.password_pwned") ctx.Data["Err_Password"] = true
if err != nil { ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
log.Error(err.Error()) case password.IsErrIsPwnedRequest(err):
errMsg = ctx.Tr("auth.password_pwned_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 return
} }
ctx.Flash.Success(ctx.Tr("settings.change_password_success")) 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) { if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)

View file

@ -7,7 +7,6 @@ package org
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -71,53 +71,50 @@ func SettingsPost(ctx *context.Context) {
} }
org := ctx.Org.Organization org := ctx.Org.Organization
nameChanged := org.Name != form.Name
// Check if organization name has been changed. if org.Name != form.Name {
if nameChanged { if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
err := user_service.RenameUser(ctx, org.AsUser(), form.Name) if user_model.IsErrUserAlreadyExist(err) {
switch { ctx.Data["Err_Name"] = true
case user_model.IsErrUserAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
ctx.Data["OrgName"] = true } else if db.IsErrNameReserved(err) {
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) ctx.Data["Err_Name"] = true
return ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
case db.IsErrNameReserved(err): } else if db.IsErrNamePatternNotAllowed(err) {
ctx.Data["OrgName"] = true ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
return } else {
case db.IsErrNamePatternNotAllowed(err): ctx.ServerError("RenameUser", 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)
return return
} }
// reset ctx.org.OrgLink with new name ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
} }
// In case it's just a case change. if form.Email != "" {
org.Name = form.Name if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
org.LowerName = strings.ToLower(form.Name) 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 { if ctx.Doer.IsAdmin {
org.MaxRepoCreation = form.MaxRepoCreation opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
} }
org.FullName = form.FullName visibilityChanged := org.Visibility != form.Visibility
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 if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
org.Visibility = form.Visibility
if err := user_model.UpdateUser(ctx, org.AsUser(), false); err != nil {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return return
} }

View file

@ -11,6 +11,8 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "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 // SetEditorconfigIfExists set editor config as render variable
@ -55,8 +57,12 @@ func SetDiffViewStyle(ctx *context.Context) {
} }
ctx.Data["IsSplitStyle"] = style == "split" 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)
} }
} }

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -53,33 +54,33 @@ func AccountPost(ctx *context.Context) {
return return
} }
if len(form.Password) < setting.MinPasswordLength { if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
} else if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype { } else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match")) 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 { } else {
var err error opts := &user.UpdateAuthOptions{
if err = ctx.Doer.SetPassword(form.Password); err != nil { Password: optional.Some(form.Password),
ctx.ServerError("UpdateUser", err) MustChangePassword: optional.Some(false),
return
} }
if err := user_model.UpdateUserCols(ctx, ctx.Doer, "salt", "passwd_hash_algo", "passwd"); err != nil { if err := user.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err) switch {
return 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") 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) // Only fired when the primary email is inactive (Wrong state)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
} else { } else {
mailer.SendActivateEmailMail(ctx.Doer, email) mailer.SendActivateEmailMail(ctx.Doer, email.Email)
} }
address = email.Email address = email.Email
@ -160,9 +161,12 @@ func EmailPost(ctx *context.Context) {
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return 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) log.Error("Set Email Notifications failed: %v", err)
ctx.ServerError("SetEmailNotifications", err) ctx.ServerError("UpdateUser", err)
return return
} }
log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name) log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name)
@ -178,48 +182,47 @@ func EmailPost(ctx *context.Context) {
return return
} }
email := &user_model.EmailAddress{ if err := user.AddEmailAddresses(ctx, ctx.Doer, []string{form.Email}); err != nil {
UID: ctx.Doer.ID,
Email: form.Email,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
if err := user_model.AddEmailAddress(ctx, email); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) { if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx) loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) 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) loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
return } else {
ctx.ServerError("AddEmailAddresses", err)
} }
ctx.ServerError("AddEmailAddress", err)
return return
} }
// Send confirmation email // Send confirmation email
if setting.Service.RegisterEmailConfirm { 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 { if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) 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 { } else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success")) 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") ctx.Redirect(setting.AppSubURL + "/user/settings/account")
} }
// DeleteEmail response for delete user's email // DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) { func DeleteEmail(ctx *context.Context) {
if err := user_model.DeleteEmailAddress(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil { email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
ctx.ServerError("DeleteEmail", err) 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 return
} }
log.Trace("Email address deleted: %s", ctx.Doer.Name) log.Trace("Email address deleted: %s", ctx.Doer.Name)
@ -293,7 +296,7 @@ func loadAccountData(ctx *context.Context) {
emails[i] = &email emails[i] = &email
} }
ctx.Data["Emails"] = emails ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotifications() ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm

View file

@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
@ -49,40 +50,8 @@ func Profile(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsProfile) 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 // ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context) { func ProfilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
@ -93,29 +62,40 @@ func ProfilePost(ctx *context.Context) {
return return
} }
if len(form.Name) != 0 && ctx.Doer.Name != form.Name { form := web.GetForm(ctx).(*forms.UpdateProfileForm)
log.Debug("Changing name for %s to %s", ctx.Doer.Name, form.Name)
if err := HandleUsernameChange(ctx, ctx.Doer, form.Name); err != nil { 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") ctx.Redirect(setting.AppSubURL + "/user/settings")
return return
} }
ctx.Doer.Name = form.Name
ctx.Doer.LowerName = strings.ToLower(form.Name)
} }
ctx.Doer.FullName = form.FullName opts := &user_service.UpdateOptions{
ctx.Doer.KeepEmailPrivate = form.KeepEmailPrivate FullName: optional.Some(form.FullName),
ctx.Doer.Website = form.Website KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
ctx.Doer.Location = form.Location Description: optional.Some(form.Description),
ctx.Doer.Description = form.Description Website: optional.Some(form.Website),
ctx.Doer.KeepActivityPrivate = form.KeepActivityPrivate Location: optional.Some(form.Location),
ctx.Doer.Visibility = form.Visibility Visibility: optional.Some(form.Visibility),
if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil { KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
if _, ok := err.(user_model.ErrEmailAlreadyUsed); ok { }
ctx.Flash.Error(ctx.Tr("form.email_been_used")) if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return 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 { 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 return nil
@ -371,14 +351,15 @@ func UpdateUIThemePost(ctx *context.Context) {
return 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.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") } else {
return 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") ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
} }
@ -388,17 +369,19 @@ func UpdateUserLang(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAppearance"] = true ctx.Data["PageIsSettingsAppearance"] = true
if len(form.Language) != 0 { if form.Language != "" {
if !util.SliceContainsString(setting.Langs, form.Language) { if !util.SliceContainsString(setting.Langs, form.Language) {
ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language)) ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return return
} }
ctx.Doer.Language = form.Language
} }
if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil { opts := &user_service.UpdateOptions{
ctx.ServerError("UpdateUserSetting", err) Language: optional.Some(form.Language),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return return
} }

View file

@ -14,9 +14,11 @@ import (
"code.gitea.io/gitea/modules/auth/webauthn" "code.gitea.io/gitea/modules/auth/webauthn"
gitea_context "code.gitea.io/gitea/modules/context" gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "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 // 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 the user does not have a locale set, we save the current one.
if len(user.Language) == 0 { if len(user.Language) == 0 {
lc := middleware.Locale(resp, req) lc := middleware.Locale(resp, req)
user.Language = lc.Language() opts := &user_service.UpdateOptions{
if err := user_model.UpdateUserCols(req.Context(), user, "language"); err != nil { 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)) log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language))
return return
} }

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth" auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source" source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user" 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 { if user != nil && !user.ProhibitLogin {
cols := make([]string, 0) opts := &user_service.UpdateOptions{}
if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin { if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
// Change existing admin flag only if AdminFilter option is set // Change existing admin flag only if AdminFilter option is set
user.IsAdmin = sr.IsAdmin opts.IsAdmin = optional.Some(sr.IsAdmin)
cols = append(cols, "is_admin")
} }
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 // Change existing restricted flag only if RestrictedFilter option is set
user.IsRestricted = sr.IsRestricted opts.IsRestricted = optional.Some(sr.IsRestricted)
cols = append(cols, "is_restricted")
} }
if len(cols) > 0 { if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
err = user_model.UpdateUserCols(ctx, user, cols...) if err := user_service.UpdateUser(ctx, user, opts); err != nil {
if err != nil {
return nil, err return nil, err
} }
} }

View file

@ -15,6 +15,7 @@ import (
auth_module "code.gitea.io/gitea/modules/auth" auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source" source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user" 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) log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name)
usr.FullName = fullName opts := &user_service.UpdateOptions{
emailChanged := usr.Email != su.Mail FullName: optional.Some(fullName),
usr.Email = su.Mail IsActive: optional.Some(true),
// Change existing admin flag only if AdminFilter option is set }
if len(source.AdminFilter) > 0 { if source.AdminFilter != "" {
usr.IsAdmin = su.IsAdmin opts.IsAdmin = optional.Some(su.IsAdmin)
} }
// Change existing restricted flag only if RestrictedFilter option is set // Change existing restricted flag only if RestrictedFilter option is set
if !usr.IsAdmin && len(source.RestrictedFilter) > 0 { if !su.IsAdmin && source.RestrictedFilter != "" {
usr.IsRestricted = su.IsRestricted 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 := user_service.UpdateUser(ctx, usr, opts); err != nil {
if err != nil {
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err) 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) { 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) log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
usr.IsActive = false opts := &user_service.UpdateOptions{
err = user_model.UpdateUserCols(ctx, usr, "is_active") IsActive: optional.Some(false),
if err != nil { }
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) log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
} }
} }

View file

@ -108,7 +108,7 @@ func SendResetPasswordMail(u *user_model.User) {
} }
// SendActivateEmailMail sends confirmation email to confirm new email address // 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 { if setting.MailService == nil {
// No mail service configured // No mail service configured
return return
@ -118,8 +118,8 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
"locale": locale, "locale": locale,
"DisplayName": u.DisplayName(), "DisplayName": u.DisplayName(),
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
"Code": u.GenerateEmailActivateCode(email.Email), "Code": u.GenerateEmailActivateCode(email),
"Email": email.Email, "Email": email,
"Language": locale.Language(), "Language": locale.Language(),
} }
@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
return 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) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
SendAsync(msg) SendAsync(msg)

View file

@ -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) { 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 // 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) ct := fmt.Sprintf("Assigned #%d.", issue.Index)
if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{assignee}); err != nil { 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) 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) { 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()) ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL())
if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil { 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) log.Error("Error in SendIssueAssignedMail for issue[%d] to reviewer[%d]: %v", issue.ID, reviewer.ID, err)

View file

@ -57,7 +57,7 @@ func DeleteAvatar(ctx context.Context, u *user_model.User) error {
u.UseCustomAvatar = false u.UseCustomAvatar = false
u.Avatar = "" u.Avatar = ""
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil { 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 return nil
} }

166
services/user/email.go Normal file
View file

@ -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
}

129
services/user/email_test.go Normal file
View file

@ -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))
}

212
services/user/update.go Normal file
View file

@ -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")
}

View file

@ -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)
}

View file

@ -41,10 +41,7 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string) err
} }
if newUserName == u.Name { if newUserName == u.Name {
return user_model.ErrUsernameNotChanged{ return nil
UID: u.ID,
Name: u.Name,
}
} }
if err := user_model.IsUsableUsername(newUserName); err != nil { if err := user_model.IsUsableUsername(newUserName); err != nil {

View file

@ -107,7 +107,7 @@ func TestRenameUser(t *testing.T) {
}) })
t.Run("Same username", func(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) { t.Run("Non usable username", func(t *testing.T) {

View file

@ -208,11 +208,11 @@ func TestAPIEditUser(t *testing.T) {
SourceID: 0, SourceID: 0,
Email: &empty, Email: &empty,
}).AddTokenAuth(token) }).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusUnprocessableEntity) resp := MakeRequest(t, req, http.StatusBadRequest)
errMap := make(map[string]any) errMap := make(map[string]any)
json.Unmarshal(resp.Body.Bytes(), &errMap) 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"}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.False(t, user2.IsRestricted) assert.False(t, user2.IsRestricted)
@ -254,14 +254,14 @@ func TestAPIRenameUser(t *testing.T) {
// required // required
"new_name": "User2", "new_name": "User2",
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusNoContent)
urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2") urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2")
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
// required // required
"new_name": "User2-2-2", "new_name": "User2-2-2",
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusNoContent)
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
// required // required
@ -281,7 +281,7 @@ func TestAPIRenameUser(t *testing.T) {
// required // required
"new_name": "user2", "new_name": "user2",
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusNoContent)
} }
func TestAPICron(t *testing.T) { func TestAPICron(t *testing.T) {

View file

@ -32,7 +32,7 @@ func TestNodeinfo(t *testing.T) {
DecodeJSON(t, resp, &nodeinfo) DecodeJSON(t, resp, &nodeinfo)
assert.True(t, nodeinfo.OpenRegistrations) assert.True(t, nodeinfo.OpenRegistrations)
assert.Equal(t, "gitea", nodeinfo.Software.Name) 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, 20, nodeinfo.Usage.LocalPosts)
assert.Equal(t, 3, nodeinfo.Usage.LocalComments) assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
}) })

View file

@ -67,6 +67,16 @@ func TestAPIAddEmail(t *testing.T) {
var emails []*api.Email var emails []*api.Email
DecodeJSON(t, resp, &emails) DecodeJSON(t, resp, &emails)
assert.EqualValues(t, []*api.Email{ 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", Email: "user2-3@example.com",
Verified: true, Verified: true,

View file

@ -12,7 +12,6 @@ import (
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -45,9 +44,6 @@ func TestEmptyRepo(t *testing.T) {
func TestEmptyRepoAddFile(t *testing.T) { func TestEmptyRepoAddFile(t *testing.T) {
defer tests.PrepareTestEnv(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") session := loginUser(t, "user30")
req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
@ -72,9 +68,6 @@ func TestEmptyRepoAddFile(t *testing.T) {
func TestEmptyRepoUploadFile(t *testing.T) { func TestEmptyRepoUploadFile(t *testing.T) {
defer tests.PrepareTestEnv(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") session := loginUser(t, "user30")
req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
@ -112,9 +105,6 @@ func TestEmptyRepoUploadFile(t *testing.T) {
func TestEmptyRepoAddFileByAPI(t *testing.T) { func TestEmptyRepoAddFileByAPI(t *testing.T) {
defer tests.PrepareTestEnv(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") session := loginUser(t, "user30")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)