diff options
author | guillep2k <18600385+guillep2k@users.noreply.github.com> | 2020-03-02 15:25:36 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-02 15:25:36 -0300 |
commit | 5e1438ba92fe5b4398ebf468e4ede21c7ef60409 (patch) | |
tree | 40096ee69a4ac5df46428a3cf1ae7ee3bee8078e /models/user_mail.go | |
parent | b5ecc82d6e22b5701bfadc1ebc430b9c7fef0cc8 (diff) | |
download | gitea-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.go | 281 |
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() +} |