diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2021-11-11 15:03:30 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-11 15:03:30 +0800 |
commit | 90eb9fb889e5d3a5845576dbc63e3792f3da33f2 (patch) | |
tree | 4107df9dd446fe9a93e49cba1e59d0f721e70351 /models/user | |
parent | 492e1c2fbd1b646f4428207942a9f89b56f7b6a9 (diff) | |
download | gitea-90eb9fb889e5d3a5845576dbc63e3792f3da33f2.tar.gz gitea-90eb9fb889e5d3a5845576dbc63e3792f3da33f2.zip |
Move EmailAddress & UserRedirect into models/user/ (#17607)
* Move EmailAddress into models/user/
* Fix test
* rename user_mail to user_email
* Fix test
* Move UserRedirect into models/user/
* Fix lint & test
* Fix lint
* Fix lint
* remove nolint comment
* Fix lint
Diffstat (limited to 'models/user')
-rw-r--r-- | models/user/email_address.go | 269 | ||||
-rw-r--r-- | models/user/email_address_test.go | 131 | ||||
-rw-r--r-- | models/user/main_test.go | 19 | ||||
-rw-r--r-- | models/user/redirect.go | 79 | ||||
-rw-r--r-- | models/user/redirect_test.go | 23 |
5 files changed, 521 insertions, 0 deletions
diff --git a/models/user/email_address.go b/models/user/email_address.go new file mode 100644 index 0000000000..74fb71d454 --- /dev/null +++ b/models/user/email_address.go @@ -0,0 +1,269 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "context" + "errors" + "fmt" + "net/mail" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/builder" +) + +var ( + // ErrEmailNotActivated e-mail address has not been activated error + ErrEmailNotActivated = errors.New("E-mail address has not been activated") +) + +// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322 +type ErrEmailInvalid struct { + Email string +} + +// IsErrEmailInvalid checks if an error is an ErrEmailInvalid +func IsErrEmailInvalid(err error) bool { + _, ok := err.(ErrEmailInvalid) + return ok +} + +func (err ErrEmailInvalid) Error() string { + return fmt.Sprintf("e-mail invalid [email: %s]", err.Email) +} + +// ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error. +type ErrEmailAlreadyUsed struct { + Email string +} + +// IsErrEmailAlreadyUsed checks if an error is a ErrEmailAlreadyUsed. +func IsErrEmailAlreadyUsed(err error) bool { + _, ok := err.(ErrEmailAlreadyUsed) + return ok +} + +func (err ErrEmailAlreadyUsed) Error() string { + return fmt.Sprintf("e-mail already in use [email: %s]", err.Email) +} + +// ErrEmailAddressNotExist email address not exist +type ErrEmailAddressNotExist struct { + Email string +} + +// IsErrEmailAddressNotExist checks if an error is an ErrEmailAddressNotExist +func IsErrEmailAddressNotExist(err error) bool { + _, ok := err.(ErrEmailAddressNotExist) + return ok +} + +func (err ErrEmailAddressNotExist) Error() string { + return fmt.Sprintf("Email address does not exist [email: %s]", err.Email) +} + +// ErrPrimaryEmailCannotDelete primary email address cannot be deleted +type ErrPrimaryEmailCannotDelete struct { + Email string +} + +// IsErrPrimaryEmailCannotDelete checks if an error is an ErrPrimaryEmailCannotDelete +func IsErrPrimaryEmailCannotDelete(err error) bool { + _, ok := err.(ErrPrimaryEmailCannotDelete) + return ok +} + +func (err ErrPrimaryEmailCannotDelete) Error() string { + return fmt.Sprintf("Primary email address cannot be deleted [email: %s]", err.Email) +} + +// EmailAddress is the list of all email addresses of a user. It also contains the +// primary email address which is saved in user table. +type EmailAddress struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX NOT NULL"` + Email string `xorm:"UNIQUE NOT NULL"` + LowerEmail string `xorm:"UNIQUE NOT NULL"` + IsActivated bool + IsPrimary bool `xorm:"DEFAULT(false) NOT NULL"` +} + +func init() { + db.RegisterModel(new(EmailAddress)) +} + +// BeforeInsert will be invoked by XORM before inserting a record +func (email *EmailAddress) BeforeInsert() { + if email.LowerEmail == "" { + email.LowerEmail = strings.ToLower(email.Email) + } +} + +// ValidateEmail check if email is a allowed address +func ValidateEmail(email string) error { + if len(email) == 0 { + return nil + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + // TODO: add an email allow/block list + + return nil +} + +// GetEmailAddresses returns all email addresses belongs to given user. +func GetEmailAddresses(uid int64) ([]*EmailAddress, error) { + emails := make([]*EmailAddress, 0, 5) + if err := db.GetEngine(db.DefaultContext). + Where("uid=?", uid). + Asc("id"). + Find(&emails); err != nil { + return nil, err + } + return emails, nil +} + +// GetEmailAddressByID gets a user's email address by ID +func GetEmailAddressByID(uid, id int64) (*EmailAddress, error) { + // User ID is required for security reasons + email := &EmailAddress{UID: uid} + if has, err := db.GetEngine(db.DefaultContext).ID(id).Get(email); err != nil { + return nil, err + } else if !has { + return nil, nil + } + return email, nil +} + +// IsEmailActive check if email is activated with a different emailID +func IsEmailActive(ctx context.Context, email string, excludeEmailID int64) (bool, error) { + if len(email) == 0 { + return true, nil + } + + // Can't filter by boolean field unless it's explicit + cond := builder.NewCond() + cond = cond.And(builder.Eq{"lower_email": strings.ToLower(email)}, builder.Neq{"id": excludeEmailID}) + if setting.Service.RegisterEmailConfirm { + // Inactive (unvalidated) addresses don't count as active if email validation is required + cond = cond.And(builder.Eq{"is_activated": true}) + } + + var em EmailAddress + if has, err := db.GetEngine(ctx).Where(cond).Get(&em); has || err != nil { + if has { + log.Info("isEmailActive(%q, %d) found duplicate in email ID %d", email, excludeEmailID, em.ID) + } + return has, err + } + + return false, nil +} + +// IsEmailUsed returns true if the email has been used. +func IsEmailUsed(ctx context.Context, email string) (bool, error) { + if len(email) == 0 { + return true, nil + } + + return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{}) +} + +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) +} + +// AddEmailAddress adds an email address to given user. +func AddEmailAddress(email *EmailAddress) error { + return addEmailAddress(db.DefaultContext, email) +} + +// AddEmailAddresses adds an email address to given user. +func AddEmailAddresses(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(db.DefaultContext, 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(db.DefaultContext, emails); err != nil { + return fmt.Errorf("Insert: %v", err) + } + + return nil +} + +// DeleteEmailAddress deletes an email address of given user. +func DeleteEmailAddress(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(db.DefaultContext).ID(email.ID).Delete(&address) + } else { + if email.Email != "" && email.LowerEmail == "" { + email.LowerEmail = strings.ToLower(email.Email) + } + deleted, err = db.GetEngine(db.DefaultContext). + 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(emails []*EmailAddress) (err error) { + for i := range emails { + if err = DeleteEmailAddress(emails[i]); err != nil { + return err + } + } + + return nil +} diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go new file mode 100644 index 0000000000..5ed0bf8884 --- /dev/null +++ b/models/user/email_address_test.go @@ -0,0 +1,131 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + + "github.com/stretchr/testify/assert" +) + +func TestGetEmailAddresses(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + emails, _ := GetEmailAddresses(int64(1)) + if assert.Len(t, emails, 3) { + assert.True(t, emails[0].IsPrimary) + assert.True(t, emails[2].IsActivated) + assert.False(t, emails[2].IsPrimary) + } + + emails, _ = GetEmailAddresses(int64(2)) + if assert.Len(t, emails, 2) { + assert.True(t, emails[0].IsPrimary) + assert.True(t, emails[0].IsActivated) + } +} + +func TestIsEmailUsed(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + isExist, _ := IsEmailUsed(db.DefaultContext, "") + assert.True(t, isExist) + isExist, _ = IsEmailUsed(db.DefaultContext, "user11@example.com") + assert.True(t, isExist) + isExist, _ = IsEmailUsed(db.DefaultContext, "user1234567890@example.com") + assert.False(t, isExist) +} + +func TestAddEmailAddress(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + assert.NoError(t, AddEmailAddress(&EmailAddress{ + Email: "user1234567890@example.com", + LowerEmail: "user1234567890@example.com", + IsPrimary: true, + IsActivated: true, + })) + + // ErrEmailAlreadyUsed + err := AddEmailAddress(&EmailAddress{ + Email: "user1234567890@example.com", + LowerEmail: "user1234567890@example.com", + }) + assert.Error(t, err) + assert.True(t, IsErrEmailAlreadyUsed(err)) +} + +func TestAddEmailAddresses(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + // insert multiple email address + emails := make([]*EmailAddress, 2) + emails[0] = &EmailAddress{ + Email: "user1234@example.com", + LowerEmail: "user1234@example.com", + IsActivated: true, + } + emails[1] = &EmailAddress{ + Email: "user5678@example.com", + LowerEmail: "user5678@example.com", + IsActivated: true, + } + assert.NoError(t, AddEmailAddresses(emails)) + + // ErrEmailAlreadyUsed + err := AddEmailAddresses(emails) + assert.Error(t, err) + assert.True(t, IsErrEmailAlreadyUsed(err)) +} + +func TestDeleteEmailAddress(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + assert.NoError(t, DeleteEmailAddress(&EmailAddress{ + UID: int64(1), + ID: int64(33), + Email: "user1-2@example.com", + LowerEmail: "user1-2@example.com", + })) + + assert.NoError(t, DeleteEmailAddress(&EmailAddress{ + UID: int64(1), + Email: "user1-3@example.com", + LowerEmail: "user1-3@example.com", + })) + + // Email address does not exist + err := DeleteEmailAddress(&EmailAddress{ + UID: int64(1), + Email: "user1234567890@example.com", + LowerEmail: "user1234567890@example.com", + }) + assert.Error(t, err) +} + +func TestDeleteEmailAddresses(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + // delete multiple email address + emails := make([]*EmailAddress, 2) + emails[0] = &EmailAddress{ + UID: int64(2), + ID: int64(3), + Email: "user2@example.com", + LowerEmail: "user2@example.com", + } + emails[1] = &EmailAddress{ + UID: int64(2), + Email: "user2-2@example.com", + LowerEmail: "user2-2@example.com", + } + assert.NoError(t, DeleteEmailAddresses(emails)) + + // ErrEmailAlreadyUsed + err := DeleteEmailAddresses(emails) + assert.Error(t, err) +} diff --git a/models/user/main_test.go b/models/user/main_test.go new file mode 100644 index 0000000000..2999c4c81d --- /dev/null +++ b/models/user/main_test.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/db" +) + +func TestMain(m *testing.M) { + db.MainTest(m, filepath.Join("..", ".."), + "email_address.yml", + "user_redirect.yml", + ) +} diff --git a/models/user/redirect.go b/models/user/redirect.go new file mode 100644 index 0000000000..49370218db --- /dev/null +++ b/models/user/redirect.go @@ -0,0 +1,79 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" +) + +// ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error. +type ErrUserRedirectNotExist struct { + Name string +} + +// IsErrUserRedirectNotExist check if an error is an ErrUserRedirectNotExist. +func IsErrUserRedirectNotExist(err error) bool { + _, ok := err.(ErrUserRedirectNotExist) + return ok +} + +func (err ErrUserRedirectNotExist) Error() string { + return fmt.Sprintf("user redirect does not exist [name: %s]", err.Name) +} + +// Redirect represents that a user name should be redirected to another +type Redirect struct { + ID int64 `xorm:"pk autoincr"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + RedirectUserID int64 // userID to redirect to +} + +// TableName provides the real table name +func (Redirect) TableName() string { + return "user_redirect" +} + +func init() { + db.RegisterModel(new(Redirect)) +} + +// LookupUserRedirect look up userID if a user has a redirect name +func LookupUserRedirect(userName string) (int64, error) { + userName = strings.ToLower(userName) + redirect := &Redirect{LowerName: userName} + if has, err := db.GetEngine(db.DefaultContext).Get(redirect); err != nil { + return 0, err + } else if !has { + return 0, ErrUserRedirectNotExist{Name: userName} + } + return redirect.RedirectUserID, nil +} + +// NewUserRedirect create a new user redirect +func NewUserRedirect(ctx context.Context, ID int64, oldUserName, newUserName string) error { + oldUserName = strings.ToLower(oldUserName) + newUserName = strings.ToLower(newUserName) + + if err := DeleteUserRedirect(ctx, newUserName); err != nil { + return err + } + + return db.Insert(ctx, &Redirect{ + LowerName: oldUserName, + RedirectUserID: ID, + }) +} + +// DeleteUserRedirect delete any redirect from the specified user name to +// anything else +func DeleteUserRedirect(ctx context.Context, userName string) error { + userName = strings.ToLower(userName) + _, err := db.GetEngine(ctx).Delete(&Redirect{LowerName: userName}) + return err +} diff --git a/models/user/redirect_test.go b/models/user/redirect_test.go new file mode 100644 index 0000000000..b33c42cf3d --- /dev/null +++ b/models/user/redirect_test.go @@ -0,0 +1,23 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "github.com/stretchr/testify/assert" +) + +func TestLookupUserRedirect(t *testing.T) { + assert.NoError(t, db.PrepareTestDatabase()) + + userID, err := LookupUserRedirect("olduser1") + assert.NoError(t, err) + assert.EqualValues(t, 1, userID) + + _, err = LookupUserRedirect("doesnotexist") + assert.True(t, IsErrUserRedirectNotExist(err)) +} |