aboutsummaryrefslogtreecommitdiffstats
path: root/models/user_mail.go
diff options
context:
space:
mode:
authorguillep2k <18600385+guillep2k@users.noreply.github.com>2020-03-02 15:25:36 -0300
committerGitHub <noreply@github.com>2020-03-02 15:25:36 -0300
commit5e1438ba92fe5b4398ebf468e4ede21c7ef60409 (patch)
tree40096ee69a4ac5df46428a3cf1ae7ee3bee8078e /models/user_mail.go
parentb5ecc82d6e22b5701bfadc1ebc430b9c7fef0cc8 (diff)
downloadgitea-5e1438ba92fe5b4398ebf468e4ede21c7ef60409.tar.gz
gitea-5e1438ba92fe5b4398ebf468e4ede21c7ef60409.zip
Admin page for managing user e-mail activation (#10557)
* Implement mail activation admin panel * Add export comments * Fix another export comment * again... * And again! * Apply suggestions by @lunny * Add UI for user activated emails * Make new activation UI work * Fix lint * Prevent admin from self-deactivate; add modal Co-authored-by: zeripath <art27@cantab.net>
Diffstat (limited to 'models/user_mail.go')
-rw-r--r--models/user_mail.go281
1 files changed, 265 insertions, 16 deletions
diff --git a/models/user_mail.go b/models/user_mail.go
index 41f08c9db2..af9602e714 100644
--- a/models/user_mail.go
+++ b/models/user_mail.go
@@ -1,4 +1,5 @@
// 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.
@@ -8,6 +9,12 @@ import (
"errors"
"fmt"
"strings"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
)
var (
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
if !isPrimaryFound {
emails = append(emails, &EmailAddress{
Email: u.Email,
- IsActivated: true,
+ IsActivated: u.IsActive,
IsPrimary: true,
})
}
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{ID: id, UID: uid}
+ if has, err := x.Get(email); err != nil {
+ return nil, err
+ } else if !has {
+ return nil, nil
+ }
+ return email, nil
+}
+
+func isEmailActive(e Engine, email string, userID, emailID 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{"email": email}, builder.Neq{"id": emailID})
+ 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})
+ }
+
+ em := EmailAddress{}
+
+ if has, err := e.Where(cond).Get(&em); has || err != nil {
+ if has {
+ log.Info("isEmailActive('%s',%d,%d) found duplicate in email ID %d", email, userID, emailID, em.ID)
+ }
+ return has, err
+ }
+
+ // Can't filter by boolean field unless it's explicit
+ cond = builder.NewCond()
+ cond = cond.And(builder.Eq{"email": email}, builder.Neq{"id": userID})
+ if setting.Service.RegisterEmailConfirm {
+ cond = cond.And(builder.Eq{"is_active": true})
+ }
+
+ us := User{}
+
+ if has, err := e.Where(cond).Get(&us); has || err != nil {
+ if has {
+ log.Info("isEmailActive('%s',%d,%d) found duplicate in user ID %d", email, userID, emailID, us.ID)
+ }
+ return has, err
+ }
+
+ return false, nil
+}
+
func isEmailUsed(e Engine, email string) (bool, error) {
if len(email) == 0 {
return true, nil
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
// Activate activates the email address to given user.
func (email *EmailAddress) Activate() error {
- user, err := GetUserByID(email.UID)
- if err != nil {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
return err
}
- if user.Rands, err = GetUserSalt(); err != nil {
+ if err := email.updateActivation(sess, true); err != nil {
return err
}
+ return sess.Commit()
+}
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
+func (email *EmailAddress) updateActivation(e Engine, activate bool) error {
+ user, err := getUserByID(e, email.UID)
+ if err != nil {
return err
}
-
- email.IsActivated = true
- if _, err := sess.
- ID(email.ID).
- Cols("is_activated").
- Update(email); err != nil {
+ if user.Rands, err = GetUserSalt(); err != nil {
return err
- } else if err = updateUserCols(sess, user, "rands"); err != nil {
+ }
+ email.IsActivated = activate
+ if _, err := e.ID(email.ID).Cols("is_activated").Update(email); err != nil {
return err
}
-
- return sess.Commit()
+ return updateUserCols(e, user, "rands")
}
// DeleteEmailAddress deletes an email address of given user.
@@ -228,3 +287,193 @@ func MakeEmailPrimary(email *EmailAddress) error {
return sess.Commit()
}
+
+// SearchEmailOrderBy is used to sort the results from SearchEmails()
+type SearchEmailOrderBy string
+
+func (s SearchEmailOrderBy) String() string {
+ return string(s)
+}
+
+// Strings for sorting result
+const (
+ SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
+ SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
+ SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
+ SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
+)
+
+// SearchEmailOptions are options to search e-mail addresses for the admin panel
+type SearchEmailOptions struct {
+ ListOptions
+ Keyword string
+ SortType SearchEmailOrderBy
+ IsPrimary util.OptionalBool
+ IsActivated util.OptionalBool
+}
+
+// SearchEmailResult is an e-mail address found in the user or email_address table
+type SearchEmailResult struct {
+ UID int64
+ Email string
+ IsActivated bool
+ IsPrimary bool
+ // From User
+ Name string
+ FullName string
+}
+
+// SearchEmails takes options i.e. keyword and part of email name to search,
+// it returns results in given range and number of total results.
+func SearchEmails(opts *SearchEmailOptions) ([]*SearchEmailResult, int64, error) {
+ // Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
+ // build the SQL ourselves.
+ where := make([]string, 0, 5)
+ args := make([]interface{}, 0, 5)
+
+ emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
+ "FROM email_address " +
+ "UNION ALL " +
+ "SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
+ "FROM `user` " +
+ "WHERE type = ?) AS emails"
+ args = append(args, UserTypeIndividual)
+
+ if len(opts.Keyword) > 0 {
+ // Note: % can be injected in the Keyword parameter, but it won't do any harm.
+ where = append(where, "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)")
+ likeStr := "%" + strings.ToLower(opts.Keyword) + "%"
+ args = append(args, likeStr)
+ args = append(args, likeStr)
+ args = append(args, likeStr)
+ }
+
+ switch {
+ case opts.IsPrimary.IsTrue():
+ where = append(where, "emails.is_primary = ?")
+ args = append(args, true)
+ case opts.IsPrimary.IsFalse():
+ where = append(where, "emails.is_primary = ?")
+ args = append(args, false)
+ }
+
+ switch {
+ case opts.IsActivated.IsTrue():
+ where = append(where, "emails.is_activated = ?")
+ args = append(args, true)
+ case opts.IsActivated.IsFalse():
+ where = append(where, "emails.is_activated = ?")
+ args = append(args, false)
+ }
+
+ var whereStr string
+ if len(where) > 0 {
+ whereStr = "WHERE " + strings.Join(where, " AND ")
+ }
+
+ joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
+
+ count, err := x.SQL("SELECT count(*) "+joinSQL, args...).Count()
+ if err != nil {
+ return nil, 0, fmt.Errorf("Count: %v", err)
+ }
+
+ orderby := opts.SortType.String()
+ if orderby == "" {
+ orderby = SearchEmailOrderByEmail.String()
+ }
+
+ querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
+ "`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
+
+ opts.setDefaultValues()
+
+ rows, err := x.SQL(querySQL, args...).Rows(new(SearchEmailResult))
+ if err != nil {
+ return nil, 0, fmt.Errorf("Emails: %v", err)
+ }
+
+ // Page manually because xorm can't handle Limit() with raw SQL
+ defer rows.Close()
+
+ emails := make([]*SearchEmailResult, 0, opts.PageSize)
+ skip := (opts.Page - 1) * opts.PageSize
+
+ for rows.Next() {
+ var email SearchEmailResult
+ if err := rows.Scan(&email); err != nil {
+ return nil, 0, err
+ }
+ if skip > 0 {
+ skip--
+ continue
+ }
+ emails = append(emails, &email)
+ if len(emails) == opts.PageSize {
+ break
+ }
+ }
+
+ return emails, count, err
+}
+
+// ActivateUserEmail will change the activated state of an email address,
+// either primary (in the user table) or secondary (in the email_address table)
+func ActivateUserEmail(userID int64, email string, primary, activate bool) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+ if primary {
+ // Activate/deactivate a user's primary email address
+ user := User{ID: userID, Email: email}
+ if has, err := sess.Get(&user); err != nil {
+ return err
+ } else if !has {
+ return fmt.Errorf("no such user: %d (%s)", userID, email)
+ }
+ if user.IsActive == activate {
+ // Already in the desired state; no action
+ return nil
+ }
+ if activate {
+ if used, err := isEmailActive(sess, email, userID, 0); err != nil {
+ return fmt.Errorf("isEmailActive(): %v", err)
+ } else if used {
+ return ErrEmailAlreadyUsed{Email: email}
+ }
+ }
+ user.IsActive = activate
+ if user.Rands, err = GetUserSalt(); err != nil {
+ return fmt.Errorf("generate salt: %v", err)
+ }
+ if err = updateUserCols(sess, &user, "is_active", "rands"); err != nil {
+ return fmt.Errorf("updateUserCols(): %v", err)
+ }
+ } else {
+ // Activate/deactivate a user's secondary email address
+ // First check if there's another user active with the same address
+ addr := EmailAddress{UID: userID, Email: email}
+ if has, err := sess.Get(&addr); err != nil {
+ return err
+ } else if !has {
+ return fmt.Errorf("no such email: %d (%s)", userID, email)
+ }
+ if addr.IsActivated == activate {
+ // Already in the desired state; no action
+ return nil
+ }
+ if activate {
+ if used, err := isEmailActive(sess, email, 0, addr.ID); err != nil {
+ return fmt.Errorf("isEmailActive(): %v", err)
+ } else if used {
+ return ErrEmailAlreadyUsed{Email: email}
+ }
+ }
+ if err = addr.updateActivation(sess, activate); err != nil {
+ return fmt.Errorf("updateActivation(): %v", err)
+ }
+ }
+ return sess.Commit()
+}