소스 검색

refactor(db): migrate methods off and delete deprecated methods from `user.go` (#7231)

Joe Chen 2 년 전
부모
커밋
5fb29db2db
46개의 변경된 파일659개의 추가작업 그리고 258개의 파일을 삭제
  1. 1 1
      internal/cmd/serv.go
  2. 4 3
      internal/context/auth.go
  3. 1 1
      internal/context/org.go
  4. 1 1
      internal/context/repo.go
  5. 1 1
      internal/context/user.go
  6. 2 1
      internal/db/comment.go
  7. 1 0
      internal/db/db.go
  8. 72 0
      internal/db/email_addresses.go
  9. 66 0
      internal/db/email_addresses_test.go
  10. 3 2
      internal/db/issue.go
  11. 2 1
      internal/db/issue_mail.go
  12. 1 1
      internal/db/models.go
  13. 5 4
      internal/db/org.go
  14. 2 1
      internal/db/org_team.go
  15. 3 3
      internal/db/orgs.go
  16. 6 6
      internal/db/orgs_test.go
  17. 5 5
      internal/db/repo.go
  18. 1 1
      internal/db/update.go
  19. 8 153
      internal/db/user.go
  20. 9 8
      internal/db/user_mail.go
  21. 24 3
      internal/db/users.go
  22. 63 0
      internal/db/users_test.go
  23. 1 1
      internal/db/wiki.go
  24. 9 3
      internal/route/admin/orgs.go
  25. 4 4
      internal/route/admin/users.go
  26. 2 2
      internal/route/api/v1/api.go
  27. 3 2
      internal/route/api/v1/convert/convert.go
  28. 1 1
      internal/route/api/v1/org/org.go
  29. 3 3
      internal/route/api/v1/repo/collaborators.go
  30. 2 2
      internal/route/api/v1/repo/commits.go
  31. 2 2
      internal/route/api/v1/repo/issue.go
  32. 5 5
      internal/route/api/v1/repo/repo.go
  33. 3 3
      internal/route/api/v1/user/email.go
  34. 1 1
      internal/route/api/v1/user/key.go
  35. 1 1
      internal/route/api/v1/user/user.go
  36. 14 9
      internal/route/home.go
  37. 243 0
      internal/route/lfs/mocks_test.go
  38. 1 1
      internal/route/org/members.go
  39. 1 1
      internal/route/org/teams.go
  40. 2 2
      internal/route/repo/pull.go
  41. 1 1
      internal/route/repo/repo.go
  42. 1 1
      internal/route/repo/setting.go
  43. 2 2
      internal/route/repo/webhook.go
  44. 69 8
      internal/route/user/auth.go
  45. 4 4
      internal/route/user/home.go
  46. 3 3
      internal/route/user/setting.go

+ 1 - 1
internal/cmd/serv.go

@@ -161,7 +161,7 @@ func runServ(c *cli.Context) error {
 	repoName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
 	repoName = strings.TrimSuffix(repoName, ".wiki")
 
-	owner, err := db.GetUserByName(ownerName)
+	owner, err := db.Users.GetByUsername(context.Background(), ownerName)
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			fail("Repository owner does not exist", "Unregistered owner: %s", ownerName)

+ 4 - 3
internal/context/auth.go

@@ -151,7 +151,8 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok
 		return 0, false
 	}
 	if id, ok := uid.(int64); ok {
-		if _, err := db.GetUserByID(id); err != nil {
+		_, err := db.Users.GetByID(c.Req.Context(), id)
+		if err != nil {
 			if !db.IsErrUserNotExist(err) {
 				log.Error("Failed to get user by ID: %v", err)
 			}
@@ -175,7 +176,7 @@ func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *db.User, is
 		if conf.Auth.EnableReverseProxyAuthentication {
 			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
 			if len(webAuthUser) > 0 {
-				user, err := db.GetUserByName(webAuthUser)
+				user, err := db.Users.GetByUsername(ctx.Req.Context(), webAuthUser)
 				if err != nil {
 					if !db.IsErrUserNotExist(err) {
 						log.Error("Failed to get user by name: %v", err)
@@ -223,7 +224,7 @@ func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *db.User, is
 		return nil, false, false
 	}
 
-	u, err := db.GetUserByID(uid)
+	u, err := db.Users.GetByID(ctx.Req.Context(), uid)
 	if err != nil {
 		log.Error("GetUserByID: %v", err)
 		return nil, false, false

+ 1 - 1
internal/context/org.go

@@ -47,7 +47,7 @@ func HandleOrgAssignment(c *Context, args ...bool) {
 	orgName := c.Params(":org")
 
 	var err error
-	c.Org.Organization, err = db.GetUserByName(orgName)
+	c.Org.Organization, err = db.Users.GetByUsername(c.Req.Context(), orgName)
 	if err != nil {
 		c.NotFoundOrError(err, "get organization by name")
 		return

+ 1 - 1
internal/context/repo.go

@@ -145,7 +145,7 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 		if c.IsLogged && c.User.LowerName == strings.ToLower(ownerName) {
 			owner = c.User
 		} else {
-			owner, err = db.GetUserByName(ownerName)
+			owner, err = db.Users.GetByUsername(c.Req.Context(), ownerName)
 			if err != nil {
 				c.NotFoundOrError(err, "get user by name")
 				return

+ 1 - 1
internal/context/user.go

@@ -19,7 +19,7 @@ type ParamsUser struct {
 // and injects it as *ParamsUser.
 func InjectParamsUser() macaron.Handler {
 	return func(c *Context) {
-		user, err := db.GetUserByName(c.Params(":username"))
+		user, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
 		if err != nil {
 			c.NotFoundOrError(err, "get user by name")
 			return

+ 2 - 1
internal/db/comment.go

@@ -5,6 +5,7 @@
 package db
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
@@ -94,7 +95,7 @@ func (c *Comment) AfterSet(colName string, _ xorm.Cell) {
 
 func (c *Comment) loadAttributes(e Engine) (err error) {
 	if c.Poster == nil {
-		c.Poster, err = GetUserByID(c.PosterID)
+		c.Poster, err = Users.GetByID(context.TODO(), c.PosterID)
 		if err != nil {
 			if IsErrUserNotExist(err) {
 				c.PosterID = -1

+ 1 - 0
internal/db/db.go

@@ -121,6 +121,7 @@ func Init(w logger.Writer) (*gorm.DB, error) {
 	// Initialize stores, sorted in alphabetical order.
 	AccessTokens = &accessTokens{DB: db}
 	Actions = NewActionsStore(db)
+	EmailAddresses = NewEmailAddressesStore(db)
 	Follows = NewFollowsStore(db)
 	LoginSources = &loginSources{DB: db, files: sourceFiles}
 	LFS = &lfs{DB: db}

+ 72 - 0
internal/db/email_addresses.go

@@ -0,0 +1,72 @@
+// Copyright 2022 The Gogs 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 db
+
+import (
+	"context"
+	"fmt"
+
+	"gorm.io/gorm"
+
+	"gogs.io/gogs/internal/errutil"
+)
+
+// EmailAddressesStore is the persistent interface for email addresses.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type EmailAddressesStore interface {
+	// GetByEmail returns the email address with given email. It may return
+	// unverified email addresses and returns ErrEmailNotExist when not found.
+	GetByEmail(ctx context.Context, email string) (*EmailAddress, error)
+}
+
+var EmailAddresses EmailAddressesStore
+
+var _ EmailAddressesStore = (*emailAddresses)(nil)
+
+type emailAddresses struct {
+	*gorm.DB
+}
+
+// NewEmailAddressesStore returns a persistent interface for email addresses
+// with given database connection.
+func NewEmailAddressesStore(db *gorm.DB) EmailAddressesStore {
+	return &emailAddresses{DB: db}
+}
+
+var _ errutil.NotFound = (*ErrEmailNotExist)(nil)
+
+type ErrEmailNotExist struct {
+	args errutil.Args
+}
+
+func IsErrEmailAddressNotExist(err error) bool {
+	_, ok := err.(ErrEmailNotExist)
+	return ok
+}
+
+func (err ErrEmailNotExist) Error() string {
+	return fmt.Sprintf("email address does not exist: %v", err.args)
+}
+
+func (ErrEmailNotExist) NotFound() bool {
+	return true
+}
+
+func (db *emailAddresses) GetByEmail(ctx context.Context, email string) (*EmailAddress, error) {
+	emailAddress := new(EmailAddress)
+	err := db.WithContext(ctx).Where("email = ?", email).First(emailAddress).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrEmailNotExist{
+				args: errutil.Args{
+					"email": email,
+				},
+			}
+		}
+		return nil, err
+	}
+	return emailAddress, nil
+}

+ 66 - 0
internal/db/email_addresses_test.go

@@ -0,0 +1,66 @@
+// Copyright 2022 The Gogs 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 db
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"gogs.io/gogs/internal/dbtest"
+	"gogs.io/gogs/internal/errutil"
+)
+
+func TestEmailAddresses(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
+	t.Parallel()
+
+	tables := []interface{}{new(EmailAddress)}
+	db := &emailAddresses{
+		DB: dbtest.NewDB(t, "emailAddresses", tables...),
+	}
+
+	for _, tc := range []struct {
+		name string
+		test func(t *testing.T, db *emailAddresses)
+	}{
+		{"GetByEmail", emailAddressesGetByEmail},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Cleanup(func() {
+				err := clearTables(t, db.DB, tables...)
+				require.NoError(t, err)
+			})
+			tc.test(t, db)
+		})
+		if t.Failed() {
+			break
+		}
+	}
+}
+
+func emailAddressesGetByEmail(t *testing.T, db *emailAddresses) {
+	ctx := context.Background()
+
+	const testEmail = "[email protected]"
+	_, err := db.GetByEmail(ctx, testEmail)
+	wantErr := ErrEmailNotExist{
+		args: errutil.Args{
+			"email": testEmail,
+		},
+	}
+	assert.Equal(t, wantErr, err)
+
+	// TODO: Use EmailAddresses.Create to replace SQL hack when the method is available.
+	err = db.Exec(`INSERT INTO email_address (uid, email) VALUES (1, ?)`, testEmail).Error
+	require.NoError(t, err)
+	got, err := db.GetByEmail(ctx, testEmail)
+	require.NoError(t, err)
+	assert.Equal(t, testEmail, got.Email)
+}

+ 3 - 2
internal/db/issue.go

@@ -5,6 +5,7 @@
 package db
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
@@ -391,7 +392,7 @@ func (issue *Issue) GetAssignee() (err error) {
 		return nil
 	}
 
-	issue.Assignee, err = GetUserByID(issue.AssigneeID)
+	issue.Assignee, err = Users.GetByID(context.TODO(), issue.AssigneeID)
 	if IsErrUserNotExist(err) {
 		return nil
 	}
@@ -597,7 +598,7 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
 		return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
 	}
 
-	issue.Assignee, err = GetUserByID(issue.AssigneeID)
+	issue.Assignee, err = Users.GetByID(context.TODO(), issue.AssigneeID)
 	if err != nil && !IsErrUserNotExist(err) {
 		log.Error("Failed to get user by ID: %v", err)
 		return nil

+ 2 - 1
internal/db/issue_mail.go

@@ -5,6 +5,7 @@
 package db
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/unknwon/com"
@@ -124,7 +125,7 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
 			continue
 		}
 
-		to, err := GetUserByID(watchers[i].UserID)
+		to, err := Users.GetByID(context.TODO(), watchers[i].UserID)
 		if err != nil {
 			return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
 		}

+ 1 - 1
internal/db/models.go

@@ -210,7 +210,7 @@ type Statistic struct {
 }
 
 func GetStatistic(ctx context.Context) (stats Statistic) {
-	stats.Counter.User = CountUsers()
+	stats.Counter.User = Users.Count(ctx)
 	stats.Counter.Org = CountOrganizations()
 	stats.Counter.PublicKey, _ = x.Count(new(PublicKey))
 	stats.Counter.Repo = CountRepositories(true)

+ 5 - 4
internal/db/org.go

@@ -15,6 +15,7 @@ import (
 	"xorm.io/xorm"
 
 	"gogs.io/gogs/internal/errutil"
+	"gogs.io/gogs/internal/repoutil"
 	"gogs.io/gogs/internal/userutil"
 )
 
@@ -72,7 +73,7 @@ func (org *User) GetMembers(limit int) error {
 
 	org.Members = make([]*User, len(ous))
 	for i, ou := range ous {
-		org.Members[i], err = GetUserByID(ou.Uid)
+		org.Members[i], err = Users.GetByID(context.TODO(), ou.Uid)
 		if err != nil {
 			return err
 		}
@@ -166,7 +167,7 @@ func CreateOrganization(org, owner *User) (err error) {
 		return fmt.Errorf("insert team-user relation: %v", err)
 	}
 
-	if err = os.MkdirAll(UserPath(org.Name), os.ModePerm); err != nil {
+	if err = os.MkdirAll(repoutil.UserPath(org.Name), os.ModePerm); err != nil {
 		return fmt.Errorf("create directory: %v", err)
 	}
 
@@ -366,11 +367,11 @@ func RemoveOrgUser(orgID, userID int64) error {
 		return nil
 	}
 
-	user, err := GetUserByID(userID)
+	user, err := Users.GetByID(context.TODO(), userID)
 	if err != nil {
 		return fmt.Errorf("GetUserByID [%d]: %v", userID, err)
 	}
-	org, err := GetUserByID(orgID)
+	org, err := Users.GetByID(context.TODO(), orgID)
 	if err != nil {
 		return fmt.Errorf("GetUserByID [%d]: %v", orgID, err)
 	}

+ 2 - 1
internal/db/org_team.go

@@ -5,6 +5,7 @@
 package db
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
@@ -417,7 +418,7 @@ func DeleteTeam(t *Team) error {
 	}
 
 	// Get organization.
-	org, err := GetUserByID(t.OrgID)
+	org, err := Users.GetByID(context.TODO(), t.OrgID)
 	if err != nil {
 		return err
 	}

+ 3 - 3
internal/db/orgs.go

@@ -18,7 +18,7 @@ import (
 // NOTE: All methods are sorted in alphabetical order.
 type OrgsStore interface {
 	// List returns a list of organizations filtered by options.
-	List(ctx context.Context, opts ListOrgOptions) ([]*Organization, error)
+	List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
 }
 
 var Orgs OrgsStore
@@ -35,14 +35,14 @@ func NewOrgsStore(db *gorm.DB) OrgsStore {
 	return &orgs{DB: db}
 }
 
-type ListOrgOptions struct {
+type ListOrgsOptions struct {
 	// Filter by the membership with the given user ID.
 	MemberID int64
 	// Whether to include private memberships.
 	IncludePrivateMembers bool
 }
 
-func (db *orgs) List(ctx context.Context, opts ListOrgOptions) ([]*Organization, error) {
+func (db *orgs) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) {
 	if opts.MemberID <= 0 {
 		return nil, errors.New("MemberID must be greater than 0")
 	}

+ 6 - 6
internal/db/orgs_test.go

@@ -58,14 +58,14 @@ func orgsList(t *testing.T, db *orgs) {
 	org1, err := usersStore.Create(ctx, "org1", "[email protected]", CreateUserOptions{})
 	require.NoError(t, err)
 	err = db.Exec(
-		dbutil.Quote("UPDATE %s SET %s = ? WHERE id = ?", "user", "type"),
+		dbutil.Quote("UPDATE %s SET type = ? WHERE id = ?", "user"),
 		UserTypeOrganization, org1.ID,
 	).Error
 	require.NoError(t, err)
 	org2, err := usersStore.Create(ctx, "org2", "[email protected]", CreateUserOptions{})
 	require.NoError(t, err)
 	err = db.Exec(
-		dbutil.Quote("UPDATE %s SET %s = ? WHERE id = ?", "user", "type"),
+		dbutil.Quote("UPDATE %s SET type = ? WHERE id = ?", "user"),
 		UserTypeOrganization, org2.ID,
 	).Error
 	require.NoError(t, err)
@@ -80,12 +80,12 @@ func orgsList(t *testing.T, db *orgs) {
 
 	tests := []struct {
 		name         string
-		opts         ListOrgOptions
+		opts         ListOrgsOptions
 		wantOrgNames []string
 	}{
 		{
 			name: "only public memberships for a user",
-			opts: ListOrgOptions{
+			opts: ListOrgsOptions{
 				MemberID:              alice.ID,
 				IncludePrivateMembers: false,
 			},
@@ -93,7 +93,7 @@ func orgsList(t *testing.T, db *orgs) {
 		},
 		{
 			name: "all memberships for a user",
-			opts: ListOrgOptions{
+			opts: ListOrgsOptions{
 				MemberID:              alice.ID,
 				IncludePrivateMembers: true,
 			},
@@ -101,7 +101,7 @@ func orgsList(t *testing.T, db *orgs) {
 		},
 		{
 			name: "no membership for a non-existent user",
-			opts: ListOrgOptions{
+			opts: ListOrgsOptions{
 				MemberID:              404,
 				IncludePrivateMembers: true,
 			},

+ 5 - 5
internal/db/repo.go

@@ -1310,12 +1310,12 @@ func FilterRepositoryWithIssues(repoIDs []int64) ([]int64, error) {
 //
 // Deprecated: Use repoutil.RepositoryPath instead.
 func RepoPath(userName, repoName string) string {
-	return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
+	return filepath.Join(repoutil.UserPath(userName), strings.ToLower(repoName)+".git")
 }
 
 // TransferOwnership transfers all corresponding setting from old user to new one.
 func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error {
-	newOwner, err := GetUserByName(newOwnerName)
+	newOwner, err := Users.GetByUsername(context.TODO(), newOwnerName)
 	if err != nil {
 		return fmt.Errorf("get new owner '%s': %v", newOwnerName, err)
 	}
@@ -1437,7 +1437,7 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
 	}
 
 	// Rename remote repository to new path and delete local copy.
-	if err = os.MkdirAll(UserPath(newOwner.Name), os.ModePerm); err != nil {
+	if err = os.MkdirAll(repoutil.UserPath(newOwner.Name), os.ModePerm); err != nil {
 		return err
 	}
 	if err = os.Rename(RepoPath(owner.Name, repo.Name), RepoPath(newOwner.Name, repo.Name)); err != nil {
@@ -1606,7 +1606,7 @@ func DeleteRepository(ownerID, repoID int64) error {
 	}
 
 	// In case is a organization.
-	org, err := GetUserByID(ownerID)
+	org, err := Users.GetByID(context.TODO(), ownerID)
 	if err != nil {
 		return err
 	}
@@ -1724,7 +1724,7 @@ func GetRepositoryByRef(ref string) (*Repository, error) {
 	}
 
 	userName, repoName := ref[:n], ref[n+1:]
-	user, err := GetUserByName(userName)
+	user, err := Users.GetByUsername(context.TODO(), userName)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
internal/db/update.go

@@ -73,7 +73,7 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
 		return fmt.Errorf("open repository: %v", err)
 	}
 
-	owner, err := GetUserByName(opts.RepoUserName)
+	owner, err := Users.GetByUsername(ctx, opts.RepoUserName)
 	if err != nil {
 		return fmt.Errorf("GetUserByName: %v", err)
 	}

+ 8 - 153
internal/db/user.go

@@ -6,11 +6,9 @@ package db
 
 import (
 	"context"
-	"encoding/hex"
 	"fmt"
 	_ "image/jpeg"
 	"os"
-	"path/filepath"
 	"strings"
 	"time"
 
@@ -23,6 +21,7 @@ import (
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/errutil"
+	"gogs.io/gogs/internal/repoutil"
 	"gogs.io/gogs/internal/tool"
 	"gogs.io/gogs/internal/userutil"
 )
@@ -58,77 +57,6 @@ func (u *User) getOrganizationCount(e Engine) (int64, error) {
 	return e.Where("uid=?", u.ID).Count(new(OrgUser))
 }
 
-func countUsers(e Engine) int64 {
-	count, _ := e.Where("type=0").Count(new(User))
-	return count
-}
-
-// CountUsers returns number of users.
-func CountUsers() int64 {
-	return countUsers(x)
-}
-
-// Users returns number of users in given page.
-func ListUsers(page, pageSize int) ([]*User, error) {
-	users := make([]*User, 0, pageSize)
-	return users, x.Limit(pageSize, (page-1)*pageSize).Where("type=0").Asc("id").Find(&users)
-}
-
-// parseUserFromCode returns user by username encoded in code.
-// It returns nil if code or username is invalid.
-func parseUserFromCode(code string) (user *User) {
-	if len(code) <= tool.TIME_LIMIT_CODE_LENGTH {
-		return nil
-	}
-
-	// Use tail hex username to query user
-	hexStr := code[tool.TIME_LIMIT_CODE_LENGTH:]
-	if b, err := hex.DecodeString(hexStr); err == nil {
-		if user, err = GetUserByName(string(b)); user != nil {
-			return user
-		} else if !IsErrUserNotExist(err) {
-			log.Error("Failed to get user by name %q: %v", string(b), err)
-		}
-	}
-
-	return nil
-}
-
-// verify active code when active account
-func VerifyUserActiveCode(code string) (user *User) {
-	minutes := conf.Auth.ActivateCodeLives
-
-	if user = parseUserFromCode(code); user != nil {
-		// time limit code
-		prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
-		data := com.ToStr(user.ID) + user.Email + user.LowerName + user.Password + user.Rands
-
-		if tool.VerifyTimeLimitCode(data, minutes, prefix) {
-			return user
-		}
-	}
-	return nil
-}
-
-// verify active code when active account
-func VerifyActiveEmailCode(code, email string) *EmailAddress {
-	minutes := conf.Auth.ActivateCodeLives
-
-	if user := parseUserFromCode(code); user != nil {
-		// time limit code
-		prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
-		data := com.ToStr(user.ID) + email + user.LowerName + user.Password + user.Rands
-
-		if tool.VerifyTimeLimitCode(data, minutes, prefix) {
-			emailAddress := &EmailAddress{Email: email}
-			if has, _ := x.Get(emailAddress); has {
-				return emailAddress
-			}
-		}
-	}
-	return nil
-}
-
 // ChangeUserName changes all corresponding setting from old user name to new one.
 func ChangeUserName(u *User, newUserName string) (err error) {
 	if err = isUsernameAllowed(newUserName); err != nil {
@@ -155,8 +83,8 @@ func ChangeUserName(u *User, newUserName string) (err error) {
 	}
 
 	// Rename or create user base directory
-	baseDir := UserPath(u.Name)
-	newBaseDir := UserPath(newUserName)
+	baseDir := repoutil.UserPath(u.Name)
+	newBaseDir := repoutil.UserPath(newUserName)
 	if com.IsExist(baseDir) {
 		return os.Rename(baseDir, newBaseDir)
 	}
@@ -270,7 +198,7 @@ func deleteUser(e *xorm.Session, u *User) error {
 		&Follow{FollowID: u.ID},
 		&Action{UserID: u.ID},
 		&IssueUser{UID: u.ID},
-		&EmailAddress{UID: u.ID},
+		&EmailAddress{UserID: u.ID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %v", err)
 	}
@@ -303,7 +231,7 @@ func deleteUser(e *xorm.Session, u *User) error {
 	// Note: There are something just cannot be roll back,
 	//	so just keep error logs of those operations.
 
-	_ = os.RemoveAll(UserPath(u.Name))
+	_ = os.RemoveAll(repoutil.UserPath(u.Name))
 	_ = os.Remove(userutil.CustomAvatarPath(u.ID))
 
 	return nil
@@ -351,13 +279,6 @@ func DeleteInactivateUsers() (err error) {
 	return err
 }
 
-// UserPath returns the path absolute path of user repositories.
-//
-// Deprecated: Use repoutil.UserPath instead.
-func UserPath(username string) string {
-	return filepath.Join(conf.Repository.Root, strings.ToLower(username))
-}
-
 func GetUserByKeyID(keyID int64) (*User, error) {
 	user := new(User)
 	has, err := x.SQL("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyID).Get(user)
@@ -380,12 +301,6 @@ func getUserByID(e Engine, id int64) (*User, error) {
 	return u, nil
 }
 
-// GetUserByID returns the user object by given ID if exists.
-// Deprecated: Use Users.GetByID instead.
-func GetUserByID(id int64) (*User, error) {
-	return getUserByID(x, id)
-}
-
 // GetAssigneeByID returns the user with read access of repository by given ID.
 func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
 	ctx := context.TODO()
@@ -400,27 +315,11 @@ func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
 	return Users.GetByID(ctx, userID)
 }
 
-// GetUserByName returns a user by given name.
-// Deprecated: Use Users.GetByUsername instead.
-func GetUserByName(name string) (*User, error) {
-	if name == "" {
-		return nil, ErrUserNotExist{args: map[string]interface{}{"name": name}}
-	}
-	u := &User{LowerName: strings.ToLower(name)}
-	has, err := x.Get(u)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrUserNotExist{args: map[string]interface{}{"name": name}}
-	}
-	return u, nil
-}
-
 // GetUserEmailsByNames returns a list of e-mails corresponds to names.
 func GetUserEmailsByNames(names []string) []string {
 	mails := make([]string, 0, len(names))
 	for _, name := range names {
-		u, err := GetUserByName(name)
+		u, err := Users.GetByUsername(context.TODO(), name)
 		if err != nil {
 			continue
 		}
@@ -431,19 +330,6 @@ func GetUserEmailsByNames(names []string) []string {
 	return mails
 }
 
-// GetUserIDsByNames returns a slice of ids corresponds to names.
-func GetUserIDsByNames(names []string) []int64 {
-	ids := make([]int64, 0, len(names))
-	for _, name := range names {
-		u, err := GetUserByName(name)
-		if err != nil {
-			continue
-		}
-		ids = append(ids, u.ID)
-	}
-	return ids
-}
-
 // UserCommit represents a commit with validation of user.
 type UserCommit struct {
 	User *User
@@ -452,7 +338,7 @@ type UserCommit struct {
 
 // ValidateCommitWithEmail checks if author's e-mail of commit is corresponding to a user.
 func ValidateCommitWithEmail(c *git.Commit) *User {
-	u, err := GetUserByEmail(c.Author.Email)
+	u, err := Users.GetByEmail(context.TODO(), c.Author.Email)
 	if err != nil {
 		return nil
 	}
@@ -466,7 +352,7 @@ func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
 	for i := range oldCommits {
 		var u *User
 		if v, ok := emails[oldCommits[i].Author.Email]; !ok {
-			u, _ = GetUserByEmail(oldCommits[i].Author.Email)
+			u, _ = Users.GetByEmail(context.TODO(), oldCommits[i].Author.Email)
 			emails[oldCommits[i].Author.Email] = u
 		} else {
 			u = v
@@ -480,37 +366,6 @@ func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
 	return newCommits
 }
 
-// GetUserByEmail returns the user object by given e-mail if exists.
-// Deprecated: Use Users.GetByEmail instead.
-func GetUserByEmail(email string) (*User, error) {
-	if email == "" {
-		return nil, ErrUserNotExist{args: map[string]interface{}{"email": email}}
-	}
-
-	email = strings.ToLower(email)
-	// First try to find the user by primary email
-	user := &User{Email: email}
-	has, err := x.Get(user)
-	if err != nil {
-		return nil, err
-	}
-	if has {
-		return user, nil
-	}
-
-	// Otherwise, check in alternative list for activated email addresses
-	emailAddress := &EmailAddress{Email: email, IsActivated: true}
-	has, err = x.Get(emailAddress)
-	if err != nil {
-		return nil, err
-	}
-	if has {
-		return GetUserByID(emailAddress.UID)
-	}
-
-	return nil, ErrUserNotExist{args: map[string]interface{}{"email": email}}
-}
-
 type SearchUserOptions struct {
 	Keyword  string
 	Type     UserType

+ 9 - 8
internal/db/user_mail.go

@@ -5,6 +5,7 @@
 package db
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
@@ -16,8 +17,8 @@ import (
 // EmailAddresses is the list of all email addresses of a user. Can contain the
 // primary email address, but is not obligatory.
 type EmailAddress struct {
-	ID          int64
-	UID         int64  `xorm:"INDEX NOT NULL" gorm:"index;not null"`
+	ID          int64  `gorm:"primaryKey"`
+	UserID      int64  `xorm:"uid INDEX NOT NULL" gorm:"column:uid;index;not null"`
 	Email       string `xorm:"UNIQUE NOT NULL" gorm:"unique;not null"`
 	IsActivated bool   `gorm:"not null;default:FALSE"`
 	IsPrimary   bool   `xorm:"-" gorm:"-" json:"-"`
@@ -30,7 +31,7 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
 		return nil, err
 	}
 
-	u, err := GetUserByID(uid)
+	u, err := Users.GetByID(context.TODO(), uid)
 	if err != nil {
 		return nil, err
 	}
@@ -119,7 +120,7 @@ func AddEmailAddresses(emails []*EmailAddress) error {
 }
 
 func (email *EmailAddress) Activate() error {
-	user, err := GetUserByID(email.UID)
+	user, err := Users.GetByID(context.TODO(), email.UserID)
 	if err != nil {
 		return err
 	}
@@ -170,7 +171,7 @@ func MakeEmailPrimary(userID int64, email *EmailAddress) error {
 		return errors.EmailNotFound{Email: email.Email}
 	}
 
-	if email.UID != userID {
+	if email.UserID != userID {
 		return errors.New("not the owner of the email")
 	}
 
@@ -178,12 +179,12 @@ func MakeEmailPrimary(userID int64, email *EmailAddress) error {
 		return errors.EmailNotVerified{Email: email.Email}
 	}
 
-	user := &User{ID: email.UID}
+	user := &User{ID: email.UserID}
 	has, err = x.Get(user)
 	if err != nil {
 		return err
 	} else if !has {
-		return ErrUserNotExist{args: map[string]interface{}{"userID": email.UID}}
+		return ErrUserNotExist{args: map[string]interface{}{"userID": email.UserID}}
 	}
 
 	// Make sure the former primary email doesn't disappear.
@@ -200,7 +201,7 @@ func MakeEmailPrimary(userID int64, email *EmailAddress) error {
 	}
 
 	if !has {
-		formerPrimaryEmail.UID = user.ID
+		formerPrimaryEmail.UserID = user.ID
 		formerPrimaryEmail.IsActivated = user.IsActive
 		if _, err = sess.Insert(formerPrimaryEmail); err != nil {
 			return err

+ 24 - 3
internal/db/users.go

@@ -45,6 +45,8 @@ type UsersStore interface {
 	// When the "loginSourceID" is positive, it tries to authenticate via given
 	// login source and creates a new user when not yet exists in the database.
 	Authenticate(ctx context.Context, username, password string, loginSourceID int64) (*User, error)
+	// Count returns the total number of users.
+	Count(ctx context.Context) int64
 	// Create creates a new user and persists to database. It returns
 	// ErrUserAlreadyExist when a user with same name already exists, or
 	// ErrEmailAlreadyUsed if the email has been used by another user.
@@ -65,6 +67,9 @@ type UsersStore interface {
 	HasForkedRepository(ctx context.Context, userID, repoID int64) bool
 	// IsUsernameUsed returns true if the given username has been used.
 	IsUsernameUsed(ctx context.Context, username string) bool
+	// List returns a list of users. Results are paginated by given page and page
+	// size, and sorted by primary key (id) in ascending order.
+	List(ctx context.Context, page, pageSize int) ([]*User, error)
 	// ListFollowers returns a list of users that are following the given user.
 	// Results are paginated by given page and page size, and sorted by the time of
 	// follow in descending order.
@@ -183,6 +188,12 @@ func (db *users) Authenticate(ctx context.Context, login, password string, login
 	)
 }
 
+func (db *users) Count(ctx context.Context) int64 {
+	var count int64
+	db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeIndividual).Count(&count)
+	return count
+}
+
 type CreateUserOptions struct {
 	FullName    string
 	Password    string
@@ -242,6 +253,7 @@ func (db *users) Create(ctx context.Context, username, email string, opts Create
 		}
 	}
 
+	email = strings.ToLower(email)
 	_, err = db.GetByEmail(ctx, email)
 	if err == nil {
 		return nil, ErrEmailAlreadyUsed{
@@ -315,11 +327,10 @@ func (ErrUserNotExist) NotFound() bool {
 }
 
 func (db *users) GetByEmail(ctx context.Context, email string) (*User, error) {
-	email = strings.ToLower(email)
-
 	if email == "" {
 		return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
 	}
+	email = strings.ToLower(email)
 
 	// First try to find the user by primary email
 	user := new(User)
@@ -346,7 +357,7 @@ func (db *users) GetByEmail(ctx context.Context, email string) (*User, error) {
 		return nil, err
 	}
 
-	return db.GetByID(ctx, emailAddress.UID)
+	return db.GetByID(ctx, emailAddress.UserID)
 }
 
 func (db *users) GetByID(ctx context.Context, id int64) (*User, error) {
@@ -390,6 +401,16 @@ func (db *users) IsUsernameUsed(ctx context.Context, username string) bool {
 		Error != gorm.ErrRecordNotFound
 }
 
+func (db *users) List(ctx context.Context, page, pageSize int) ([]*User, error) {
+	users := make([]*User, 0, pageSize)
+	return users, db.WithContext(ctx).
+		Where("type = ?", UserTypeIndividual).
+		Limit(pageSize).Offset((page - 1) * pageSize).
+		Order("id ASC").
+		Find(&users).
+		Error
+}
+
 func (db *users) ListFollowers(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
 	/*
 		Equivalent SQL for PostgreSQL:

+ 63 - 0
internal/db/users_test.go

@@ -18,6 +18,7 @@ import (
 
 	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/dbtest"
+	"gogs.io/gogs/internal/dbutil"
 	"gogs.io/gogs/internal/errutil"
 	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/userutil"
@@ -88,6 +89,7 @@ func TestUsers(t *testing.T) {
 		test func(t *testing.T, db *users)
 	}{
 		{"Authenticate", usersAuthenticate},
+		{"Count", usersCount},
 		{"Create", usersCreate},
 		{"DeleteCustomAvatar", usersDeleteCustomAvatar},
 		{"GetByEmail", usersGetByEmail},
@@ -95,6 +97,7 @@ func TestUsers(t *testing.T) {
 		{"GetByUsername", usersGetByUsername},
 		{"HasForkedRepository", usersHasForkedRepository},
 		{"IsUsernameUsed", usersIsUsernameUsed},
+		{"List", usersList},
 		{"ListFollowers", usersListFollowers},
 		{"ListFollowings", usersListFollowings},
 		{"UseCustomAvatar", usersUseCustomAvatar},
@@ -209,6 +212,31 @@ func usersAuthenticate(t *testing.T, db *users) {
 	})
 }
 
+func usersCount(t *testing.T, db *users) {
+	ctx := context.Background()
+
+	// Has no user initially
+	got := db.Count(ctx)
+	assert.Equal(t, int64(0), got)
+
+	_, err := db.Create(ctx, "alice", "[email protected]", CreateUserOptions{})
+	require.NoError(t, err)
+	got = db.Count(ctx)
+	assert.Equal(t, int64(1), got)
+
+	// Create an organization shouldn't count
+	// TODO: Use Orgs.Create to replace SQL hack when the method is available.
+	org1, err := db.Create(ctx, "org1", "[email protected]", CreateUserOptions{})
+	require.NoError(t, err)
+	err = db.Exec(
+		dbutil.Quote("UPDATE %s SET type = ? WHERE id = ?", "user"),
+		UserTypeOrganization, org1.ID,
+	).Error
+	require.NoError(t, err)
+	got = db.Count(ctx)
+	assert.Equal(t, int64(1), got)
+}
+
 func usersCreate(t *testing.T, db *users) {
 	ctx := context.Background()
 
@@ -420,6 +448,41 @@ func usersIsUsernameUsed(t *testing.T, db *users) {
 	assert.False(t, got)
 }
 
+func usersList(t *testing.T, db *users) {
+	ctx := context.Background()
+
+	alice, err := db.Create(ctx, "alice", "[email protected]", CreateUserOptions{})
+	require.NoError(t, err)
+	bob, err := db.Create(ctx, "bob", "[email protected]", CreateUserOptions{})
+	require.NoError(t, err)
+
+	// Create an organization shouldn't count
+	// TODO: Use Orgs.Create to replace SQL hack when the method is available.
+	org1, err := db.Create(ctx, "org1", "[email protected]", CreateUserOptions{})
+	require.NoError(t, err)
+	err = db.Exec(
+		dbutil.Quote("UPDATE %s SET type = ? WHERE id = ?", "user"),
+		UserTypeOrganization, org1.ID,
+	).Error
+	require.NoError(t, err)
+
+	got, err := db.List(ctx, 1, 1)
+	require.NoError(t, err)
+	require.Len(t, got, 1)
+	assert.Equal(t, alice.ID, got[0].ID)
+
+	got, err = db.List(ctx, 2, 1)
+	require.NoError(t, err)
+	require.Len(t, got, 1)
+	assert.Equal(t, bob.ID, got[0].ID)
+
+	got, err = db.List(ctx, 1, 3)
+	require.NoError(t, err)
+	require.Len(t, got, 2)
+	assert.Equal(t, alice.ID, got[0].ID)
+	assert.Equal(t, bob.ID, got[1].ID)
+}
+
 func usersListFollowers(t *testing.T, db *users) {
 	ctx := context.Background()
 

+ 1 - 1
internal/db/wiki.go

@@ -47,7 +47,7 @@ func (repo *Repository) WikiCloneLink() (cl *repoutil.CloneLink) {
 
 // WikiPath returns wiki data path by given user and repository name.
 func WikiPath(userName, repoName string) string {
-	return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".wiki.git")
+	return filepath.Join(repoutil.UserPath(userName), strings.ToLower(repoName)+".wiki.git")
 }
 
 func (repo *Repository) WikiPath() string {

+ 9 - 3
internal/route/admin/orgs.go

@@ -5,6 +5,8 @@
 package admin
 
 import (
+	gocontext "context"
+
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
@@ -21,9 +23,13 @@ func Organizations(c *context.Context) {
 	c.Data["PageIsAdminOrganizations"] = true
 
 	route.RenderUserSearch(c, &route.UserSearchOptions{
-		Type:     db.UserTypeOrganization,
-		Counter:  db.CountOrganizations,
-		Ranger:   db.Organizations,
+		Type: db.UserTypeOrganization,
+		Counter: func(gocontext.Context) int64 {
+			return db.CountOrganizations()
+		},
+		Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
+			return db.Organizations(page, pageSize)
+		},
 		PageSize: conf.UI.Admin.OrgPagingNum,
 		OrderBy:  "id ASC",
 		TplName:  ORGS,

+ 4 - 4
internal/route/admin/users.go

@@ -33,8 +33,8 @@ func Users(c *context.Context) {
 
 	route.RenderUserSearch(c, &route.UserSearchOptions{
 		Type:     db.UserTypeIndividual,
-		Counter:  db.CountUsers,
-		Ranger:   db.ListUsers,
+		Counter:  db.Users.Count,
+		Ranger:   db.Users.List,
 		PageSize: conf.UI.Admin.UserPagingNum,
 		OrderBy:  "id ASC",
 		TplName:  USERS,
@@ -119,7 +119,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 }
 
 func prepareUserInfo(c *context.Context) *db.User {
-	u, err := db.GetUserByID(c.ParamsInt64(":userid"))
+	u, err := db.Users.GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return nil
@@ -223,7 +223,7 @@ func EditUserPost(c *context.Context, f form.AdminEditUser) {
 }
 
 func DeleteUser(c *context.Context) {
-	u, err := db.GetUserByID(c.ParamsInt64(":userid"))
+	u, err := db.Users.GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return

+ 2 - 2
internal/route/api/v1/api.go

@@ -37,7 +37,7 @@ func repoAssignment() macaron.Handler {
 		if c.IsLogged && c.User.LowerName == strings.ToLower(username) {
 			owner = c.User
 		} else {
-			owner, err = db.GetUserByName(username)
+			owner, err = db.Users.GetByUsername(c.Req.Context(), username)
 			if err != nil {
 				c.NotFoundOrError(err, "get user by name")
 				return
@@ -91,7 +91,7 @@ func orgAssignment(args ...bool) macaron.Handler {
 
 		var err error
 		if assignOrg {
-			c.Org.Organization, err = db.GetUserByName(c.Params(":orgname"))
+			c.Org.Organization, err = db.Users.GetByUsername(c.Req.Context(), c.Params(":orgname"))
 			if err != nil {
 				c.NotFoundOrError(err, "get organization by name")
 				return

+ 3 - 2
internal/route/api/v1/convert/convert.go

@@ -5,6 +5,7 @@
 package convert
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/unknwon/com"
@@ -44,12 +45,12 @@ func ToTag(b *db.Tag, c *git.Commit) *Tag {
 
 func ToCommit(c *git.Commit) *api.PayloadCommit {
 	authorUsername := ""
-	author, err := db.GetUserByEmail(c.Author.Email)
+	author, err := db.Users.GetByEmail(context.TODO(), c.Author.Email)
 	if err == nil {
 		authorUsername = author.Name
 	}
 	committerUsername := ""
-	committer, err := db.GetUserByEmail(c.Committer.Email)
+	committer, err := db.Users.GetByEmail(context.TODO(), c.Committer.Email)
 	if err == nil {
 		committerUsername = committer.Name
 	}

+ 1 - 1
internal/route/api/v1/org/org.go

@@ -45,7 +45,7 @@ func CreateOrgForUser(c *context.APIContext, apiForm api.CreateOrgOption, user *
 func listUserOrgs(c *context.APIContext, u *db.User, all bool) {
 	orgs, err := db.Orgs.List(
 		c.Req.Context(),
-		db.ListOrgOptions{
+		db.ListOrgsOptions{
 			MemberID:              u.ID,
 			IncludePrivateMembers: all,
 		},

+ 3 - 3
internal/route/api/v1/repo/collaborators.go

@@ -28,7 +28,7 @@ func ListCollaborators(c *context.APIContext) {
 }
 
 func AddCollaborator(c *context.APIContext, form api.AddCollaboratorOption) {
-	collaborator, err := db.GetUserByName(c.Params(":collaborator"))
+	collaborator, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.Status(http.StatusUnprocessableEntity)
@@ -54,7 +54,7 @@ func AddCollaborator(c *context.APIContext, form api.AddCollaboratorOption) {
 }
 
 func IsCollaborator(c *context.APIContext) {
-	collaborator, err := db.GetUserByName(c.Params(":collaborator"))
+	collaborator, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.Status(http.StatusUnprocessableEntity)
@@ -72,7 +72,7 @@ func IsCollaborator(c *context.APIContext) {
 }
 
 func DeleteCollaborator(c *context.APIContext) {
-	collaborator, err := db.GetUserByName(c.Params(":collaborator"))
+	collaborator, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.Status(http.StatusUnprocessableEntity)

+ 2 - 2
internal/route/api/v1/repo/commits.go

@@ -120,7 +120,7 @@ func GetReferenceSHA(c *context.APIContext) {
 func gitCommitToAPICommit(commit *git.Commit, c *context.APIContext) (*api.Commit, error) {
 	// Retrieve author and committer information
 	var apiAuthor, apiCommitter *api.User
-	author, err := db.GetUserByEmail(commit.Author.Email)
+	author, err := db.Users.GetByEmail(c.Req.Context(), commit.Author.Email)
 	if err != nil && !db.IsErrUserNotExist(err) {
 		return nil, err
 	} else if err == nil {
@@ -131,7 +131,7 @@ func gitCommitToAPICommit(commit *git.Commit, c *context.APIContext) (*api.Commi
 	if commit.Committer.Email == commit.Author.Email {
 		apiCommitter = apiAuthor
 	} else {
-		committer, err := db.GetUserByEmail(commit.Committer.Email)
+		committer, err := db.Users.GetByEmail(c.Req.Context(), commit.Committer.Email)
 		if err != nil && !db.IsErrUserNotExist(err) {
 			return nil, err
 		} else if err == nil {

+ 2 - 2
internal/route/api/v1/repo/issue.go

@@ -83,7 +83,7 @@ func CreateIssue(c *context.APIContext, form api.CreateIssueOption) {
 
 	if c.Repo.IsWriter() {
 		if len(form.Assignee) > 0 {
-			assignee, err := db.GetUserByName(form.Assignee)
+			assignee, err := db.Users.GetByUsername(c.Req.Context(), form.Assignee)
 			if err != nil {
 				if db.IsErrUserNotExist(err) {
 					c.ErrorStatus(http.StatusUnprocessableEntity, fmt.Errorf("assignee does not exist: [name: %s]", form.Assignee))
@@ -145,7 +145,7 @@ func EditIssue(c *context.APIContext, form api.EditIssueOption) {
 		if *form.Assignee == "" {
 			issue.AssigneeID = 0
 		} else {
-			assignee, err := db.GetUserByName(*form.Assignee)
+			assignee, err := db.Users.GetByUsername(c.Req.Context(), *form.Assignee)
 			if err != nil {
 				if db.IsErrUserNotExist(err) {
 					c.ErrorStatus(http.StatusUnprocessableEntity, fmt.Errorf("assignee does not exist: [name: %s]", *form.Assignee))

+ 5 - 5
internal/route/api/v1/repo/repo.go

@@ -32,7 +32,7 @@ func Search(c *context.APIContext) {
 		if c.User.ID == opts.OwnerID {
 			opts.Private = true
 		} else {
-			u, err := db.GetUserByID(opts.OwnerID)
+			u, err := db.Users.GetByID(c.Req.Context(), opts.OwnerID)
 			if err != nil {
 				c.JSON(http.StatusInternalServerError, map[string]interface{}{
 					"ok":    false,
@@ -77,7 +77,7 @@ func Search(c *context.APIContext) {
 }
 
 func listUserRepositories(c *context.APIContext, username string) {
-	user, err := db.GetUserByName(username)
+	user, err := db.Users.GetByUsername(c.Req.Context(), username)
 	if err != nil {
 		c.NotFoundOrError(err, "get user by name")
 		return
@@ -209,7 +209,7 @@ func Migrate(c *context.APIContext, f form.MigrateRepo) {
 	// Not equal means context user is an organization,
 	// or is another user/organization if current user is admin.
 	if f.Uid != ctxUser.ID {
-		org, err := db.GetUserByID(f.Uid)
+		org, err := db.Users.GetByID(c.Req.Context(), f.Uid)
 		if err != nil {
 			if db.IsErrUserNotExist(err) {
 				c.ErrorStatus(http.StatusUnprocessableEntity, err)
@@ -287,7 +287,7 @@ func Migrate(c *context.APIContext, f form.MigrateRepo) {
 
 // FIXME: inject in the handler chain
 func parseOwnerAndRepo(c *context.APIContext) (*db.User, *db.Repository) {
-	owner, err := db.GetUserByName(c.Params(":username"))
+	owner, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.ErrorStatus(http.StatusUnprocessableEntity, err)
@@ -453,7 +453,7 @@ func Releases(c *context.APIContext) {
 	}
 	apiReleases := make([]*api.Release, 0, len(releases))
 	for _, r := range releases {
-		publisher, err := db.GetUserByID(r.PublisherID)
+		publisher, err := db.Users.GetByID(c.Req.Context(), r.PublisherID)
 		if err != nil {
 			c.Error(err, "get release publisher")
 			return

+ 3 - 3
internal/route/api/v1/user/email.go

@@ -38,7 +38,7 @@ func AddEmail(c *context.APIContext, form api.CreateEmailOption) {
 	emails := make([]*db.EmailAddress, len(form.Emails))
 	for i := range form.Emails {
 		emails[i] = &db.EmailAddress{
-			UID:         c.User.ID,
+			UserID:      c.User.ID,
 			Email:       form.Emails[i],
 			IsActivated: !conf.Auth.RequireEmailConfirmation,
 		}
@@ -69,8 +69,8 @@ func DeleteEmail(c *context.APIContext, form api.CreateEmailOption) {
 	emails := make([]*db.EmailAddress, len(form.Emails))
 	for i := range form.Emails {
 		emails[i] = &db.EmailAddress{
-			UID:   c.User.ID,
-			Email: form.Emails[i],
+			UserID: c.User.ID,
+			Email:  form.Emails[i],
 		}
 	}
 

+ 1 - 1
internal/route/api/v1/user/key.go

@@ -18,7 +18,7 @@ import (
 )
 
 func GetUserByParamsName(c *context.APIContext, name string) *db.User {
-	user, err := db.GetUserByName(c.Params(name))
+	user, err := db.Users.GetByUsername(c.Req.Context(), c.Params(name))
 	if err != nil {
 		c.NotFoundOrError(err, "get user by name")
 		return nil

+ 1 - 1
internal/route/api/v1/user/user.go

@@ -55,7 +55,7 @@ func Search(c *context.APIContext) {
 }
 
 func GetInfo(c *context.APIContext) {
-	u, err := db.GetUserByName(c.Params(":username"))
+	u, err := db.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
 	if err != nil {
 		c.NotFoundOrError(err, "get user by name")
 		return

+ 14 - 9
internal/route/home.go

@@ -5,6 +5,7 @@
 package route
 
 import (
+	gocontext "context"
 	"fmt"
 	"net/http"
 
@@ -84,8 +85,8 @@ func ExploreRepos(c *context.Context) {
 
 type UserSearchOptions struct {
 	Type     db.UserType
-	Counter  func() int64
-	Ranger   func(int, int) ([]*db.User, error)
+	Counter  func(ctx gocontext.Context) int64
+	Ranger   func(ctx gocontext.Context, page, pageSize int) ([]*db.User, error)
 	PageSize int
 	OrderBy  string
 	TplName  string
@@ -105,12 +106,12 @@ func RenderUserSearch(c *context.Context, opts *UserSearchOptions) {
 
 	keyword := c.Query("q")
 	if keyword == "" {
-		users, err = opts.Ranger(page, opts.PageSize)
+		users, err = opts.Ranger(c.Req.Context(), page, opts.PageSize)
 		if err != nil {
 			c.Error(err, "ranger")
 			return
 		}
-		count = opts.Counter()
+		count = opts.Counter(c.Req.Context())
 	} else {
 		users, count, err = db.SearchUserByName(&db.SearchUserOptions{
 			Keyword:  keyword,
@@ -139,8 +140,8 @@ func ExploreUsers(c *context.Context) {
 
 	RenderUserSearch(c, &UserSearchOptions{
 		Type:     db.UserTypeIndividual,
-		Counter:  db.CountUsers,
-		Ranger:   db.ListUsers,
+		Counter:  db.Users.Count,
+		Ranger:   db.Users.List,
 		PageSize: conf.UI.ExplorePagingNum,
 		OrderBy:  "updated_unix DESC",
 		TplName:  EXPLORE_USERS,
@@ -153,9 +154,13 @@ func ExploreOrganizations(c *context.Context) {
 	c.Data["PageIsExploreOrganizations"] = true
 
 	RenderUserSearch(c, &UserSearchOptions{
-		Type:     db.UserTypeOrganization,
-		Counter:  db.CountOrganizations,
-		Ranger:   db.Organizations,
+		Type: db.UserTypeOrganization,
+		Counter: func(gocontext.Context) int64 {
+			return db.CountOrganizations()
+		},
+		Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
+			return db.Organizations(page, pageSize)
+		},
 		PageSize: conf.UI.ExplorePagingNum,
 		OrderBy:  "updated_unix DESC",
 		TplName:  EXPLORE_ORGANIZATIONS,

+ 243 - 0
internal/route/lfs/mocks_test.go

@@ -2295,6 +2295,9 @@ type MockUsersStore struct {
 	// AuthenticateFunc is an instance of a mock function object controlling
 	// the behavior of the method Authenticate.
 	AuthenticateFunc *UsersStoreAuthenticateFunc
+	// CountFunc is an instance of a mock function object controlling the
+	// behavior of the method Count.
+	CountFunc *UsersStoreCountFunc
 	// CreateFunc is an instance of a mock function object controlling the
 	// behavior of the method Create.
 	CreateFunc *UsersStoreCreateFunc
@@ -2316,6 +2319,9 @@ type MockUsersStore struct {
 	// IsUsernameUsedFunc is an instance of a mock function object
 	// controlling the behavior of the method IsUsernameUsed.
 	IsUsernameUsedFunc *UsersStoreIsUsernameUsedFunc
+	// ListFunc is an instance of a mock function object controlling the
+	// behavior of the method List.
+	ListFunc *UsersStoreListFunc
 	// ListFollowersFunc is an instance of a mock function object
 	// controlling the behavior of the method ListFollowers.
 	ListFollowersFunc *UsersStoreListFollowersFunc
@@ -2336,6 +2342,11 @@ func NewMockUsersStore() *MockUsersStore {
 				return
 			},
 		},
+		CountFunc: &UsersStoreCountFunc{
+			defaultHook: func(context.Context) (r0 int64) {
+				return
+			},
+		},
 		CreateFunc: &UsersStoreCreateFunc{
 			defaultHook: func(context.Context, string, string, db.CreateUserOptions) (r0 *db.User, r1 error) {
 				return
@@ -2371,6 +2382,11 @@ func NewMockUsersStore() *MockUsersStore {
 				return
 			},
 		},
+		ListFunc: &UsersStoreListFunc{
+			defaultHook: func(context.Context, int, int) (r0 []*db.User, r1 error) {
+				return
+			},
+		},
 		ListFollowersFunc: &UsersStoreListFollowersFunc{
 			defaultHook: func(context.Context, int64, int, int) (r0 []*db.User, r1 error) {
 				return
@@ -2398,6 +2414,11 @@ func NewStrictMockUsersStore() *MockUsersStore {
 				panic("unexpected invocation of MockUsersStore.Authenticate")
 			},
 		},
+		CountFunc: &UsersStoreCountFunc{
+			defaultHook: func(context.Context) int64 {
+				panic("unexpected invocation of MockUsersStore.Count")
+			},
+		},
 		CreateFunc: &UsersStoreCreateFunc{
 			defaultHook: func(context.Context, string, string, db.CreateUserOptions) (*db.User, error) {
 				panic("unexpected invocation of MockUsersStore.Create")
@@ -2433,6 +2454,11 @@ func NewStrictMockUsersStore() *MockUsersStore {
 				panic("unexpected invocation of MockUsersStore.IsUsernameUsed")
 			},
 		},
+		ListFunc: &UsersStoreListFunc{
+			defaultHook: func(context.Context, int, int) ([]*db.User, error) {
+				panic("unexpected invocation of MockUsersStore.List")
+			},
+		},
 		ListFollowersFunc: &UsersStoreListFollowersFunc{
 			defaultHook: func(context.Context, int64, int, int) ([]*db.User, error) {
 				panic("unexpected invocation of MockUsersStore.ListFollowers")
@@ -2458,6 +2484,9 @@ func NewMockUsersStoreFrom(i db.UsersStore) *MockUsersStore {
 		AuthenticateFunc: &UsersStoreAuthenticateFunc{
 			defaultHook: i.Authenticate,
 		},
+		CountFunc: &UsersStoreCountFunc{
+			defaultHook: i.Count,
+		},
 		CreateFunc: &UsersStoreCreateFunc{
 			defaultHook: i.Create,
 		},
@@ -2479,6 +2508,9 @@ func NewMockUsersStoreFrom(i db.UsersStore) *MockUsersStore {
 		IsUsernameUsedFunc: &UsersStoreIsUsernameUsedFunc{
 			defaultHook: i.IsUsernameUsed,
 		},
+		ListFunc: &UsersStoreListFunc{
+			defaultHook: i.List,
+		},
 		ListFollowersFunc: &UsersStoreListFollowersFunc{
 			defaultHook: i.ListFollowers,
 		},
@@ -2605,6 +2637,107 @@ func (c UsersStoreAuthenticateFuncCall) Results() []interface{} {
 	return []interface{}{c.Result0, c.Result1}
 }
 
+// UsersStoreCountFunc describes the behavior when the Count method of the
+// parent MockUsersStore instance is invoked.
+type UsersStoreCountFunc struct {
+	defaultHook func(context.Context) int64
+	hooks       []func(context.Context) int64
+	history     []UsersStoreCountFuncCall
+	mutex       sync.Mutex
+}
+
+// Count delegates to the next hook function in the queue and stores the
+// parameter and result values of this invocation.
+func (m *MockUsersStore) Count(v0 context.Context) int64 {
+	r0 := m.CountFunc.nextHook()(v0)
+	m.CountFunc.appendCall(UsersStoreCountFuncCall{v0, r0})
+	return r0
+}
+
+// SetDefaultHook sets function that is called when the Count method of the
+// parent MockUsersStore instance is invoked and the hook queue is empty.
+func (f *UsersStoreCountFunc) SetDefaultHook(hook func(context.Context) int64) {
+	f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// Count method of the parent MockUsersStore instance invokes the hook at
+// the front of the queue and discards it. After the queue is empty, the
+// default hook function is invoked for any future action.
+func (f *UsersStoreCountFunc) PushHook(hook func(context.Context) int64) {
+	f.mutex.Lock()
+	f.hooks = append(f.hooks, hook)
+	f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *UsersStoreCountFunc) SetDefaultReturn(r0 int64) {
+	f.SetDefaultHook(func(context.Context) int64 {
+		return r0
+	})
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *UsersStoreCountFunc) PushReturn(r0 int64) {
+	f.PushHook(func(context.Context) int64 {
+		return r0
+	})
+}
+
+func (f *UsersStoreCountFunc) nextHook() func(context.Context) int64 {
+	f.mutex.Lock()
+	defer f.mutex.Unlock()
+
+	if len(f.hooks) == 0 {
+		return f.defaultHook
+	}
+
+	hook := f.hooks[0]
+	f.hooks = f.hooks[1:]
+	return hook
+}
+
+func (f *UsersStoreCountFunc) appendCall(r0 UsersStoreCountFuncCall) {
+	f.mutex.Lock()
+	f.history = append(f.history, r0)
+	f.mutex.Unlock()
+}
+
+// History returns a sequence of UsersStoreCountFuncCall objects describing
+// the invocations of this function.
+func (f *UsersStoreCountFunc) History() []UsersStoreCountFuncCall {
+	f.mutex.Lock()
+	history := make([]UsersStoreCountFuncCall, len(f.history))
+	copy(history, f.history)
+	f.mutex.Unlock()
+
+	return history
+}
+
+// UsersStoreCountFuncCall is an object that describes an invocation of
+// method Count on an instance of MockUsersStore.
+type UsersStoreCountFuncCall struct {
+	// Arg0 is the value of the 1st argument passed to this method
+	// invocation.
+	Arg0 context.Context
+	// Result0 is the value of the 1st result returned from this method
+	// invocation.
+	Result0 int64
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c UsersStoreCountFuncCall) Args() []interface{} {
+	return []interface{}{c.Arg0}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c UsersStoreCountFuncCall) Results() []interface{} {
+	return []interface{}{c.Result0}
+}
+
 // UsersStoreCreateFunc describes the behavior when the Create method of the
 // parent MockUsersStore instance is invoked.
 type UsersStoreCreateFunc struct {
@@ -3363,6 +3496,116 @@ func (c UsersStoreIsUsernameUsedFuncCall) Results() []interface{} {
 	return []interface{}{c.Result0}
 }
 
+// UsersStoreListFunc describes the behavior when the List method of the
+// parent MockUsersStore instance is invoked.
+type UsersStoreListFunc struct {
+	defaultHook func(context.Context, int, int) ([]*db.User, error)
+	hooks       []func(context.Context, int, int) ([]*db.User, error)
+	history     []UsersStoreListFuncCall
+	mutex       sync.Mutex
+}
+
+// List delegates to the next hook function in the queue and stores the
+// parameter and result values of this invocation.
+func (m *MockUsersStore) List(v0 context.Context, v1 int, v2 int) ([]*db.User, error) {
+	r0, r1 := m.ListFunc.nextHook()(v0, v1, v2)
+	m.ListFunc.appendCall(UsersStoreListFuncCall{v0, v1, v2, r0, r1})
+	return r0, r1
+}
+
+// SetDefaultHook sets function that is called when the List method of the
+// parent MockUsersStore instance is invoked and the hook queue is empty.
+func (f *UsersStoreListFunc) SetDefaultHook(hook func(context.Context, int, int) ([]*db.User, error)) {
+	f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// List method of the parent MockUsersStore instance invokes the hook at the
+// front of the queue and discards it. After the queue is empty, the default
+// hook function is invoked for any future action.
+func (f *UsersStoreListFunc) PushHook(hook func(context.Context, int, int) ([]*db.User, error)) {
+	f.mutex.Lock()
+	f.hooks = append(f.hooks, hook)
+	f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *UsersStoreListFunc) SetDefaultReturn(r0 []*db.User, r1 error) {
+	f.SetDefaultHook(func(context.Context, int, int) ([]*db.User, error) {
+		return r0, r1
+	})
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *UsersStoreListFunc) PushReturn(r0 []*db.User, r1 error) {
+	f.PushHook(func(context.Context, int, int) ([]*db.User, error) {
+		return r0, r1
+	})
+}
+
+func (f *UsersStoreListFunc) nextHook() func(context.Context, int, int) ([]*db.User, error) {
+	f.mutex.Lock()
+	defer f.mutex.Unlock()
+
+	if len(f.hooks) == 0 {
+		return f.defaultHook
+	}
+
+	hook := f.hooks[0]
+	f.hooks = f.hooks[1:]
+	return hook
+}
+
+func (f *UsersStoreListFunc) appendCall(r0 UsersStoreListFuncCall) {
+	f.mutex.Lock()
+	f.history = append(f.history, r0)
+	f.mutex.Unlock()
+}
+
+// History returns a sequence of UsersStoreListFuncCall objects describing
+// the invocations of this function.
+func (f *UsersStoreListFunc) History() []UsersStoreListFuncCall {
+	f.mutex.Lock()
+	history := make([]UsersStoreListFuncCall, len(f.history))
+	copy(history, f.history)
+	f.mutex.Unlock()
+
+	return history
+}
+
+// UsersStoreListFuncCall is an object that describes an invocation of
+// method List on an instance of MockUsersStore.
+type UsersStoreListFuncCall struct {
+	// Arg0 is the value of the 1st argument passed to this method
+	// invocation.
+	Arg0 context.Context
+	// Arg1 is the value of the 2nd argument passed to this method
+	// invocation.
+	Arg1 int
+	// Arg2 is the value of the 3rd argument passed to this method
+	// invocation.
+	Arg2 int
+	// Result0 is the value of the 1st result returned from this method
+	// invocation.
+	Result0 []*db.User
+	// Result1 is the value of the 2nd result returned from this method
+	// invocation.
+	Result1 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c UsersStoreListFuncCall) Args() []interface{} {
+	return []interface{}{c.Arg0, c.Arg1, c.Arg2}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c UsersStoreListFuncCall) Results() []interface{} {
+	return []interface{}{c.Result0, c.Result1}
+}
+
 // UsersStoreListFollowersFunc describes the behavior when the ListFollowers
 // method of the parent MockUsersStore instance is invoked.
 type UsersStoreListFollowersFunc struct {

+ 1 - 1
internal/route/org/members.go

@@ -97,7 +97,7 @@ func Invitation(c *context.Context) {
 
 	if c.Req.Method == "POST" {
 		uname := c.Query("uname")
-		u, err := db.GetUserByName(uname)
+		u, err := db.Users.GetByUsername(c.Req.Context(), uname)
 		if err != nil {
 			if db.IsErrUserNotExist(err) {
 				c.Flash.Error(c.Tr("form.user_not_exist"))

+ 1 - 1
internal/route/org/teams.go

@@ -71,7 +71,7 @@ func TeamsAction(c *context.Context) {
 		}
 		uname := c.Query("uname")
 		var u *db.User
-		u, err = db.GetUserByName(uname)
+		u, err = db.Users.GetByUsername(c.Req.Context(), uname)
 		if err != nil {
 			if db.IsErrUserNotExist(err) {
 				c.Flash.Error(c.Tr("form.user_not_exist"))

+ 2 - 2
internal/route/repo/pull.go

@@ -71,7 +71,7 @@ func parseBaseRepository(c *context.Context) *db.Repository {
 
 	orgs, err := db.Orgs.List(
 		c.Req.Context(),
-		db.ListOrgOptions{
+		db.ListOrgsOptions{
 			MemberID:              c.User.ID,
 			IncludePrivateMembers: true,
 		},
@@ -466,7 +466,7 @@ func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Reposi
 		headBranch = headInfos[0]
 
 	} else if len(headInfos) == 2 {
-		headUser, err = db.GetUserByName(headInfos[0])
+		headUser, err = db.Users.GetByUsername(c.Req.Context(), headInfos[0])
 		if err != nil {
 			c.NotFoundOrError(err, "get user by name")
 			return nil, nil, nil, nil, "", ""

+ 1 - 1
internal/route/repo/repo.go

@@ -47,7 +47,7 @@ func checkContextUser(c *context.Context, uid int64) *db.User {
 		return c.User
 	}
 
-	org, err := db.GetUserByID(uid)
+	org, err := db.Users.GetByID(c.Req.Context(), uid)
 	if db.IsErrUserNotExist(err) {
 		return c.User
 	}

+ 1 - 1
internal/route/repo/setting.go

@@ -380,7 +380,7 @@ func SettingsCollaborationPost(c *context.Context) {
 		return
 	}
 
-	u, err := db.GetUserByName(name)
+	u, err := db.Users.GetByUsername(c.Req.Context(), name)
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.Flash.Error(c.Tr("form.user_not_exist"))

+ 2 - 2
internal/route/repo/webhook.go

@@ -493,7 +493,7 @@ func TestWebhook(c *context.Context) {
 		committer = c.Repo.Commit.Committer
 
 		// Try to match email with a real user.
-		author, err := db.GetUserByEmail(c.Repo.Commit.Author.Email)
+		author, err := db.Users.GetByEmail(c.Req.Context(), c.Repo.Commit.Author.Email)
 		if err == nil {
 			authorUsername = author.Name
 		} else if !db.IsErrUserNotExist(err) {
@@ -501,7 +501,7 @@ func TestWebhook(c *context.Context) {
 			return
 		}
 
-		user, err := db.GetUserByEmail(c.Repo.Commit.Committer.Email)
+		user, err := db.Users.GetByEmail(c.Req.Context(), c.Repo.Commit.Committer.Email)
 		if err == nil {
 			committerUsername = user.Name
 		} else if !db.IsErrUserNotExist(err) {

+ 69 - 8
internal/route/user/auth.go

@@ -5,12 +5,15 @@
 package user
 
 import (
+	gocontext "context"
+	"encoding/hex"
 	"fmt"
 	"net/http"
 	"net/url"
 
 	"github.com/go-macaron/captcha"
 	"github.com/pkg/errors"
+	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
 
 	"gogs.io/gogs/internal/auth"
@@ -54,7 +57,7 @@ func AutoLogin(c *context.Context) (bool, error) {
 		}
 	}()
 
-	u, err := db.GetUserByName(uname)
+	u, err := db.Users.GetByUsername(c.Req.Context(), uname)
 	if err != nil {
 		if !db.IsErrUserNotExist(err) {
 			return false, fmt.Errorf("get user by name: %v", err)
@@ -229,7 +232,7 @@ func LoginTwoFactorPost(c *context.Context) {
 		return
 	}
 
-	u, err := db.GetUserByID(userID)
+	u, err := db.Users.GetByID(c.Req.Context(), userID)
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return
@@ -275,7 +278,7 @@ func LoginTwoFactorRecoveryCodePost(c *context.Context) {
 		return
 	}
 
-	u, err := db.GetUserByID(userID)
+	u, err := db.Users.GetByID(c.Req.Context(), userID)
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return
@@ -360,8 +363,11 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
 	}
 	log.Trace("Account created: %s", user.Name)
 
+	// FIXME: Count has pretty bad performance implication in large instances, we
+	// should have a dedicate method to check whether the "user" table is empty.
+	//
 	// Auto-set admin for the only user.
-	if db.CountUsers() == 1 {
+	if db.Users.Count(c.Req.Context()) == 1 {
 		user.IsAdmin = true
 		user.IsActive = true
 		if err := db.UpdateUser(user); err != nil {
@@ -387,6 +393,61 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
 	c.RedirectSubpath("/user/login")
 }
 
+// parseUserFromCode returns user by username encoded in code.
+// It returns nil if code or username is invalid.
+func parseUserFromCode(code string) (user *db.User) {
+	if len(code) <= tool.TIME_LIMIT_CODE_LENGTH {
+		return nil
+	}
+
+	// Use tail hex username to query user
+	hexStr := code[tool.TIME_LIMIT_CODE_LENGTH:]
+	if b, err := hex.DecodeString(hexStr); err == nil {
+		if user, err = db.Users.GetByUsername(gocontext.TODO(), string(b)); user != nil {
+			return user
+		} else if !db.IsErrUserNotExist(err) {
+			log.Error("Failed to get user by name %q: %v", string(b), err)
+		}
+	}
+
+	return nil
+}
+
+// verify active code when active account
+func verifyUserActiveCode(code string) (user *db.User) {
+	minutes := conf.Auth.ActivateCodeLives
+
+	if user = parseUserFromCode(code); user != nil {
+		// time limit code
+		prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
+		data := com.ToStr(user.ID) + user.Email + user.LowerName + user.Password + user.Rands
+
+		if tool.VerifyTimeLimitCode(data, minutes, prefix) {
+			return user
+		}
+	}
+	return nil
+}
+
+// verify active code when active account
+func verifyActiveEmailCode(code, email string) *db.EmailAddress {
+	minutes := conf.Auth.ActivateCodeLives
+
+	if user := parseUserFromCode(code); user != nil {
+		// time limit code
+		prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
+		data := com.ToStr(user.ID) + email + user.LowerName + user.Password + user.Rands
+
+		if tool.VerifyTimeLimitCode(data, minutes, prefix) {
+			emailAddress, err := db.EmailAddresses.GetByEmail(gocontext.TODO(), email)
+			if err == nil {
+				return emailAddress
+			}
+		}
+	}
+	return nil
+}
+
 func Activate(c *context.Context) {
 	code := c.Query("code")
 	if code == "" {
@@ -415,7 +476,7 @@ func Activate(c *context.Context) {
 	}
 
 	// Verify code.
-	if user := db.VerifyUserActiveCode(code); user != nil {
+	if user := verifyUserActiveCode(code); user != nil {
 		user.IsActive = true
 		var err error
 		if user.Rands, err = userutil.RandomSalt(); err != nil {
@@ -444,7 +505,7 @@ func ActivateEmail(c *context.Context) {
 	emailAddr := c.Query("email")
 
 	// Verify code.
-	if email := db.VerifyActiveEmailCode(code, emailAddr); email != nil {
+	if email := verifyActiveEmailCode(code, emailAddr); email != nil {
 		if err := email.Activate(); err != nil {
 			c.Error(err, "activate email")
 		}
@@ -481,7 +542,7 @@ func ForgotPasswdPost(c *context.Context) {
 	emailAddr := c.Query("email")
 	c.Data["Email"] = emailAddr
 
-	u, err := db.GetUserByEmail(emailAddr)
+	u, err := db.Users.GetByEmail(c.Req.Context(), emailAddr)
 	if err != nil {
 		if db.IsErrUserNotExist(err) {
 			c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
@@ -539,7 +600,7 @@ func ResetPasswdPost(c *context.Context) {
 	}
 	c.Data["Code"] = code
 
-	if u := db.VerifyUserActiveCode(code); u != nil {
+	if u := verifyUserActiveCode(code); u != nil {
 		// Validate password length.
 		passwd := c.Query("password")
 		if len(passwd) < 6 {

+ 4 - 4
internal/route/user/home.go

@@ -31,7 +31,7 @@ func getDashboardContextUser(c *context.Context) *db.User {
 	orgName := c.Params(":org")
 	if len(orgName) > 0 {
 		// Organization.
-		org, err := db.GetUserByName(orgName)
+		org, err := db.Users.GetByUsername(c.Req.Context(), orgName)
 		if err != nil {
 			c.NotFoundOrError(err, "get user by name")
 			return nil
@@ -42,7 +42,7 @@ func getDashboardContextUser(c *context.Context) *db.User {
 
 	orgs, err := db.Orgs.List(
 		c.Req.Context(),
-		db.ListOrgOptions{
+		db.ListOrgsOptions{
 			MemberID:              c.User.ID,
 			IncludePrivateMembers: true,
 		},
@@ -81,7 +81,7 @@ func retrieveFeeds(c *context.Context, ctxUser *db.User, userID int64, isProfile
 		// Cache results to reduce queries.
 		_, ok := unameAvatars[act.ActUserName]
 		if !ok {
-			u, err := db.GetUserByName(act.ActUserName)
+			u, err := db.Users.GetByUsername(c.Req.Context(), act.ActUserName)
 			if err != nil {
 				if db.IsErrUserNotExist(err) {
 					continue
@@ -444,7 +444,7 @@ func showOrgProfile(c *context.Context) {
 }
 
 func Email2User(c *context.Context) {
-	u, err := db.GetUserByEmail(c.Query("email"))
+	u, err := db.Users.GetByEmail(c.Req.Context(), c.Query("email"))
 	if err != nil {
 		c.NotFoundOrError(err, "get user by email")
 		return

+ 3 - 3
internal/route/user/setting.go

@@ -256,7 +256,7 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 	}
 
 	emailAddr := &db.EmailAddress{
-		UID:         c.User.ID,
+		UserID:      c.User.ID,
 		Email:       f.Email,
 		IsActivated: !conf.Auth.RequireEmailConfirmation,
 	}
@@ -286,8 +286,8 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 
 func DeleteEmail(c *context.Context) {
 	if err := db.DeleteEmailAddress(&db.EmailAddress{
-		ID:  c.QueryInt64("id"),
-		UID: c.User.ID,
+		ID:     c.QueryInt64("id"),
+		UserID: c.User.ID,
 	}); err != nil {
 		c.Errorf(err, "delete email address")
 		return