mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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>
This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
				
			|||||||
// Copyright 2016 The Gogs Authors. All rights reserved.
 | 
					// 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
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,6 +9,12 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
 | 
				
			|||||||
	if !isPrimaryFound {
 | 
						if !isPrimaryFound {
 | 
				
			||||||
		emails = append(emails, &EmailAddress{
 | 
							emails = append(emails, &EmailAddress{
 | 
				
			||||||
			Email:       u.Email,
 | 
								Email:       u.Email,
 | 
				
			||||||
			IsActivated: true,
 | 
								IsActivated: u.IsActive,
 | 
				
			||||||
			IsPrimary:   true,
 | 
								IsPrimary:   true,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return emails, nil
 | 
						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) {
 | 
					func isEmailUsed(e Engine, email string) (bool, error) {
 | 
				
			||||||
	if len(email) == 0 {
 | 
						if len(email) == 0 {
 | 
				
			||||||
		return true, nil
 | 
							return true, nil
 | 
				
			||||||
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Activate activates the email address to given user.
 | 
					// Activate activates the email address to given user.
 | 
				
			||||||
func (email *EmailAddress) Activate() error {
 | 
					func (email *EmailAddress) Activate() error {
 | 
				
			||||||
	user, err := GetUserByID(email.UID)
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
						if err := sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := email.updateActivation(sess, true); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sess.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (email *EmailAddress) updateActivation(e Engine, activate bool) error {
 | 
				
			||||||
 | 
						user, err := getUserByID(e, email.UID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if user.Rands, err = GetUserSalt(); err != nil {
 | 
						if user.Rands, err = GetUserSalt(); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						email.IsActivated = activate
 | 
				
			||||||
	sess := x.NewSession()
 | 
						if _, err := e.ID(email.ID).Cols("is_activated").Update(email); err != nil {
 | 
				
			||||||
	defer sess.Close()
 | 
					 | 
				
			||||||
	if err = sess.Begin(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return updateUserCols(e, user, "rands")
 | 
				
			||||||
	email.IsActivated = true
 | 
					 | 
				
			||||||
	if _, err := sess.
 | 
					 | 
				
			||||||
		ID(email.ID).
 | 
					 | 
				
			||||||
		Cols("is_activated").
 | 
					 | 
				
			||||||
		Update(email); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	} else if err = updateUserCols(sess, user, "rands"); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return sess.Commit()
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeleteEmailAddress deletes an email address of given user.
 | 
					// DeleteEmailAddress deletes an email address of given user.
 | 
				
			||||||
@@ -228,3 +287,193 @@ func MakeEmailPrimary(email *EmailAddress) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return sess.Commit()
 | 
						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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ package models
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -169,3 +171,67 @@ func TestActivate(t *testing.T) {
 | 
				
			|||||||
	assert.True(t, emails[2].IsActivated)
 | 
						assert.True(t, emails[2].IsActivated)
 | 
				
			||||||
	assert.True(t, emails[2].IsPrimary)
 | 
						assert.True(t, emails[2].IsPrimary)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListEmails(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find all users and their emails
 | 
				
			||||||
 | 
						opts := &SearchEmailOptions{}
 | 
				
			||||||
 | 
						emails, count, err := SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.NotEqual(t, int64(0), count)
 | 
				
			||||||
 | 
						assert.True(t, count > 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						contains := func(match func(s *SearchEmailResult) bool) bool {
 | 
				
			||||||
 | 
							for _, v := range emails {
 | 
				
			||||||
 | 
								if match(v) {
 | 
				
			||||||
 | 
									return true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 18 }))
 | 
				
			||||||
 | 
						// 'user3' is an organization
 | 
				
			||||||
 | 
						assert.False(t, contains(func(s *SearchEmailResult) bool { return s.UID == 3 }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find no records
 | 
				
			||||||
 | 
						opts = &SearchEmailOptions{Keyword: "NOTFOUND"}
 | 
				
			||||||
 | 
						emails, count, err = SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Equal(t, int64(0), count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find users 'user2', 'user28', etc.
 | 
				
			||||||
 | 
						opts = &SearchEmailOptions{Keyword: "user2"}
 | 
				
			||||||
 | 
						emails, count, err = SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.NotEqual(t, int64(0), count)
 | 
				
			||||||
 | 
						assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 2 }))
 | 
				
			||||||
 | 
						assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 27 }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find only primary addresses (i.e. from the `user` table)
 | 
				
			||||||
 | 
						opts = &SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
 | 
				
			||||||
 | 
						emails, count, err = SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.True(t, contains(func(s *SearchEmailResult) bool { return s.IsPrimary }))
 | 
				
			||||||
 | 
						assert.False(t, contains(func(s *SearchEmailResult) bool { return !s.IsPrimary }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find only inactive addresses (i.e. not validated)
 | 
				
			||||||
 | 
						opts = &SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
 | 
				
			||||||
 | 
						emails, count, err = SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.True(t, contains(func(s *SearchEmailResult) bool { return !s.IsActivated }))
 | 
				
			||||||
 | 
						assert.False(t, contains(func(s *SearchEmailResult) bool { return s.IsActivated }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must find more than one page, but retrieve only one
 | 
				
			||||||
 | 
						opts = &SearchEmailOptions{
 | 
				
			||||||
 | 
							ListOptions: ListOptions{
 | 
				
			||||||
 | 
								PageSize: 5,
 | 
				
			||||||
 | 
								Page:     1,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						emails, count, err = SearchEmails(opts)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Equal(t, 5, len(emails))
 | 
				
			||||||
 | 
						assert.True(t, count > int64(len(emails)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -440,7 +440,11 @@ manage_openid = Manage OpenID Addresses
 | 
				
			|||||||
email_desc = Your primary email address will be used for notifications and other operations.
 | 
					email_desc = Your primary email address will be used for notifications and other operations.
 | 
				
			||||||
theme_desc = This will be your default theme across the site.
 | 
					theme_desc = This will be your default theme across the site.
 | 
				
			||||||
primary = Primary
 | 
					primary = Primary
 | 
				
			||||||
 | 
					activated = Activated
 | 
				
			||||||
 | 
					requires_activation = Requires activation
 | 
				
			||||||
primary_email = Make Primary
 | 
					primary_email = Make Primary
 | 
				
			||||||
 | 
					activate_email = Send Activation
 | 
				
			||||||
 | 
					activations_pending = Activations Pending
 | 
				
			||||||
delete_email = Remove
 | 
					delete_email = Remove
 | 
				
			||||||
email_deletion = Remove Email Address
 | 
					email_deletion = Remove Email Address
 | 
				
			||||||
email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue?
 | 
					email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue?
 | 
				
			||||||
@@ -1724,6 +1728,7 @@ organizations = Organizations
 | 
				
			|||||||
repositories = Repositories
 | 
					repositories = Repositories
 | 
				
			||||||
hooks = Default Webhooks
 | 
					hooks = Default Webhooks
 | 
				
			||||||
authentication = Authentication Sources
 | 
					authentication = Authentication Sources
 | 
				
			||||||
 | 
					emails = User Emails
 | 
				
			||||||
config = Configuration
 | 
					config = Configuration
 | 
				
			||||||
notices = System Notices
 | 
					notices = System Notices
 | 
				
			||||||
monitor = Monitoring
 | 
					monitor = Monitoring
 | 
				
			||||||
@@ -1793,6 +1798,7 @@ dashboard.gc_times = GC Times
 | 
				
			|||||||
users.user_manage_panel = User Account Management
 | 
					users.user_manage_panel = User Account Management
 | 
				
			||||||
users.new_account = Create User Account
 | 
					users.new_account = Create User Account
 | 
				
			||||||
users.name = Username
 | 
					users.name = Username
 | 
				
			||||||
 | 
					users.full_name = Full Name
 | 
				
			||||||
users.activated = Activated
 | 
					users.activated = Activated
 | 
				
			||||||
users.admin = Admin
 | 
					users.admin = Admin
 | 
				
			||||||
users.restricted = Restricted
 | 
					users.restricted = Restricted
 | 
				
			||||||
@@ -1824,6 +1830,19 @@ users.still_own_repo = This user still owns one or more repositories. Delete or
 | 
				
			|||||||
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
 | 
					users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
 | 
				
			||||||
users.deletion_success = The user account has been deleted.
 | 
					users.deletion_success = The user account has been deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					emails.email_manage_panel = User Email Management
 | 
				
			||||||
 | 
					emails.primary = Primary
 | 
				
			||||||
 | 
					emails.activated = Activated
 | 
				
			||||||
 | 
					emails.filter_sort.email = Email
 | 
				
			||||||
 | 
					emails.filter_sort.email_reverse = Email (reverse)
 | 
				
			||||||
 | 
					emails.filter_sort.name = User Name
 | 
				
			||||||
 | 
					emails.filter_sort.name_reverse = User Name (reverse)
 | 
				
			||||||
 | 
					emails.updated = Email updated
 | 
				
			||||||
 | 
					emails.not_updated = Failed to update the requested email address: %v
 | 
				
			||||||
 | 
					emails.duplicate_active = This email address is already active for a different user.
 | 
				
			||||||
 | 
					emails.change_email_header = Update Email Properties
 | 
				
			||||||
 | 
					emails.change_email_text = Are your sure you want to update this email address?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
orgs.org_manage_panel = Organization Management
 | 
					orgs.org_manage_panel = Organization Management
 | 
				
			||||||
orgs.name = Name
 | 
					orgs.name = Name
 | 
				
			||||||
orgs.teams = Teams
 | 
					orgs.teams = Teams
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										157
									
								
								routers/admin/emails.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								routers/admin/emails.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/unknwon/com"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplEmails base.TplName = "admin/emails/list"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Emails show all emails
 | 
				
			||||||
 | 
					func Emails(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("admin.emails")
 | 
				
			||||||
 | 
						ctx.Data["PageIsAdmin"] = true
 | 
				
			||||||
 | 
						ctx.Data["PageIsAdminEmails"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts := &models.SearchEmailOptions{
 | 
				
			||||||
 | 
							ListOptions: models.ListOptions{
 | 
				
			||||||
 | 
								PageSize: setting.UI.Admin.UserPagingNum,
 | 
				
			||||||
 | 
								Page:     ctx.QueryInt("page"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opts.Page <= 1 {
 | 
				
			||||||
 | 
							opts.Page = 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type ActiveEmail struct {
 | 
				
			||||||
 | 
							models.SearchEmailResult
 | 
				
			||||||
 | 
							CanChange bool
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							baseEmails []*models.SearchEmailResult
 | 
				
			||||||
 | 
							emails     []ActiveEmail
 | 
				
			||||||
 | 
							count      int64
 | 
				
			||||||
 | 
							err        error
 | 
				
			||||||
 | 
							orderBy    models.SearchEmailOrderBy
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["SortType"] = ctx.Query("sort")
 | 
				
			||||||
 | 
						switch ctx.Query("sort") {
 | 
				
			||||||
 | 
						case "email":
 | 
				
			||||||
 | 
							orderBy = models.SearchEmailOrderByEmail
 | 
				
			||||||
 | 
						case "reverseemail":
 | 
				
			||||||
 | 
							orderBy = models.SearchEmailOrderByEmailReverse
 | 
				
			||||||
 | 
						case "username":
 | 
				
			||||||
 | 
							orderBy = models.SearchEmailOrderByName
 | 
				
			||||||
 | 
						case "reverseusername":
 | 
				
			||||||
 | 
							orderBy = models.SearchEmailOrderByNameReverse
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							ctx.Data["SortType"] = "email"
 | 
				
			||||||
 | 
							orderBy = models.SearchEmailOrderByEmail
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts.Keyword = ctx.QueryTrim("q")
 | 
				
			||||||
 | 
						opts.SortType = orderBy
 | 
				
			||||||
 | 
						if len(ctx.Query("is_activated")) != 0 {
 | 
				
			||||||
 | 
							opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(ctx.Query("is_primary")) != 0 {
 | 
				
			||||||
 | 
							opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
 | 
				
			||||||
 | 
							baseEmails, count, err = models.SearchEmails(opts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("SearchEmails", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							emails = make([]ActiveEmail, len(baseEmails))
 | 
				
			||||||
 | 
							for i := range baseEmails {
 | 
				
			||||||
 | 
								emails[i].SearchEmailResult = *baseEmails[i]
 | 
				
			||||||
 | 
								// Don't let the admin deactivate its own primary email address
 | 
				
			||||||
 | 
								// We already know the user is admin
 | 
				
			||||||
 | 
								emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Data["Keyword"] = opts.Keyword
 | 
				
			||||||
 | 
						ctx.Data["Total"] = count
 | 
				
			||||||
 | 
						ctx.Data["Emails"] = emails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
 | 
				
			||||||
 | 
						pager.SetDefaultParams(ctx)
 | 
				
			||||||
 | 
						ctx.Data["Page"] = pager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.HTML(200, tplEmails)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						nullByte = []byte{0x00}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isKeywordValid(keyword string) bool {
 | 
				
			||||||
 | 
						return !bytes.Contains([]byte(keyword), nullByte)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ActivateEmail serves a POST request for activating/deactivating a user's email
 | 
				
			||||||
 | 
					func ActivateEmail(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						truefalse := map[string]bool{"1": true, "0": false}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uid := com.StrTo(ctx.Query("uid")).MustInt64()
 | 
				
			||||||
 | 
						email := ctx.Query("email")
 | 
				
			||||||
 | 
						primary, okp := truefalse[ctx.Query("primary")]
 | 
				
			||||||
 | 
						activate, oka := truefalse[ctx.Query("activate")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if uid == 0 || len(email) == 0 || !okp || !oka {
 | 
				
			||||||
 | 
							ctx.Error(400)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
 | 
				
			||||||
 | 
							log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
 | 
				
			||||||
 | 
							if models.IsErrEmailAlreadyUsed(err) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
 | 
				
			||||||
 | 
							ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
 | 
				
			||||||
 | 
						q := url.Values{}
 | 
				
			||||||
 | 
						if val := ctx.QueryTrim("q"); len(val) > 0 {
 | 
				
			||||||
 | 
							q.Set("q", val)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if val := ctx.QueryTrim("sort"); len(val) > 0 {
 | 
				
			||||||
 | 
							q.Set("sort", val)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
 | 
				
			||||||
 | 
							q.Set("is_primary", val)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
 | 
				
			||||||
 | 
							q.Set("is_activated", val)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						redirect.RawQuery = q.Encode()
 | 
				
			||||||
 | 
						ctx.Redirect(redirect.String())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -444,6 +444,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
				
			|||||||
			m.Post("/:userid/delete", admin.DeleteUser)
 | 
								m.Post("/:userid/delete", admin.DeleteUser)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.Group("/emails", func() {
 | 
				
			||||||
 | 
								m.Get("", admin.Emails)
 | 
				
			||||||
 | 
								m.Post("/activate", admin.ActivateEmail)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		m.Group("/orgs", func() {
 | 
							m.Group("/orgs", func() {
 | 
				
			||||||
			m.Get("", admin.Organizations)
 | 
								m.Get("", admin.Organizations)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1217,8 +1217,18 @@ func ActivateEmail(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		log.Trace("Email activated: %s", email.Email)
 | 
							log.Trace("Email activated: %s", email.Email)
 | 
				
			||||||
		ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
 | 
							ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if u, err := models.GetUserByID(email.UID); err != nil {
 | 
				
			||||||
 | 
								log.Warn("GetUserByID: %d", email.UID)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Allow user to validate more emails
 | 
				
			||||||
 | 
								_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FIXME: e-mail verification does not require the user to be logged in,
 | 
				
			||||||
 | 
						// so this could be redirecting to the login page.
 | 
				
			||||||
 | 
						// Should users be logged in automatically here? (consider 2FA requirements, etc.)
 | 
				
			||||||
	ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
						ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -88,6 +88,51 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
 | 
				
			|||||||
		ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
							ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// Send activation Email
 | 
				
			||||||
 | 
						if ctx.Query("_method") == "SENDACTIVATION" {
 | 
				
			||||||
 | 
							var address string
 | 
				
			||||||
 | 
							if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
 | 
				
			||||||
 | 
								log.Error("Send activation: activation still pending")
 | 
				
			||||||
 | 
								ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ctx.Query("id") == "PRIMARY" {
 | 
				
			||||||
 | 
								if ctx.User.IsActive {
 | 
				
			||||||
 | 
									log.Error("Send activation: email not set for activation")
 | 
				
			||||||
 | 
									ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
 | 
				
			||||||
 | 
								address = ctx.User.Email
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								id := ctx.QueryInt64("id")
 | 
				
			||||||
 | 
								email, err := models.GetEmailAddressByID(ctx.User.ID, id)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
 | 
				
			||||||
 | 
									ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if email == nil {
 | 
				
			||||||
 | 
									log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
 | 
				
			||||||
 | 
									ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if email.IsActivated {
 | 
				
			||||||
 | 
									log.Error("Send activation: email not set for activation")
 | 
				
			||||||
 | 
									ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
 | 
				
			||||||
 | 
								address = email.Email
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
 | 
				
			||||||
 | 
								log.Error("Set cache(MailResendLimit) fail: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
 | 
				
			||||||
 | 
							ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// Set Email Notification Preference
 | 
						// Set Email Notification Preference
 | 
				
			||||||
	if ctx.Query("_method") == "NOTIFICATION" {
 | 
						if ctx.Query("_method") == "NOTIFICATION" {
 | 
				
			||||||
		preference := ctx.Query("preference")
 | 
							preference := ctx.Query("preference")
 | 
				
			||||||
@@ -134,7 +179,6 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
 | 
				
			|||||||
	// Send confirmation email
 | 
						// Send confirmation email
 | 
				
			||||||
	if setting.Service.RegisterEmailConfirm {
 | 
						if setting.Service.RegisterEmailConfirm {
 | 
				
			||||||
		mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
 | 
							mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
 | 
							if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
 | 
				
			||||||
			log.Error("Set cache(MailResendLimit) fail: %v", err)
 | 
								log.Error("Set cache(MailResendLimit) fail: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -223,11 +267,25 @@ func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadAccountData(ctx *context.Context) {
 | 
					func loadAccountData(ctx *context.Context) {
 | 
				
			||||||
	emails, err := models.GetEmailAddresses(ctx.User.ID)
 | 
						emlist, err := models.GetEmailAddresses(ctx.User.ID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("GetEmailAddresses", err)
 | 
							ctx.ServerError("GetEmailAddresses", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						type UserEmail struct {
 | 
				
			||||||
 | 
							models.EmailAddress
 | 
				
			||||||
 | 
							CanBePrimary bool
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
 | 
				
			||||||
 | 
						emails := make([]*UserEmail, len(emlist))
 | 
				
			||||||
 | 
						for i, em := range emlist {
 | 
				
			||||||
 | 
							var email UserEmail
 | 
				
			||||||
 | 
							email.EmailAddress = *em
 | 
				
			||||||
 | 
							email.CanBePrimary = em.IsActivated
 | 
				
			||||||
 | 
							emails[i] = &email
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	ctx.Data["Emails"] = emails
 | 
						ctx.Data["Emails"] = emails
 | 
				
			||||||
	ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
 | 
						ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
 | 
				
			||||||
 | 
						ctx.Data["ActivationsPending"] = pendingActivation
 | 
				
			||||||
 | 
						ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										101
									
								
								templates/admin/emails/list.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								templates/admin/emails/list.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					{{template "base/head" .}}
 | 
				
			||||||
 | 
					<div class="admin user">
 | 
				
			||||||
 | 
						{{template "admin/navbar" .}}
 | 
				
			||||||
 | 
						<div class="ui container">
 | 
				
			||||||
 | 
							{{template "base/alert" .}}
 | 
				
			||||||
 | 
							<h4 class="ui top attached header">
 | 
				
			||||||
 | 
								{{.i18n.Tr "admin.emails.email_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}})
 | 
				
			||||||
 | 
							</h4>
 | 
				
			||||||
 | 
							<div class="ui attached segment">
 | 
				
			||||||
 | 
								<div class="ui right floated secondary filter menu">
 | 
				
			||||||
 | 
								<!-- Sort -->
 | 
				
			||||||
 | 
									<div class="ui dropdown type jump item">
 | 
				
			||||||
 | 
										<span class="text">
 | 
				
			||||||
 | 
											{{.i18n.Tr "repo.issues.filter_sort"}}
 | 
				
			||||||
 | 
											<i class="dropdown icon"></i>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
										<div class="menu">
 | 
				
			||||||
 | 
											<a class="{{if or (eq .SortType "email") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email"}}</a>
 | 
				
			||||||
 | 
											<a class="{{if eq .SortType "reverseemail"}}active{{end}} item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email_reverse"}}</a>
 | 
				
			||||||
 | 
											<a class="{{if eq .SortType "username"}}active{{end}} item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name"}}</a>
 | 
				
			||||||
 | 
											<a class="{{if eq .SortType "reverseusername"}}active{{end}} item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name_reverse"}}</a>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<form class="ui form ignore-dirty"  style="max-width: 90%">
 | 
				
			||||||
 | 
									<div class="ui fluid action input">
 | 
				
			||||||
 | 
									<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
 | 
				
			||||||
 | 
									<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="ui attached table segment">
 | 
				
			||||||
 | 
								<table class="ui very basic striped table">
 | 
				
			||||||
 | 
									<thead>
 | 
				
			||||||
 | 
										<tr>
 | 
				
			||||||
 | 
											<th>{{.i18n.Tr "admin.users.name"}}</th>
 | 
				
			||||||
 | 
											<th>{{.i18n.Tr "admin.users.full_name"}}</th>
 | 
				
			||||||
 | 
											<th>{{.i18n.Tr "email"}}</th>
 | 
				
			||||||
 | 
											<th>{{.i18n.Tr "admin.emails.primary"}}</th>
 | 
				
			||||||
 | 
											<th>{{.i18n.Tr "admin.emails.activated"}}</th>
 | 
				
			||||||
 | 
										</tr>
 | 
				
			||||||
 | 
									</thead>
 | 
				
			||||||
 | 
									<tbody>
 | 
				
			||||||
 | 
										{{range .Emails}}
 | 
				
			||||||
 | 
											<tr>
 | 
				
			||||||
 | 
												<td><a href="{{AppSubUrl}}/{{.Name}}">{{.Name}}</a></td>
 | 
				
			||||||
 | 
												<td><span class="text truncate">{{.FullName}}</span></td>
 | 
				
			||||||
 | 
												<td><span class="text email">{{.Email}}</span></td>
 | 
				
			||||||
 | 
												<td><i class="fa fa{{if .IsPrimary}}-check{{end}}-square-o"></i></td>
 | 
				
			||||||
 | 
												<td>
 | 
				
			||||||
 | 
													{{if .CanChange}}
 | 
				
			||||||
 | 
														<a class="link-email-action" href data-uid="{{.UID}}"
 | 
				
			||||||
 | 
															data-email="{{.Email}}"
 | 
				
			||||||
 | 
															data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
 | 
				
			||||||
 | 
															data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
 | 
				
			||||||
 | 
															<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
 | 
				
			||||||
 | 
														</a>
 | 
				
			||||||
 | 
													{{else}}
 | 
				
			||||||
 | 
														<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
 | 
				
			||||||
 | 
													{{end}}
 | 
				
			||||||
 | 
												</td>
 | 
				
			||||||
 | 
											</tr>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
 | 
									</tbody>
 | 
				
			||||||
 | 
								</table>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{{template "base/paginate" .}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="ui basic modal" id="change-email-modal">
 | 
				
			||||||
 | 
								<div class="ui icon header">
 | 
				
			||||||
 | 
									{{.i18n.Tr "admin.emails.change_email_header"}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="content center">
 | 
				
			||||||
 | 
									<p>{{.i18n.Tr "admin.emails.change_email_text"}}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/admin/emails/activate" method="post">
 | 
				
			||||||
 | 
										{{$.CsrfTokenHtml}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<input type="hidden" id="query-sort" name="sort" value="{{.SortType}}">
 | 
				
			||||||
 | 
										<input type="hidden" id="query-keyword" name="q" value="{{.Keyword}}">
 | 
				
			||||||
 | 
										<input type="hidden" id="query-primary" name="is_primary" value="{{.IsPrimary}}" required>
 | 
				
			||||||
 | 
										<input type="hidden" id="query-activated" name="is_activated" value="{{.IsActivated}}" required>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<input type="hidden" id="form-uid" name="uid" value="" required>
 | 
				
			||||||
 | 
										<input type="hidden" id="form-email" name="email" value="" required>
 | 
				
			||||||
 | 
										<input type="hidden" id="form-primary" name="primary" value="" required>
 | 
				
			||||||
 | 
										<input type="hidden" id="form-activate" name="activate" value="" required>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<div class="center actions">
 | 
				
			||||||
 | 
											<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
 | 
				
			||||||
 | 
											<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
@@ -8,6 +8,7 @@
 | 
				
			|||||||
			<li {{if .PageIsAdminRepositories}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/repos">{{.i18n.Tr "admin.repositories"}}</a></li>
 | 
								<li {{if .PageIsAdminRepositories}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/repos">{{.i18n.Tr "admin.repositories"}}</a></li>
 | 
				
			||||||
			<li {{if .PageIsAdminHooks}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/hooks">{{.i18n.Tr "admin.hooks"}}</a></li>
 | 
								<li {{if .PageIsAdminHooks}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/hooks">{{.i18n.Tr "admin.hooks"}}</a></li>
 | 
				
			||||||
			<li {{if .PageIsAdminAuthentications}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/auths">{{.i18n.Tr "admin.authentication"}}</a></li>
 | 
								<li {{if .PageIsAdminAuthentications}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/auths">{{.i18n.Tr "admin.authentication"}}</a></li>
 | 
				
			||||||
 | 
								<li {{if .PageIsAdminEmails}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/emails">{{.i18n.Tr "admin.emails"}}</a></li>
 | 
				
			||||||
			<li {{if .PageIsAdminConfig}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/config">{{.i18n.Tr "admin.config"}}</a></li>
 | 
								<li {{if .PageIsAdminConfig}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/config">{{.i18n.Tr "admin.config"}}</a></li>
 | 
				
			||||||
			<li {{if .PageIsAdminNotices}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/notices">{{.i18n.Tr "admin.notices"}}</a></li>
 | 
								<li {{if .PageIsAdminNotices}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/notices">{{.i18n.Tr "admin.notices"}}</a></li>
 | 
				
			||||||
			<li {{if .PageIsAdminMonitor}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/monitor">{{.i18n.Tr "admin.monitor"}}</a></li>
 | 
								<li {{if .PageIsAdminMonitor}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/monitor">{{.i18n.Tr "admin.monitor"}}</a></li>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,9 @@
 | 
				
			|||||||
	<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
 | 
						<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
 | 
				
			||||||
		{{.i18n.Tr "admin.authentication"}}
 | 
							{{.i18n.Tr "admin.authentication"}}
 | 
				
			||||||
	</a>
 | 
						</a>
 | 
				
			||||||
 | 
						<a class="{{if .PageIsAdminEmails}}active{{end}} item" href="{{AppSubUrl}}/admin/emails">
 | 
				
			||||||
 | 
							{{.i18n.Tr "admin.emails"}}
 | 
				
			||||||
 | 
						</a>
 | 
				
			||||||
	<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
 | 
						<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
 | 
				
			||||||
		{{.i18n.Tr "admin.config"}}
 | 
							{{.i18n.Tr "admin.config"}}
 | 
				
			||||||
	</a>
 | 
						</a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,7 +76,7 @@
 | 
				
			|||||||
									{{$.i18n.Tr "settings.delete_email"}}
 | 
														{{$.i18n.Tr "settings.delete_email"}}
 | 
				
			||||||
								</button>
 | 
													</button>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							{{if .IsActivated}}
 | 
												{{if .CanBePrimary}}
 | 
				
			||||||
								<div class="right floated content">
 | 
													<div class="right floated content">
 | 
				
			||||||
									<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
 | 
														<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
 | 
				
			||||||
										{{$.CsrfTokenHtml}}
 | 
															{{$.CsrfTokenHtml}}
 | 
				
			||||||
@@ -87,9 +87,30 @@
 | 
				
			|||||||
								</div>
 | 
													</div>
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
 | 
											{{if not .IsActivated}}
 | 
				
			||||||
 | 
												<div class="right floated content">
 | 
				
			||||||
 | 
													<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
 | 
				
			||||||
 | 
														{{$.CsrfTokenHtml}}
 | 
				
			||||||
 | 
														<input name="_method" type="hidden" value="SENDACTIVATION">
 | 
				
			||||||
 | 
														<input name="id" type="hidden" value="{{if .IsPrimary}}PRIMARY{{else}}}.ID{{end}}">
 | 
				
			||||||
 | 
														{{if $.ActivationsPending}}
 | 
				
			||||||
 | 
															<button disabled class="ui blue tiny button">{{$.i18n.Tr "settings.activations_pending"}}</button>
 | 
				
			||||||
 | 
														{{else}}
 | 
				
			||||||
 | 
															<button class="ui blue tiny button">{{$.i18n.Tr "settings.activate_email"}}</button>
 | 
				
			||||||
 | 
														{{end}}
 | 
				
			||||||
 | 
													</form>
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
											{{end}}
 | 
				
			||||||
						<div class="content">
 | 
											<div class="content">
 | 
				
			||||||
							<strong>{{.Email}}</strong>
 | 
												<strong>{{.Email}}</strong>
 | 
				
			||||||
							{{if .IsPrimary}}<span class="text red">{{$.i18n.Tr "settings.primary"}}</span>{{end}}
 | 
												{{if .IsPrimary}}
 | 
				
			||||||
 | 
													<div class="ui blue label">{{$.i18n.Tr "settings.primary"}}</div>
 | 
				
			||||||
 | 
												{{end}}
 | 
				
			||||||
 | 
												{{if .IsActivated}}
 | 
				
			||||||
 | 
													<div class="ui green label">{{$.i18n.Tr "settings.activated"}}</div>
 | 
				
			||||||
 | 
												{{else}}
 | 
				
			||||||
 | 
													<div class="ui label">{{$.i18n.Tr "settings.requires_activation"}}</div>
 | 
				
			||||||
 | 
												{{end}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
@@ -100,9 +121,9 @@
 | 
				
			|||||||
				{{.CsrfTokenHtml}}
 | 
									{{.CsrfTokenHtml}}
 | 
				
			||||||
				<div class="required field {{if .Err_Email}}error{{end}}">
 | 
									<div class="required field {{if .Err_Email}}error{{end}}">
 | 
				
			||||||
					<label for="email">{{.i18n.Tr "settings.add_new_email"}}</label>
 | 
										<label for="email">{{.i18n.Tr "settings.add_new_email"}}</label>
 | 
				
			||||||
					<input id="email" name="email" type="email" required>
 | 
										<input id="email" name="email" type="email" required {{if not .CanAddEmails}}disabled{{end}}>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<button class="ui green button">
 | 
									<button class="ui green button" {{if not .CanAddEmails}}disabled{{end}}>
 | 
				
			||||||
					{{.i18n.Tr "settings.add_email"}}
 | 
										{{.i18n.Tr "settings.add_email"}}
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
			</form>
 | 
								</form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2470,6 +2470,7 @@ $(document).ready(async () => {
 | 
				
			|||||||
  $('.delete-button').click(showDeletePopup);
 | 
					  $('.delete-button').click(showDeletePopup);
 | 
				
			||||||
  $('.add-all-button').click(showAddAllPopup);
 | 
					  $('.add-all-button').click(showAddAllPopup);
 | 
				
			||||||
  $('.link-action').click(linkAction);
 | 
					  $('.link-action').click(linkAction);
 | 
				
			||||||
 | 
					  $('.link-email-action').click(linkEmailAction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $('.delete-branch-button').click(showDeletePopup);
 | 
					  $('.delete-branch-button').click(showDeletePopup);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2749,6 +2750,17 @@ function linkAction() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function linkEmailAction(e) {
 | 
				
			||||||
 | 
					  const $this = $(this);
 | 
				
			||||||
 | 
					  $('#form-uid').val($this.data('uid'));
 | 
				
			||||||
 | 
					  $('#form-email').val($this.data('email'));
 | 
				
			||||||
 | 
					  $('#form-primary').val($this.data('primary'));
 | 
				
			||||||
 | 
					  $('#form-activate').val($this.data('activate'));
 | 
				
			||||||
 | 
					  $('#form-uid').val($this.data('uid'));
 | 
				
			||||||
 | 
					  $('#change-email-modal').modal('show');
 | 
				
			||||||
 | 
					  e.preventDefault();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initVueComponents() {
 | 
					function initVueComponents() {
 | 
				
			||||||
  const vueDelimeters = ['${', '}'];
 | 
					  const vueDelimeters = ['${', '}'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user