Browse Source

all: unwrap `database.UsersStore` interface (#7708)

Joe Chen 6 months ago
parent
commit
d9ecdcaef0
62 changed files with 854 additions and 4159 deletions
  1. 2 2
      internal/cmd/admin.go
  2. 2 2
      internal/cmd/serv.go
  3. 32 6
      internal/context/auth.go
  4. 1 1
      internal/context/go_get.go
  5. 1 1
      internal/context/org.go
  6. 1 1
      internal/context/repo.go
  7. 42 0
      internal/context/store.go
  8. 1 1
      internal/context/user.go
  9. 6 6
      internal/database/actions.go
  10. 10 10
      internal/database/actions_test.go
  11. 1 1
      internal/database/comment.go
  12. 5 3
      internal/database/database.go
  13. 2 2
      internal/database/issue.go
  14. 2 2
      internal/database/issue_mail.go
  15. 1 1
      internal/database/login_sources_test.go
  16. 0 17
      internal/database/mocks.go
  17. 1 1
      internal/database/models.go
  18. 5 5
      internal/database/org.go
  19. 1 1
      internal/database/org_team.go
  20. 2 2
      internal/database/organizations_test.go
  21. 1 1
      internal/database/pull.go
  22. 6 6
      internal/database/repo.go
  23. 1 1
      internal/database/repositories_test.go
  24. 1 1
      internal/database/update.go
  25. 164 211
      internal/database/users.go
  26. 177 177
      internal/database/users_test.go
  27. 1 1
      internal/route/admin/admin.go
  28. 7 7
      internal/route/admin/users.go
  29. 4 4
      internal/route/api/v1/admin/user.go
  30. 2 2
      internal/route/api/v1/api.go
  31. 2 2
      internal/route/api/v1/convert/convert.go
  32. 1 1
      internal/route/api/v1/org/org.go
  33. 3 3
      internal/route/api/v1/repo/collaborators.go
  34. 2 2
      internal/route/api/v1/repo/commits.go
  35. 2 2
      internal/route/api/v1/repo/issue.go
  36. 5 5
      internal/route/api/v1/repo/repo.go
  37. 3 3
      internal/route/api/v1/user/email.go
  38. 5 5
      internal/route/api/v1/user/follower.go
  39. 1 1
      internal/route/api/v1/user/key.go
  40. 2 2
      internal/route/api/v1/user/user.go
  41. 3 3
      internal/route/home.go
  42. 2 2
      internal/route/install.go
  43. 171 3524
      internal/route/lfs/mocks_test.go
  44. 2 2
      internal/route/lfs/route.go
  45. 31 71
      internal/route/lfs/route_test.go
  46. 42 0
      internal/route/lfs/store.go
  47. 1 1
      internal/route/org/members.go
  48. 4 4
      internal/route/org/setting.go
  49. 1 1
      internal/route/org/teams.go
  50. 3 2
      internal/route/repo/commit.go
  51. 2 2
      internal/route/repo/http.go
  52. 1 1
      internal/route/repo/pull.go
  53. 1 1
      internal/route/repo/repo.go
  54. 2 2
      internal/route/repo/setting.go
  55. 42 0
      internal/route/repo/store.go
  56. 2 2
      internal/route/repo/tasks.go
  57. 2 2
      internal/route/repo/webhook.go
  58. 13 13
      internal/route/user/auth.go
  59. 3 3
      internal/route/user/home.go
  60. 4 4
      internal/route/user/profile.go
  61. 14 14
      internal/route/user/setting.go
  62. 0 3
      mockgen.yaml

+ 2 - 2
internal/cmd/admin.go

@@ -52,7 +52,7 @@ to make automatic initialization process more smoothly`,
 		Name:  "delete-inactive-users",
 		Usage: "Delete all inactive accounts",
 		Action: adminDashboardOperation(
-			func() error { return database.Users.DeleteInactivated() },
+			func() error { return database.Handle.Users().DeleteInactivated() },
 			"All inactivated accounts have been deleted successfully",
 		),
 		Flags: []cli.Flag{
@@ -152,7 +152,7 @@ func runCreateUser(c *cli.Context) error {
 		return errors.Wrap(err, "set engine")
 	}
 
-	user, err := database.Users.Create(
+	user, err := database.Handle.Users().Create(
 		context.Background(),
 		c.String("name"),
 		c.String("email"),

+ 2 - 2
internal/cmd/serv.go

@@ -162,7 +162,7 @@ func runServ(c *cli.Context) error {
 	repoName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
 	repoName = strings.TrimSuffix(repoName, ".wiki")
 
-	owner, err := database.Users.GetByUsername(ctx, ownerName)
+	owner, err := database.Handle.Users().GetByUsername(ctx, ownerName)
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			fail("Repository owner does not exist", "Unregistered owner: %s", ownerName)
@@ -205,7 +205,7 @@ func runServ(c *cli.Context) error {
 			}
 			checkDeployKey(key, repo)
 		} else {
-			user, err = database.Users.GetByKeyID(ctx, key.ID)
+			user, err = database.Handle.Users().GetByKeyID(ctx, key.ID)
 			if err != nil {
 				fail("Internal error", "Failed to get user by key ID '%d': %v", key.ID, err)
 			}

+ 32 - 6
internal/context/auth.go

@@ -113,6 +113,32 @@ type AuthStore interface {
 	// TouchAccessTokenByID updates the updated time of the given access token to
 	// the current time.
 	TouchAccessTokenByID(ctx context.Context, id int64) error
+
+	// GetUserByID returns the user with given ID. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByID(ctx context.Context, id int64) (*database.User, error)
+	// GetUserByUsername returns the user with given username. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByUsername(ctx context.Context, username string) (*database.User, error)
+	// CreateUser creates a new user and persists to database. It returns
+	// database.ErrNameNotAllowed if the given name or pattern of the name is not
+	// allowed as a username, or database.ErrUserAlreadyExist when a user with same
+	// name already exists, or database.ErrEmailAlreadyUsed if the email has been
+	// verified by another user.
+	CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error)
+	// AuthenticateUser validates username and password via given login source ID.
+	// It returns database.ErrUserNotExist when the user was not found.
+	//
+	// When the "loginSourceID" is negative, it aborts the process and returns
+	// database.ErrUserNotExist if the user was not found in the database.
+	//
+	// When the "loginSourceID" is non-negative, it returns
+	// database.ErrLoginSourceMismatch if the user has different login source ID
+	// than the "loginSourceID".
+	//
+	// 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.
+	AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error)
 }
 
 // authenticatedUserID returns the ID of the authenticated user, along with a bool value
@@ -160,7 +186,7 @@ func authenticatedUserID(store AuthStore, c *macaron.Context, sess session.Store
 		return 0, false
 	}
 	if id, ok := uid.(int64); ok {
-		_, err := database.Users.GetByID(c.Req.Context(), id)
+		_, err := store.GetUserByID(c.Req.Context(), id)
 		if err != nil {
 			if !database.IsErrUserNotExist(err) {
 				log.Error("Failed to get user by ID: %v", err)
@@ -185,7 +211,7 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
 		if conf.Auth.EnableReverseProxyAuthentication {
 			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
 			if len(webAuthUser) > 0 {
-				user, err := database.Users.GetByUsername(ctx.Req.Context(), webAuthUser)
+				user, err := store.GetUserByUsername(ctx.Req.Context(), webAuthUser)
 				if err != nil {
 					if !database.IsErrUserNotExist(err) {
 						log.Error("Failed to get user by name: %v", err)
@@ -194,7 +220,7 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
 
 					// Check if enabled auto-registration.
 					if conf.Auth.EnableReverseProxyAutoRegistration {
-						user, err = database.Users.Create(
+						user, err = store.CreateUser(
 							ctx.Req.Context(),
 							webAuthUser,
 							gouuid.NewV4().String()+"@localhost",
@@ -219,7 +245,7 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
 			if len(auths) == 2 && auths[0] == "Basic" {
 				uname, passwd, _ := tool.BasicAuthDecode(auths[1])
 
-				u, err := database.Users.Authenticate(ctx.Req.Context(), uname, passwd, -1)
+				u, err := store.AuthenticateUser(ctx.Req.Context(), uname, passwd, -1)
 				if err != nil {
 					if !auth.IsErrBadCredentials(err) {
 						log.Error("Failed to authenticate user: %v", err)
@@ -233,7 +259,7 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
 		return nil, false, false
 	}
 
-	u, err := database.Users.GetByID(ctx.Req.Context(), uid)
+	u, err := store.GetUserByID(ctx.Req.Context(), uid)
 	if err != nil {
 		log.Error("GetUserByID: %v", err)
 		return nil, false, false
@@ -254,7 +280,7 @@ func AuthenticateByToken(store AuthStore, ctx context.Context, token string) (*d
 		log.Error("Failed to touch access token [id: %d]: %v", t.ID, err)
 	}
 
-	user, err := database.Users.GetByID(ctx, t.UserID)
+	user, err := store.GetUserByID(ctx, t.UserID)
 	if err != nil {
 		return nil, errors.Wrapf(err, "get user by ID [user_id: %d]", t.UserID)
 	}

+ 1 - 1
internal/context/go_get.go

@@ -27,7 +27,7 @@ func ServeGoGet() macaron.Handler {
 		repoName := c.Params(":reponame")
 		branchName := "master"
 
-		owner, err := database.Users.GetByUsername(c.Req.Context(), ownerName)
+		owner, err := database.Handle.Users().GetByUsername(c.Req.Context(), ownerName)
 		if err == nil {
 			repo, err := database.Handle.Repositories().GetByName(c.Req.Context(), owner.ID, repoName)
 			if err == nil && repo.DefaultBranch != "" {

+ 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 = database.Users.GetByUsername(c.Req.Context(), orgName)
+	c.Org.Organization, err = database.Handle.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 = database.Users.GetByUsername(c.Req.Context(), ownerName)
+			owner, err = database.Handle.Users().GetByUsername(c.Req.Context(), ownerName)
 			if err != nil {
 				c.NotFoundOrError(err, "get user by name")
 				return

+ 42 - 0
internal/context/store.go

@@ -16,6 +16,32 @@ type Store interface {
 	// TouchAccessTokenByID updates the updated time of the given access token to
 	// the current time.
 	TouchAccessTokenByID(ctx context.Context, id int64) error
+
+	// GetUserByID returns the user with given ID. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByID(ctx context.Context, id int64) (*database.User, error)
+	// GetUserByUsername returns the user with given username. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByUsername(ctx context.Context, username string) (*database.User, error)
+	// CreateUser creates a new user and persists to database. It returns
+	// database.ErrNameNotAllowed if the given name or pattern of the name is not
+	// allowed as a username, or database.ErrUserAlreadyExist when a user with same
+	// name already exists, or database.ErrEmailAlreadyUsed if the email has been
+	// verified by another user.
+	CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error)
+	// AuthenticateUser validates username and password via given login source ID.
+	// It returns database.ErrUserNotExist when the user was not found.
+	//
+	// When the "loginSourceID" is negative, it aborts the process and returns
+	// database.ErrUserNotExist if the user was not found in the database.
+	//
+	// When the "loginSourceID" is non-negative, it returns
+	// database.ErrLoginSourceMismatch if the user has different login source ID
+	// than the "loginSourceID".
+	//
+	// 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.
+	AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error)
 }
 
 type store struct{}
@@ -32,3 +58,19 @@ func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.
 func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
 	return database.Handle.AccessTokens().Touch(ctx, id)
 }
+
+func (*store) GetUserByID(ctx context.Context, id int64) (*database.User, error) {
+	return database.Handle.Users().GetByID(ctx, id)
+}
+
+func (*store) GetUserByUsername(ctx context.Context, username string) (*database.User, error) {
+	return database.Handle.Users().GetByUsername(ctx, username)
+}
+
+func (*store) CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error) {
+	return database.Handle.Users().Create(ctx, username, email, opts)
+}
+
+func (*store) AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error) {
+	return database.Handle.Users().Authenticate(ctx, login, password, loginSourceID)
+}

+ 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 := database.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
+		user, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":username"))
 		if err != nil {
 			c.NotFoundOrError(err, "get user by name")
 			return

+ 6 - 6
internal/database/actions.go

@@ -221,7 +221,7 @@ func (s *ActionsStore) MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOp
 	}
 
 	apiCommits, err := opts.Commits.APIFormat(ctx,
-		NewUsersStore(s.db),
+		newUsersStore(s.db),
 		repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
 		repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
 	)
@@ -470,7 +470,7 @@ func (s *ActionsStore) CommitRepo(ctx context.Context, opts CommitRepoOptions) e
 		return errors.Wrap(err, "touch repository")
 	}
 
-	pusher, err := NewUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
+	pusher, err := newUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
 	if err != nil {
 		return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
 	}
@@ -566,7 +566,7 @@ func (s *ActionsStore) CommitRepo(ctx context.Context, opts CommitRepoOptions) e
 	}
 
 	commits, err := opts.Commits.APIFormat(ctx,
-		NewUsersStore(s.db),
+		newUsersStore(s.db),
 		repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
 		repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
 	)
@@ -617,7 +617,7 @@ func (s *ActionsStore) PushTag(ctx context.Context, opts PushTagOptions) error {
 		return errors.Wrap(err, "touch repository")
 	}
 
-	pusher, err := NewUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
+	pusher, err := newUsersStore(s.db).GetByUsername(ctx, opts.PusherName)
 	if err != nil {
 		return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
 	}
@@ -852,7 +852,7 @@ func NewPushCommits() *PushCommits {
 	}
 }
 
-func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore UsersStore, repoPath, repoURL string) ([]*api.PayloadCommit, error) {
+func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore *UsersStore, repoPath, repoURL string) ([]*api.PayloadCommit, error) {
 	// NOTE: We cache query results in case there are many commits in a single push.
 	usernameByEmail := make(map[string]string)
 	getUsernameByEmail := func(email string) (string, error) {
@@ -925,7 +925,7 @@ func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore UsersStore, re
 func (pcs *PushCommits) AvatarLink(email string) string {
 	_, ok := pcs.avatars[email]
 	if !ok {
-		u, err := Users.GetByEmail(context.Background(), email)
+		u, err := Handle.Users().GetByEmail(context.Background(), email)
 		if err != nil {
 			pcs.avatars[email] = tool.AvatarLink(email)
 			if !IsErrUserNotExist(err) {

+ 10 - 10
internal/database/actions_test.go

@@ -133,7 +133,7 @@ func TestActions(t *testing.T) {
 }
 
 func actionsCommitRepo(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -436,7 +436,7 @@ func actionsListByUser(t *testing.T, ctx context.Context, s *ActionsStore) {
 }
 
 func actionsMergePullRequest(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -481,7 +481,7 @@ func actionsMergePullRequest(t *testing.T, ctx context.Context, s *ActionsStore)
 }
 
 func actionsMirrorSyncCreate(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -522,7 +522,7 @@ func actionsMirrorSyncCreate(t *testing.T, ctx context.Context, s *ActionsStore)
 }
 
 func actionsMirrorSyncDelete(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -563,7 +563,7 @@ func actionsMirrorSyncDelete(t *testing.T, ctx context.Context, s *ActionsStore)
 }
 
 func actionsMirrorSyncPush(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -628,7 +628,7 @@ func actionsMirrorSyncPush(t *testing.T, ctx context.Context, s *ActionsStore) {
 }
 
 func actionsNewRepo(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -707,7 +707,7 @@ func actionsPushTag(t *testing.T, ctx context.Context, s *ActionsStore) {
 	// to the mock server because this function holds a lock.
 	conf.SetMockServer(t, conf.ServerOpts{})
 
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -799,7 +799,7 @@ func actionsPushTag(t *testing.T, ctx context.Context, s *ActionsStore) {
 }
 
 func actionsRenameRepo(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,
@@ -836,9 +836,9 @@ func actionsRenameRepo(t *testing.T, ctx context.Context, s *ActionsStore) {
 }
 
 func actionsTransferRepo(t *testing.T, ctx context.Context, s *ActionsStore) {
-	alice, err := NewUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+	alice, err := newUsersStore(s.db).Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
-	bob, err := NewUsersStore(s.db).Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
+	bob, err := newUsersStore(s.db).Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	repo, err := newReposStore(s.db).Create(ctx,
 		alice.ID,

+ 1 - 1
internal/database/comment.go

@@ -95,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 = Users.GetByID(context.TODO(), c.PosterID)
+		c.Poster, err = Handle.Users().GetByID(context.TODO(), c.PosterID)
 		if err != nil {
 			if IsErrUserNotExist(err) {
 				c.PosterID = -1

+ 5 - 3
internal/database/database.go

@@ -122,9 +122,7 @@ func NewConnection(w logger.Writer) (*gorm.DB, error) {
 		return nil, errors.Wrap(err, "load login source files")
 	}
 
-	// Initialize stores, sorted in alphabetical order.
-	Users = NewUsersStore(db)
-
+	// Initialize the database handle.
 	Handle = &DB{db: db}
 	return db, nil
 }
@@ -189,3 +187,7 @@ func (db *DB) Repositories() *RepositoriesStore {
 func (db *DB) TwoFactors() *TwoFactorsStore {
 	return newTwoFactorsStore(db.db)
 }
+
+func (db *DB) Users() *UsersStore {
+	return newUsersStore(db.db)
+}

+ 2 - 2
internal/database/issue.go

@@ -408,7 +408,7 @@ func (issue *Issue) GetAssignee() (err error) {
 		return nil
 	}
 
-	issue.Assignee, err = Users.GetByID(context.TODO(), issue.AssigneeID)
+	issue.Assignee, err = Handle.Users().GetByID(context.TODO(), issue.AssigneeID)
 	if IsErrUserNotExist(err) {
 		return nil
 	}
@@ -614,7 +614,7 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
 		return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
 	}
 
-	issue.Assignee, err = Users.GetByID(context.TODO(), issue.AssigneeID)
+	issue.Assignee, err = Handle.Users().GetByID(context.TODO(), issue.AssigneeID)
 	if err != nil && !IsErrUserNotExist(err) {
 		log.Error("Failed to get user by ID: %v", err)
 		return nil

+ 2 - 2
internal/database/issue_mail.go

@@ -128,7 +128,7 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
 			continue
 		}
 
-		to, err := Users.GetByID(ctx, watchers[i].UserID)
+		to, err := Handle.Users().GetByID(ctx, watchers[i].UserID)
 		if err != nil {
 			return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
 		}
@@ -168,7 +168,7 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
 		toUsernames = append(toUsernames, mentions[i])
 	}
 
-	tos, err = Users.GetMailableEmailsByUsernames(ctx, toUsernames)
+	tos, err = Handle.Users().GetMailableEmailsByUsernames(ctx, toUsernames)
 	if err != nil {
 		return errors.Wrap(err, "get mailable emails by usernames")
 	}

+ 1 - 1
internal/database/login_sources_test.go

@@ -265,7 +265,7 @@ func loginSourcesDeleteByID(t *testing.T, ctx context.Context, s *LoginSourcesSt
 		require.NoError(t, err)
 
 		// Create a user that uses this login source
-		_, err = NewUsersStore(s.db).Create(ctx, "alice", "",
+		_, err = newUsersStore(s.db).Create(ctx, "alice", "",
 			CreateUserOptions{
 				LoginSource: source.ID,
 			},

+ 0 - 17
internal/database/mocks.go

@@ -1,17 +0,0 @@
-// Copyright 2020 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 database
-
-import (
-	"testing"
-)
-
-func SetMockUsersStore(t *testing.T, mock UsersStore) {
-	before := Users
-	Users = mock
-	t.Cleanup(func() {
-		Users = before
-	})
-}

+ 1 - 1
internal/database/models.go

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

+ 5 - 5
internal/database/org.go

@@ -73,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 = Users.GetByID(context.TODO(), ou.Uid)
+		org.Members[i], err = Handle.Users().GetByID(context.TODO(), ou.Uid)
 		if err != nil {
 			return err
 		}
@@ -106,7 +106,7 @@ func CreateOrganization(org, owner *User) (err error) {
 		return err
 	}
 
-	if Users.IsUsernameUsed(context.TODO(), org.Name, 0) {
+	if Handle.Users().IsUsernameUsed(context.TODO(), org.Name, 0) {
 		return ErrUserAlreadyExist{
 			args: errutil.Args{
 				"name": org.Name,
@@ -216,7 +216,7 @@ func deleteBeans(e Engine, beans ...any) (err error) {
 
 // DeleteOrganization completely and permanently deletes everything of organization.
 func DeleteOrganization(org *User) error {
-	err := Users.DeleteByID(context.TODO(), org.ID, false)
+	err := Handle.Users().DeleteByID(context.TODO(), org.ID, false)
 	if err != nil {
 		return err
 	}
@@ -373,11 +373,11 @@ func RemoveOrgUser(orgID, userID int64) error {
 		return nil
 	}
 
-	user, err := Users.GetByID(context.TODO(), userID)
+	user, err := Handle.Users().GetByID(context.TODO(), userID)
 	if err != nil {
 		return fmt.Errorf("GetUserByID [%d]: %v", userID, err)
 	}
-	org, err := Users.GetByID(context.TODO(), orgID)
+	org, err := Handle.Users().GetByID(context.TODO(), orgID)
 	if err != nil {
 		return fmt.Errorf("GetUserByID [%d]: %v", orgID, err)
 	}

+ 1 - 1
internal/database/org_team.go

@@ -418,7 +418,7 @@ func DeleteTeam(t *Team) error {
 	}
 
 	// Get organization.
-	org, err := Users.GetByID(context.TODO(), t.OrgID)
+	org, err := Handle.Users().GetByID(context.TODO(), t.OrgID)
 	if err != nil {
 		return err
 	}

+ 2 - 2
internal/database/organizations_test.go

@@ -47,7 +47,7 @@ func TestOrgs(t *testing.T) {
 }
 
 func orgsList(t *testing.T, ctx context.Context, s *OrganizationsStore) {
-	usersStore := NewUsersStore(s.db)
+	usersStore := newUsersStore(s.db)
 	alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 	bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
@@ -118,7 +118,7 @@ func orgsList(t *testing.T, ctx context.Context, s *OrganizationsStore) {
 
 func organizationsSearchByName(t *testing.T, ctx context.Context, s *OrganizationsStore) {
 	// TODO: Use Orgs.Create to replace SQL hack when the method is available.
-	usersStore := NewUsersStore(s.db)
+	usersStore := newUsersStore(s.db)
 	org1, err := usersStore.Create(ctx, "org1", "org1@example.com", CreateUserOptions{FullName: "Acme Corp"})
 	require.NoError(t, err)
 	org2, err := usersStore.Create(ctx, "org2", "org2@example.com", CreateUserOptions{FullName: "Acme Corp 2"})

+ 1 - 1
internal/database/pull.go

@@ -373,7 +373,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
 		commits = append([]*git.Commit{mergeCommit}, commits...)
 	}
 
-	pcs, err := CommitsToPushCommits(commits).APIFormat(ctx, Users, pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
+	pcs, err := CommitsToPushCommits(commits).APIFormat(ctx, Handle.Users(), pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
 	if err != nil {
 		log.Error("Failed to convert to API payload commits: %v", err)
 		return nil

+ 6 - 6
internal/database/repo.go

@@ -542,7 +542,7 @@ func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
 	) {
 		return nil, ErrUserNotExist{args: errutil.Args{"userID": userID}}
 	}
-	return Users.GetByID(ctx, userID)
+	return Handle.Users().GetByID(ctx, userID)
 }
 
 // GetWriters returns all users that have write access to the repository.
@@ -1255,7 +1255,7 @@ func CreateRepository(doer, owner *User, opts CreateRepoOptionsLegacy) (_ *Repos
 	}
 
 	// Remember visibility preference
-	err = Users.Update(context.TODO(), owner.ID, UpdateUserOptions{LastRepoVisibility: &repo.IsPrivate})
+	err = Handle.Users().Update(context.TODO(), owner.ID, UpdateUserOptions{LastRepoVisibility: &repo.IsPrivate})
 	if err != nil {
 		return nil, errors.Wrap(err, "update user")
 	}
@@ -1352,7 +1352,7 @@ func RepoPath(userName, repoName string) string {
 
 // TransferOwnership transfers all corresponding setting from old user to new one.
 func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error {
-	newOwner, err := Users.GetByUsername(context.TODO(), newOwnerName)
+	newOwner, err := Handle.Users().GetByUsername(context.TODO(), newOwnerName)
 	if err != nil {
 		return fmt.Errorf("get new owner '%s': %v", newOwnerName, err)
 	}
@@ -1643,7 +1643,7 @@ func DeleteRepository(ownerID, repoID int64) error {
 	}
 
 	// In case is a organization.
-	org, err := Users.GetByID(context.TODO(), ownerID)
+	org, err := Handle.Users().GetByID(context.TODO(), ownerID)
 	if err != nil {
 		return err
 	}
@@ -1761,7 +1761,7 @@ func GetRepositoryByRef(ref string) (*Repository, error) {
 	}
 
 	userName, repoName := ref[:n], ref[n+1:]
-	user, err := Users.GetByUsername(context.TODO(), userName)
+	user, err := Handle.Users().GetByUsername(context.TODO(), userName)
 	if err != nil {
 		return nil, err
 	}
@@ -2577,7 +2577,7 @@ func ForkRepository(doer, owner *User, baseRepo *Repository, name, desc string)
 	}
 
 	// Remember visibility preference
-	err = Users.Update(context.TODO(), owner.ID, UpdateUserOptions{LastRepoVisibility: &repo.IsPrivate})
+	err = Handle.Users().Update(context.TODO(), owner.ID, UpdateUserOptions{LastRepoVisibility: &repo.IsPrivate})
 	if err != nil {
 		return nil, errors.Wrap(err, "update user")
 	}

+ 1 - 1
internal/database/repositories_test.go

@@ -245,7 +245,7 @@ func reposGetByName(t *testing.T, ctx context.Context, s *RepositoriesStore) {
 func reposStar(t *testing.T, ctx context.Context, s *RepositoriesStore) {
 	repo1, err := s.Create(ctx, 1, CreateRepoOptions{Name: "repo1"})
 	require.NoError(t, err)
-	usersStore := NewUsersStore(s.db)
+	usersStore := newUsersStore(s.db)
 	alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
 	require.NoError(t, err)
 

+ 1 - 1
internal/database/update.go

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

+ 164 - 211
internal/database/users.go

@@ -32,130 +32,13 @@ import (
 	"gogs.io/gogs/internal/userutil"
 )
 
-// UsersStore is the persistent interface for users.
-type UsersStore interface {
-	// Authenticate validates username and password via given login source ID. It
-	// returns ErrUserNotExist when the user was not found.
-	//
-	// When the "loginSourceID" is negative, it aborts the process and returns
-	// ErrUserNotExist if the user was not found in the database.
-	//
-	// When the "loginSourceID" is non-negative, it returns ErrLoginSourceMismatch
-	// if the user has different login source ID than the "loginSourceID".
-	//
-	// 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)
-	// Create creates a new user and persists to database. It returns
-	// ErrNameNotAllowed if the given name or pattern of the name is not allowed as
-	// a username, or ErrUserAlreadyExist when a user with same name already exists,
-	// or ErrEmailAlreadyUsed if the email has been verified by another user.
-	Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error)
-
-	// GetByEmail returns the user (not organization) with given email. It ignores
-	// records with unverified emails and returns ErrUserNotExist when not found.
-	GetByEmail(ctx context.Context, email string) (*User, error)
-	// GetByID returns the user with given ID. It returns ErrUserNotExist when not
-	// found.
-	GetByID(ctx context.Context, id int64) (*User, error)
-	// GetByUsername returns the user with given username. It returns
-	// ErrUserNotExist when not found.
-	GetByUsername(ctx context.Context, username string) (*User, error)
-	// GetByKeyID returns the owner of given public key ID. It returns
-	// ErrUserNotExist when not found.
-	GetByKeyID(ctx context.Context, keyID int64) (*User, error)
-	// GetMailableEmailsByUsernames returns a list of verified primary email
-	// addresses (where email notifications are sent to) of users with given list of
-	// usernames. Non-existing usernames are ignored.
-	GetMailableEmailsByUsernames(ctx context.Context, usernames []string) ([]string, error)
-	// SearchByName returns a list of users whose username or full name matches the
-	// given keyword case-insensitively. Results are paginated by given page and
-	// page size, and sorted by the given order (e.g. "id DESC"). A total count of
-	// all results is also returned. If the order is not given, it's up to the
-	// database to decide.
-	SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*User, int64, error)
-
-	// IsUsernameUsed returns true if the given username has been used other than
-	// the excluded user (a non-positive ID effectively meaning check against all
-	// users).
-	IsUsernameUsed(ctx context.Context, username string, excludeUserId int64) bool
-	// ChangeUsername changes the username of the given user and updates all
-	// references to the old username. It returns ErrNameNotAllowed if the given
-	// name or pattern of the name is not allowed as a username, or
-	// ErrUserAlreadyExist when another user with same name already exists.
-	ChangeUsername(ctx context.Context, userID int64, newUsername string) error
-	// Update updates fields for the given user.
-	Update(ctx context.Context, userID int64, opts UpdateUserOptions) error
-	// UseCustomAvatar uses the given avatar as the user custom avatar.
-	UseCustomAvatar(ctx context.Context, userID int64, avatar []byte) error
-
-	// DeleteCustomAvatar deletes the current user custom avatar and falls back to
-	// use look up avatar by email.
-	DeleteCustomAvatar(ctx context.Context, userID int64) error
-	// DeleteByID deletes the given user and all their resources. It returns
-	// ErrUserOwnRepos when the user still has repository ownership, or returns
-	// ErrUserHasOrgs when the user still has organization membership. It is more
-	// performant to skip rewriting the "authorized_keys" file for individual
-	// deletion in a batch operation.
-	DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error
-	// DeleteInactivated deletes all inactivated users.
-	DeleteInactivated() error
-
-	// AddEmail adds a new email address to given user. It returns
-	// ErrEmailAlreadyUsed if the email has been verified by another user.
-	AddEmail(ctx context.Context, userID int64, email string, isActivated bool) error
-	// GetEmail returns the email address of the given user. If `needsActivated` is
-	// true, only activated email will be returned, otherwise, it may return
-	// inactivated email addresses. It returns ErrEmailNotExist when no qualified
-	// email is not found.
-	GetEmail(ctx context.Context, userID int64, email string, needsActivated bool) (*EmailAddress, error)
-	// ListEmails returns all email addresses of the given user. It always includes
-	// a primary email address.
-	ListEmails(ctx context.Context, userID int64) ([]*EmailAddress, error)
-	// MarkEmailActivated marks the email address of the given user as activated,
-	// and new rands are generated for the user.
-	MarkEmailActivated(ctx context.Context, userID int64, email string) error
-	// MarkEmailPrimary marks the email address of the given user as primary. It
-	// returns ErrEmailNotExist when the email is not found for the user, and
-	// ErrEmailNotActivated when the email is not activated.
-	MarkEmailPrimary(ctx context.Context, userID int64, email string) error
-	// DeleteEmail deletes the email address of the given user.
-	DeleteEmail(ctx context.Context, userID int64, email string) error
-
-	// Follow marks the user to follow the other user.
-	Follow(ctx context.Context, userID, followID int64) error
-	// Unfollow removes the mark the user to follow the other user.
-	Unfollow(ctx context.Context, userID, followID int64) error
-	// IsFollowing returns true if the user is following the other user.
-	IsFollowing(ctx context.Context, userID, followID int64) bool
-	// 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.
-	ListFollowers(ctx context.Context, userID int64, page, pageSize int) ([]*User, error)
-	// ListFollowings returns a list of users that are followed by the given user.
-	// Results are paginated by given page and page size, and sorted by the time of
-	// follow in descending order.
-	ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error)
-
-	// 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)
-	// Count returns the total number of users.
-	Count(ctx context.Context) int64
-}
-
-var Users UsersStore
-
-var _ UsersStore = (*usersStore)(nil)
-
-type usersStore struct {
-	*gorm.DB
-}
-
-// NewUsersStore returns a persistent interface for users with given database
-// connection.
-func NewUsersStore(db *gorm.DB) UsersStore {
-	return &usersStore{DB: db}
+// UsersStore is the storage layer for users.
+type UsersStore struct {
+	db *gorm.DB
+}
+
+func newUsersStore(db *gorm.DB) *UsersStore {
+	return &UsersStore{db: db}
 }
 
 type ErrLoginSourceMismatch struct {
@@ -165,18 +48,28 @@ type ErrLoginSourceMismatch struct {
 // IsErrLoginSourceMismatch returns true if the underlying error has the type
 // ErrLoginSourceMismatch.
 func IsErrLoginSourceMismatch(err error) bool {
-	_, ok := errors.Cause(err).(ErrLoginSourceMismatch)
-	return ok
+	return errors.As(err, &ErrLoginSourceMismatch{})
 }
 
 func (err ErrLoginSourceMismatch) Error() string {
 	return fmt.Sprintf("login source mismatch: %v", err.args)
 }
 
-func (s *usersStore) Authenticate(ctx context.Context, login, password string, loginSourceID int64) (*User, error) {
+// Authenticate validates username and password via given login source ID. It
+// returns ErrUserNotExist when the user was not found.
+//
+// When the "loginSourceID" is negative, it aborts the process and returns
+// ErrUserNotExist if the user was not found in the database.
+//
+// When the "loginSourceID" is non-negative, it returns ErrLoginSourceMismatch
+// if the user has different login source ID than the "loginSourceID".
+//
+// 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.
+func (s *UsersStore) Authenticate(ctx context.Context, login, password string, loginSourceID int64) (*User, error) {
 	login = strings.ToLower(login)
 
-	query := s.WithContext(ctx)
+	query := s.db.WithContext(ctx)
 	if strings.Contains(login, "@") {
 		query = query.Where("email = ?", login)
 	} else {
@@ -221,7 +114,7 @@ func (s *usersStore) Authenticate(ctx context.Context, login, password string, l
 		createNewUser = true
 	}
 
-	source, err := newLoginSourcesStore(s.DB, loadedLoginSourceFilesStore).GetByID(ctx, authSourceID)
+	source, err := newLoginSourcesStore(s.db, loadedLoginSourceFilesStore).GetByID(ctx, authSourceID)
 	if err != nil {
 		return nil, errors.Wrap(err, "get login source")
 	}
@@ -257,7 +150,11 @@ func (s *usersStore) Authenticate(ctx context.Context, login, password string, l
 	)
 }
 
-func (s *usersStore) ChangeUsername(ctx context.Context, userID int64, newUsername string) error {
+// ChangeUsername changes the username of the given user and updates all
+// references to the old username. It returns ErrNameNotAllowed if the given
+// name or pattern of the name is not allowed as a username, or
+// ErrUserAlreadyExist when another user with same name already exists.
+func (s *UsersStore) ChangeUsername(ctx context.Context, userID int64, newUsername string) error {
 	err := isUsernameAllowed(newUsername)
 	if err != nil {
 		return err
@@ -276,7 +173,7 @@ func (s *usersStore) ChangeUsername(ctx context.Context, userID int64, newUserna
 		return errors.Wrap(err, "get user")
 	}
 
-	return s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		err := tx.Model(&User{}).
 			Where("id = ?", user.ID).
 			Updates(map[string]any{
@@ -338,9 +235,10 @@ func (s *usersStore) ChangeUsername(ctx context.Context, userID int64, newUserna
 	})
 }
 
-func (s *usersStore) Count(ctx context.Context) int64 {
+// Count returns the total number of users.
+func (s *UsersStore) Count(ctx context.Context) int64 {
 	var count int64
-	s.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeIndividual).Count(&count)
+	s.db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeIndividual).Count(&count)
 	return count
 }
 
@@ -362,8 +260,7 @@ type ErrUserAlreadyExist struct {
 // IsErrUserAlreadyExist returns true if the underlying error has the type
 // ErrUserAlreadyExist.
 func IsErrUserAlreadyExist(err error) bool {
-	_, ok := errors.Cause(err).(ErrUserAlreadyExist)
-	return ok
+	return errors.As(err, &ErrUserAlreadyExist{})
 }
 
 func (err ErrUserAlreadyExist) Error() string {
@@ -377,8 +274,7 @@ type ErrEmailAlreadyUsed struct {
 // IsErrEmailAlreadyUsed returns true if the underlying error has the type
 // ErrEmailAlreadyUsed.
 func IsErrEmailAlreadyUsed(err error) bool {
-	_, ok := errors.Cause(err).(ErrEmailAlreadyUsed)
-	return ok
+	return errors.As(err, &ErrEmailAlreadyUsed{})
 }
 
 func (err ErrEmailAlreadyUsed) Email() string {
@@ -393,7 +289,11 @@ func (err ErrEmailAlreadyUsed) Error() string {
 	return fmt.Sprintf("email has been used: %v", err.args)
 }
 
-func (s *usersStore) Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error) {
+// Create creates a new user and persists to database. It returns
+// ErrNameNotAllowed if the given name or pattern of the name is not allowed as
+// a username, or ErrUserAlreadyExist when a user with same name already exists,
+// or ErrEmailAlreadyUsed if the email has been verified by another user.
+func (s *UsersStore) Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error) {
 	err := isUsernameAllowed(username)
 	if err != nil {
 		return nil, err
@@ -446,17 +346,19 @@ func (s *usersStore) Create(ctx context.Context, username, email string, opts Cr
 	}
 	user.Password = userutil.EncodePassword(user.Password, user.Salt)
 
-	return user, s.WithContext(ctx).Create(user).Error
+	return user, s.db.WithContext(ctx).Create(user).Error
 }
 
-func (s *usersStore) DeleteCustomAvatar(ctx context.Context, userID int64) error {
+// DeleteCustomAvatar deletes the current user custom avatar and falls back to
+// use look up avatar by email.
+func (s *UsersStore) DeleteCustomAvatar(ctx context.Context, userID int64) error {
 	_ = os.Remove(userutil.CustomAvatarPath(userID))
-	return s.WithContext(ctx).
+	return s.db.WithContext(ctx).
 		Model(&User{}).
 		Where("id = ?", userID).
 		Updates(map[string]any{
 			"use_custom_avatar": false,
-			"updated_unix":      s.NowFunc().Unix(),
+			"updated_unix":      s.db.NowFunc().Unix(),
 		}).
 		Error
 }
@@ -468,8 +370,7 @@ type ErrUserOwnRepos struct {
 // IsErrUserOwnRepos returns true if the underlying error has the type
 // ErrUserOwnRepos.
 func IsErrUserOwnRepos(err error) bool {
-	_, ok := errors.Cause(err).(ErrUserOwnRepos)
-	return ok
+	return errors.As(err, &ErrUserOwnRepos{})
 }
 
 func (err ErrUserOwnRepos) Error() string {
@@ -483,15 +384,19 @@ type ErrUserHasOrgs struct {
 // IsErrUserHasOrgs returns true if the underlying error has the type
 // ErrUserHasOrgs.
 func IsErrUserHasOrgs(err error) bool {
-	_, ok := errors.Cause(err).(ErrUserHasOrgs)
-	return ok
+	return errors.As(err, &ErrUserHasOrgs{})
 }
 
 func (err ErrUserHasOrgs) Error() string {
 	return fmt.Sprintf("user still has organization membership: %v", err.args)
 }
 
-func (s *usersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error {
+// DeleteByID deletes the given user and all their resources. It returns
+// ErrUserOwnRepos when the user still has repository ownership, or returns
+// ErrUserHasOrgs when the user still has organization membership. It is more
+// performant to skip rewriting the "authorized_keys" file for individual
+// deletion in a batch operation.
+func (s *UsersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error {
 	user, err := s.GetByID(ctx, userID)
 	if err != nil {
 		if IsErrUserNotExist(err) {
@@ -500,17 +405,17 @@ func (s *usersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAu
 		return errors.Wrap(err, "get user")
 	}
 
-	// Double check the user is not a direct owner of any repository and not a
+	// Double-check the user is not a direct owner of any repository and not a
 	// member of any organization.
 	var count int64
-	err = s.WithContext(ctx).Model(&Repository{}).Where("owner_id = ?", userID).Count(&count).Error
+	err = s.db.WithContext(ctx).Model(&Repository{}).Where("owner_id = ?", userID).Count(&count).Error
 	if err != nil {
 		return errors.Wrap(err, "count repositories")
 	} else if count > 0 {
 		return ErrUserOwnRepos{args: errutil.Args{"userID": userID}}
 	}
 
-	err = s.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
+	err = s.db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
 	if err != nil {
 		return errors.Wrap(err, "count organization membership")
 	} else if count > 0 {
@@ -518,7 +423,7 @@ func (s *usersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAu
 	}
 
 	needsRewriteAuthorizedKeys := false
-	err = s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		/*
 			Equivalent SQL for PostgreSQL:
 
@@ -645,7 +550,7 @@ func (s *usersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAu
 	_ = os.Remove(userutil.CustomAvatarPath(userID))
 
 	if needsRewriteAuthorizedKeys {
-		err = newPublicKeysStore(s.DB).RewriteAuthorizedKeys()
+		err = newPublicKeysStore(s.db).RewriteAuthorizedKeys()
 		if err != nil {
 			return errors.Wrap(err, `rewrite "authorized_keys" file`)
 		}
@@ -653,11 +558,13 @@ func (s *usersStore) DeleteByID(ctx context.Context, userID int64, skipRewriteAu
 	return nil
 }
 
+// DeleteInactivated deletes all inactivated users.
+//
 // NOTE: We do not take context.Context here because this operation in practice
 // could much longer than the general request timeout (e.g. one minute).
-func (s *usersStore) DeleteInactivated() error {
+func (s *UsersStore) DeleteInactivated() error {
 	var userIDs []int64
-	err := s.Model(&User{}).Where("is_active = ?", false).Pluck("id", &userIDs).Error
+	err := s.db.Model(&User{}).Where("is_active = ?", false).Pluck("id", &userIDs).Error
 	if err != nil {
 		return errors.Wrap(err, "get inactivated user IDs")
 	}
@@ -672,14 +579,14 @@ func (s *usersStore) DeleteInactivated() error {
 			return errors.Wrapf(err, "delete user with ID %d", userID)
 		}
 	}
-	err = newPublicKeysStore(s.DB).RewriteAuthorizedKeys()
+	err = newPublicKeysStore(s.db).RewriteAuthorizedKeys()
 	if err != nil {
 		return errors.Wrap(err, `rewrite "authorized_keys" file`)
 	}
 	return nil
 }
 
-func (*usersStore) recountFollows(tx *gorm.DB, userID, followID int64) error {
+func (*UsersStore) recountFollows(tx *gorm.DB, userID, followID int64) error {
 	/*
 		Equivalent SQL for PostgreSQL:
 
@@ -722,12 +629,13 @@ func (*usersStore) recountFollows(tx *gorm.DB, userID, followID int64) error {
 	return nil
 }
 
-func (s *usersStore) Follow(ctx context.Context, userID, followID int64) error {
+// Follow marks the user to follow the other user.
+func (s *UsersStore) Follow(ctx context.Context, userID, followID int64) error {
 	if userID == followID {
 		return nil
 	}
 
-	return s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		f := &Follow{
 			UserID:   userID,
 			FollowID: followID,
@@ -743,12 +651,13 @@ func (s *usersStore) Follow(ctx context.Context, userID, followID int64) error {
 	})
 }
 
-func (s *usersStore) Unfollow(ctx context.Context, userID, followID int64) error {
+// Unfollow removes the mark the user to follow the other user.
+func (s *UsersStore) Unfollow(ctx context.Context, userID, followID int64) error {
 	if userID == followID {
 		return nil
 	}
 
-	return s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		err := tx.Where("user_id = ? AND follow_id = ?", userID, followID).Delete(&Follow{}).Error
 		if err != nil {
 			return errors.Wrap(err, "delete")
@@ -757,8 +666,9 @@ func (s *usersStore) Unfollow(ctx context.Context, userID, followID int64) error
 	})
 }
 
-func (s *usersStore) IsFollowing(ctx context.Context, userID, followID int64) bool {
-	return s.WithContext(ctx).Where("user_id = ? AND follow_id = ?", userID, followID).First(&Follow{}).Error == nil
+// IsFollowing returns true if the user is following the other user.
+func (s *UsersStore) IsFollowing(ctx context.Context, userID, followID int64) bool {
+	return s.db.WithContext(ctx).Where("user_id = ? AND follow_id = ?", userID, followID).First(&Follow{}).Error == nil
 }
 
 var _ errutil.NotFound = (*ErrUserNotExist)(nil)
@@ -782,7 +692,9 @@ func (ErrUserNotExist) NotFound() bool {
 	return true
 }
 
-func (s *usersStore) GetByEmail(ctx context.Context, email string) (*User, error) {
+// GetByEmail returns the user (not organization) with given email. It ignores
+// records with unverified emails and returns ErrUserNotExist when not found.
+func (s *UsersStore) GetByEmail(ctx context.Context, email string) (*User, error) {
 	if email == "" {
 		return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
 	}
@@ -801,17 +713,17 @@ func (s *usersStore) GetByEmail(ctx context.Context, email string) (*User, error
 		)
 	*/
 	user := new(User)
-	err := s.WithContext(ctx).
+	err := s.db.WithContext(ctx).
 		Joins(dbutil.Quote("LEFT JOIN email_address ON email_address.uid = %s.id", "user"), true).
 		Where(dbutil.Quote("%s.type = ?", "user"), UserTypeIndividual).
-		Where(s.
+		Where(s.db.
 			Where(dbutil.Quote("%[1]s.email = ? AND %[1]s.is_active = ?", "user"), email, true).
 			Or("email_address.email = ? AND email_address.is_activated = ?", email, true),
 		).
 		First(&user).
 		Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
 		}
 		return nil, err
@@ -819,11 +731,13 @@ func (s *usersStore) GetByEmail(ctx context.Context, email string) (*User, error
 	return user, nil
 }
 
-func (s *usersStore) GetByID(ctx context.Context, id int64) (*User, error) {
+// GetByID returns the user with given ID. It returns ErrUserNotExist when not
+// found.
+func (s *UsersStore) GetByID(ctx context.Context, id int64) (*User, error) {
 	user := new(User)
-	err := s.WithContext(ctx).Where("id = ?", id).First(user).Error
+	err := s.db.WithContext(ctx).Where("id = ?", id).First(user).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrUserNotExist{args: errutil.Args{"userID": id}}
 		}
 		return nil, err
@@ -831,11 +745,13 @@ func (s *usersStore) GetByID(ctx context.Context, id int64) (*User, error) {
 	return user, nil
 }
 
-func (s *usersStore) GetByUsername(ctx context.Context, username string) (*User, error) {
+// GetByUsername returns the user with given username. It returns
+// ErrUserNotExist when not found.
+func (s *UsersStore) GetByUsername(ctx context.Context, username string) (*User, error) {
 	user := new(User)
-	err := s.WithContext(ctx).Where("lower_name = ?", strings.ToLower(username)).First(user).Error
+	err := s.db.WithContext(ctx).Where("lower_name = ?", strings.ToLower(username)).First(user).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrUserNotExist{args: errutil.Args{"name": username}}
 		}
 		return nil, err
@@ -843,15 +759,17 @@ func (s *usersStore) GetByUsername(ctx context.Context, username string) (*User,
 	return user, nil
 }
 
-func (s *usersStore) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
+// GetByKeyID returns the owner of given public key ID. It returns
+// ErrUserNotExist when not found.
+func (s *UsersStore) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
 	user := new(User)
-	err := s.WithContext(ctx).
+	err := s.db.WithContext(ctx).
 		Joins(dbutil.Quote("JOIN public_key ON public_key.owner_id = %s.id", "user")).
 		Where("public_key.id = ?", keyID).
 		First(user).
 		Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrUserNotExist{args: errutil.Args{"keyID": keyID}}
 		}
 		return nil, err
@@ -859,29 +777,37 @@ func (s *usersStore) GetByKeyID(ctx context.Context, keyID int64) (*User, error)
 	return user, nil
 }
 
-func (s *usersStore) GetMailableEmailsByUsernames(ctx context.Context, usernames []string) ([]string, error) {
+// GetMailableEmailsByUsernames returns a list of verified primary email
+// addresses (where email notifications are sent to) of users with given list of
+// usernames. Non-existing usernames are ignored.
+func (s *UsersStore) GetMailableEmailsByUsernames(ctx context.Context, usernames []string) ([]string, error) {
 	emails := make([]string, 0, len(usernames))
-	return emails, s.WithContext(ctx).
+	return emails, s.db.WithContext(ctx).
 		Model(&User{}).
 		Select("email").
 		Where("lower_name IN (?) AND is_active = ?", usernames, true).
 		Find(&emails).Error
 }
 
-func (s *usersStore) IsUsernameUsed(ctx context.Context, username string, excludeUserId int64) bool {
+// IsUsernameUsed returns true if the given username has been used other than
+// the excluded user (a non-positive ID effectively meaning check against all
+// users).
+func (s *UsersStore) IsUsernameUsed(ctx context.Context, username string, excludeUserId int64) bool {
 	if username == "" {
 		return false
 	}
-	return s.WithContext(ctx).
+	return s.db.WithContext(ctx).
 		Select("id").
 		Where("lower_name = ? AND id != ?", strings.ToLower(username), excludeUserId).
 		First(&User{}).
 		Error != gorm.ErrRecordNotFound
 }
 
-func (s *usersStore) List(ctx context.Context, page, pageSize int) ([]*User, error) {
+// List returns a list of users. Results are paginated by given page and page
+// size, and sorted by primary key (id) in ascending order.
+func (s *UsersStore) List(ctx context.Context, page, pageSize int) ([]*User, error) {
 	users := make([]*User, 0, pageSize)
-	return users, s.WithContext(ctx).
+	return users, s.db.WithContext(ctx).
 		Where("type = ?", UserTypeIndividual).
 		Limit(pageSize).Offset((page - 1) * pageSize).
 		Order("id ASC").
@@ -889,7 +815,10 @@ func (s *usersStore) List(ctx context.Context, page, pageSize int) ([]*User, err
 		Error
 }
 
-func (s *usersStore) ListFollowers(ctx context.Context, userID int64, 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.
+func (s *UsersStore) ListFollowers(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
 	/*
 		Equivalent SQL for PostgreSQL:
 
@@ -900,7 +829,7 @@ func (s *usersStore) ListFollowers(ctx context.Context, userID int64, page, page
 		LIMIT @limit OFFSET @offset
 	*/
 	users := make([]*User, 0, pageSize)
-	return users, s.WithContext(ctx).
+	return users, s.db.WithContext(ctx).
 		Joins(dbutil.Quote("LEFT JOIN follow ON follow.user_id = %s.id", "user")).
 		Where("follow.follow_id = ?", userID).
 		Limit(pageSize).Offset((page - 1) * pageSize).
@@ -909,7 +838,10 @@ func (s *usersStore) ListFollowers(ctx context.Context, userID int64, page, page
 		Error
 }
 
-func (s *usersStore) ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
+// ListFollowings returns a list of users that are followed by the given user.
+// Results are paginated by given page and page size, and sorted by the time of
+// follow in descending order.
+func (s *UsersStore) ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
 	/*
 		Equivalent SQL for PostgreSQL:
 
@@ -920,7 +852,7 @@ func (s *usersStore) ListFollowings(ctx context.Context, userID int64, page, pag
 		LIMIT @limit OFFSET @offset
 	*/
 	users := make([]*User, 0, pageSize)
-	return users, s.WithContext(ctx).
+	return users, s.db.WithContext(ctx).
 		Joins(dbutil.Quote("LEFT JOIN follow ON follow.follow_id = %s.id", "user")).
 		Where("follow.user_id = ?", userID).
 		Limit(pageSize).Offset((page - 1) * pageSize).
@@ -948,8 +880,13 @@ func searchUserByName(ctx context.Context, db *gorm.DB, userType UserType, keywo
 	return users, count, tx.Order(orderBy).Limit(pageSize).Offset((page - 1) * pageSize).Find(&users).Error
 }
 
-func (s *usersStore) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*User, int64, error) {
-	return searchUserByName(ctx, s.DB, UserTypeIndividual, keyword, page, pageSize, orderBy)
+// SearchByName returns a list of users whose username or full name matches the
+// given keyword case-insensitively. Results are paginated by given page and
+// page size, and sorted by the given order (e.g. "id DESC"). A total count of
+// all results is also returned. If the order is not given, it's up to the
+// database to decide.
+func (s *UsersStore) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*User, int64, error) {
+	return searchUserByName(ctx, s.db, UserTypeIndividual, keyword, page, pageSize, orderBy)
 }
 
 type UpdateUserOptions struct {
@@ -979,9 +916,10 @@ type UpdateUserOptions struct {
 	AvatarEmail *string
 }
 
-func (s *usersStore) Update(ctx context.Context, userID int64, opts UpdateUserOptions) error {
+// Update updates fields for the given user.
+func (s *UsersStore) Update(ctx context.Context, userID int64, opts UpdateUserOptions) error {
 	updates := map[string]any{
-		"updated_unix": s.NowFunc().Unix(),
+		"updated_unix": s.db.NowFunc().Unix(),
 	}
 
 	if opts.LoginSource != nil {
@@ -1063,26 +1001,29 @@ func (s *usersStore) Update(ctx context.Context, userID int64, opts UpdateUserOp
 		updates["avatar_email"] = strutil.Truncate(*opts.AvatarEmail, 255)
 	}
 
-	return s.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Updates(updates).Error
+	return s.db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Updates(updates).Error
 }
 
-func (s *usersStore) UseCustomAvatar(ctx context.Context, userID int64, avatar []byte) error {
+// UseCustomAvatar uses the given avatar as the user custom avatar.
+func (s *UsersStore) UseCustomAvatar(ctx context.Context, userID int64, avatar []byte) error {
 	err := userutil.SaveAvatar(userID, avatar)
 	if err != nil {
 		return errors.Wrap(err, "save avatar")
 	}
 
-	return s.WithContext(ctx).
+	return s.db.WithContext(ctx).
 		Model(&User{}).
 		Where("id = ?", userID).
 		Updates(map[string]any{
 			"use_custom_avatar": true,
-			"updated_unix":      s.NowFunc().Unix(),
+			"updated_unix":      s.db.NowFunc().Unix(),
 		}).
 		Error
 }
 
-func (s *usersStore) AddEmail(ctx context.Context, userID int64, email string, isActivated bool) error {
+// AddEmail adds a new email address to given user. It returns
+// ErrEmailAlreadyUsed if the email has been verified by another user.
+func (s *UsersStore) AddEmail(ctx context.Context, userID int64, email string, isActivated bool) error {
 	email = strings.ToLower(strings.TrimSpace(email))
 	_, err := s.GetByEmail(ctx, email)
 	if err == nil {
@@ -1095,7 +1036,7 @@ func (s *usersStore) AddEmail(ctx context.Context, userID int64, email string, i
 		return errors.Wrap(err, "check user by email")
 	}
 
-	return s.WithContext(ctx).Create(
+	return s.db.WithContext(ctx).Create(
 		&EmailAddress{
 			UserID:      userID,
 			Email:       email,
@@ -1125,8 +1066,12 @@ func (ErrEmailNotExist) NotFound() bool {
 	return true
 }
 
-func (s *usersStore) GetEmail(ctx context.Context, userID int64, email string, needsActivated bool) (*EmailAddress, error) {
-	tx := s.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
+// GetEmail returns the email address of the given user. If `needsActivated` is
+// true, only activated email will be returned, otherwise, it may return
+// inactivated email addresses. It returns ErrEmailNotExist when no qualified
+// email is not found.
+func (s *UsersStore) GetEmail(ctx context.Context, userID int64, email string, needsActivated bool) (*EmailAddress, error) {
+	tx := s.db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
 	if needsActivated {
 		tx = tx.Where("is_activated = ?", true)
 	}
@@ -1134,7 +1079,7 @@ func (s *usersStore) GetEmail(ctx context.Context, userID int64, email string, n
 	emailAddress := new(EmailAddress)
 	err := tx.First(emailAddress).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrEmailNotExist{
 				args: errutil.Args{
 					"email": email,
@@ -1146,14 +1091,16 @@ func (s *usersStore) GetEmail(ctx context.Context, userID int64, email string, n
 	return emailAddress, nil
 }
 
-func (s *usersStore) ListEmails(ctx context.Context, userID int64) ([]*EmailAddress, error) {
+// ListEmails returns all email addresses of the given user. It always includes
+// a primary email address.
+func (s *UsersStore) ListEmails(ctx context.Context, userID int64) ([]*EmailAddress, error) {
 	user, err := s.GetByID(ctx, userID)
 	if err != nil {
 		return nil, errors.Wrap(err, "get user")
 	}
 
 	var emails []*EmailAddress
-	err = s.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&emails).Error
+	err = s.db.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&emails).Error
 	if err != nil {
 		return nil, errors.Wrap(err, "list emails")
 	}
@@ -1179,9 +1126,11 @@ func (s *usersStore) ListEmails(ctx context.Context, userID int64) ([]*EmailAddr
 	return emails, nil
 }
 
-func (s *usersStore) MarkEmailActivated(ctx context.Context, userID int64, email string) error {
-	return s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
-		err := s.WithContext(ctx).
+// MarkEmailActivated marks the email address of the given user as activated,
+// and new rands are generated for the user.
+func (s *UsersStore) MarkEmailActivated(ctx context.Context, userID int64, email string) error {
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		err := s.db.WithContext(ctx).
 			Model(&EmailAddress{}).
 			Where("uid = ? AND email = ?", userID, email).
 			Update("is_activated", true).
@@ -1190,7 +1139,7 @@ func (s *usersStore) MarkEmailActivated(ctx context.Context, userID int64, email
 			return errors.Wrap(err, "mark email activated")
 		}
 
-		return NewUsersStore(tx).Update(ctx, userID, UpdateUserOptions{GenerateNewRands: true})
+		return newUsersStore(tx).Update(ctx, userID, UpdateUserOptions{GenerateNewRands: true})
 	})
 }
 
@@ -1209,11 +1158,14 @@ func (err ErrEmailNotVerified) Error() string {
 	return fmt.Sprintf("email has not been verified: %v", err.args)
 }
 
-func (s *usersStore) MarkEmailPrimary(ctx context.Context, userID int64, email string) error {
+// MarkEmailPrimary marks the email address of the given user as primary. It
+// returns ErrEmailNotExist when the email is not found for the user, and
+// ErrEmailNotActivated when the email is not activated.
+func (s *UsersStore) MarkEmailPrimary(ctx context.Context, userID int64, email string) error {
 	var emailAddress EmailAddress
-	err := s.WithContext(ctx).Where("uid = ? AND email = ?", userID, email).First(&emailAddress).Error
+	err := s.db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email).First(&emailAddress).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return ErrEmailNotExist{args: errutil.Args{"email": email}}
 		}
 		return errors.Wrap(err, "get email address")
@@ -1228,7 +1180,7 @@ func (s *usersStore) MarkEmailPrimary(ctx context.Context, userID int64, email s
 		return errors.Wrap(err, "get user")
 	}
 
-	return s.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		// Make sure the former primary email doesn't disappear.
 		err = tx.FirstOrCreate(
 			&EmailAddress{
@@ -1255,8 +1207,9 @@ func (s *usersStore) MarkEmailPrimary(ctx context.Context, userID int64, email s
 	})
 }
 
-func (s *usersStore) DeleteEmail(ctx context.Context, userID int64, email string) error {
-	return s.WithContext(ctx).Where("uid = ? AND email = ?", userID, email).Delete(&EmailAddress{}).Error
+// DeleteEmail deletes the email address of the given user.
+func (s *UsersStore) DeleteEmail(ctx context.Context, userID int64, email string) error {
+	return s.db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email).Delete(&EmailAddress{}).Error
 }
 
 // UserType indicates the type of the user account.
@@ -1464,7 +1417,7 @@ func (u *User) AvatarURL() string {
 // TODO(unknwon): This is also used in templates, which should be fixed by
 // having a dedicated type `template.User`.
 func (u *User) IsFollowing(followID int64) bool {
-	return Users.IsFollowing(context.TODO(), u.ID, followID)
+	return Handle.Users().IsFollowing(context.TODO(), u.ID, followID)
 }
 
 // IsUserOrgOwner returns true if the user is in the owner team of the given

File diff suppressed because it is too large
+ 177 - 177
internal/database/users_test.go


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

@@ -145,7 +145,7 @@ func Operation(c *context.Context) {
 	switch AdminOperation(c.QueryInt("op")) {
 	case CleanInactivateUser:
 		success = c.Tr("admin.dashboard.delete_inactivate_accounts_success")
-		err = database.Users.DeleteInactivated()
+		err = database.Handle.Users().DeleteInactivated()
 	case CleanRepoArchives:
 		success = c.Tr("admin.dashboard.delete_repo_archives_success")
 		err = database.DeleteRepositoryArchives()

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

@@ -31,8 +31,8 @@ func Users(c *context.Context) {
 
 	route.RenderUserSearch(c, &route.UserSearchOptions{
 		Type:     database.UserTypeIndividual,
-		Counter:  database.Users.Count,
-		Ranger:   database.Users.List,
+		Counter:  database.Handle.Users().Count,
+		Ranger:   database.Handle.Users().List,
 		PageSize: conf.UI.Admin.UserPagingNum,
 		OrderBy:  "id ASC",
 		TplName:  USERS,
@@ -88,7 +88,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 		}
 	}
 
-	user, err := database.Users.Create(c.Req.Context(), f.UserName, f.Email, createUserOpts)
+	user, err := database.Handle.Users().Create(c.Req.Context(), f.UserName, f.Email, createUserOpts)
 	if err != nil {
 		switch {
 		case database.IsErrUserAlreadyExist(err):
@@ -117,7 +117,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 }
 
 func prepareUserInfo(c *context.Context) *database.User {
-	u, err := database.Users.GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
+	u, err := database.Handle.Users().GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return nil
@@ -203,7 +203,7 @@ func EditUserPost(c *context.Context, f form.AdminEditUser) {
 		opts.Email = &f.Email
 	}
 
-	err := database.Users.Update(c.Req.Context(), u.ID, opts)
+	err := database.Handle.Users().Update(c.Req.Context(), u.ID, opts)
 	if err != nil {
 		if database.IsErrEmailAlreadyUsed(err) {
 			c.Data["Err_Email"] = true
@@ -220,13 +220,13 @@ func EditUserPost(c *context.Context, f form.AdminEditUser) {
 }
 
 func DeleteUser(c *context.Context) {
-	u, err := database.Users.GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
+	u, err := database.Handle.Users().GetByID(c.Req.Context(), c.ParamsInt64(":userid"))
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return
 	}
 
-	if err = database.Users.DeleteByID(c.Req.Context(), u.ID, false); err != nil {
+	if err = database.Handle.Users().DeleteByID(c.Req.Context(), u.ID, false); err != nil {
 		switch {
 		case database.IsErrUserOwnRepos(err):
 			c.Flash.Error(c.Tr("admin.users.still_own_repo"))

+ 4 - 4
internal/route/api/v1/admin/user.go

@@ -39,7 +39,7 @@ func CreateUser(c *context.APIContext, form api.CreateUserOption) {
 		return
 	}
 
-	user, err := database.Users.Create(
+	user, err := database.Handle.Users().Create(
 		c.Req.Context(),
 		form.Username,
 		form.Email,
@@ -104,7 +104,7 @@ func EditUser(c *context.APIContext, form api.EditUserOption) {
 		opts.Email = &form.Email
 	}
 
-	err := database.Users.Update(c.Req.Context(), u.ID, opts)
+	err := database.Handle.Users().Update(c.Req.Context(), u.ID, opts)
 	if err != nil {
 		if database.IsErrEmailAlreadyUsed(err) {
 			c.ErrorStatus(http.StatusUnprocessableEntity, err)
@@ -115,7 +115,7 @@ func EditUser(c *context.APIContext, form api.EditUserOption) {
 	}
 	log.Trace("Account updated by admin %q: %s", c.User.Name, u.Name)
 
-	u, err = database.Users.GetByID(c.Req.Context(), u.ID)
+	u, err = database.Handle.Users().GetByID(c.Req.Context(), u.ID)
 	if err != nil {
 		c.Error(err, "get user")
 		return
@@ -129,7 +129,7 @@ func DeleteUser(c *context.APIContext) {
 		return
 	}
 
-	if err := database.Users.DeleteByID(c.Req.Context(), u.ID, false); err != nil {
+	if err := database.Handle.Users().DeleteByID(c.Req.Context(), u.ID, false); err != nil {
 		if database.IsErrUserOwnRepos(err) ||
 			database.IsErrUserHasOrgs(err) {
 			c.ErrorStatus(http.StatusUnprocessableEntity, err)

+ 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 = database.Users.GetByUsername(c.Req.Context(), username)
+			owner, err = database.Handle.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 = database.Users.GetByUsername(c.Req.Context(), c.Params(":orgname"))
+			c.Org.Organization, err = database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":orgname"))
 			if err != nil {
 				c.NotFoundOrError(err, "get organization by name")
 				return

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

@@ -45,12 +45,12 @@ func ToTag(b *database.Tag, c *git.Commit) *Tag {
 
 func ToCommit(c *git.Commit) *api.PayloadCommit {
 	authorUsername := ""
-	author, err := database.Users.GetByEmail(context.TODO(), c.Author.Email)
+	author, err := database.Handle.Users().GetByEmail(context.TODO(), c.Author.Email)
 	if err == nil {
 		authorUsername = author.Name
 	}
 	committerUsername := ""
-	committer, err := database.Users.GetByEmail(context.TODO(), c.Committer.Email)
+	committer, err := database.Handle.Users().GetByEmail(context.TODO(), c.Committer.Email)
 	if err == nil {
 		committerUsername = committer.Name
 	}

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

@@ -89,7 +89,7 @@ func Edit(c *context.APIContext, form api.EditOrgOption) {
 		return
 	}
 
-	err := database.Users.Update(
+	err := database.Handle.Users().Update(
 		c.Req.Context(),
 		c.Org.Organization.ID,
 		database.UpdateUserOptions{

+ 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 := database.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
+	collaborator, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if database.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 := database.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
+	collaborator, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			c.Status(http.StatusUnprocessableEntity)
@@ -72,7 +72,7 @@ func IsCollaborator(c *context.APIContext) {
 }
 
 func DeleteCollaborator(c *context.APIContext) {
-	collaborator, err := database.Users.GetByUsername(c.Req.Context(), c.Params(":collaborator"))
+	collaborator, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":collaborator"))
 	if err != nil {
 		if database.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 := database.Users.GetByEmail(c.Req.Context(), commit.Author.Email)
+	author, err := database.Handle.Users().GetByEmail(c.Req.Context(), commit.Author.Email)
 	if err != nil && !database.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 := database.Users.GetByEmail(c.Req.Context(), commit.Committer.Email)
+		committer, err := database.Handle.Users().GetByEmail(c.Req.Context(), commit.Committer.Email)
 		if err != nil && !database.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 := database.Users.GetByUsername(c.Req.Context(), form.Assignee)
+			assignee, err := database.Handle.Users().GetByUsername(c.Req.Context(), form.Assignee)
 			if err != nil {
 				if database.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 := database.Users.GetByUsername(c.Req.Context(), *form.Assignee)
+			assignee, err := database.Handle.Users().GetByUsername(c.Req.Context(), *form.Assignee)
 			if err != nil {
 				if database.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 := database.Users.GetByID(c.Req.Context(), opts.OwnerID)
+			u, err := database.Handle.Users().GetByID(c.Req.Context(), opts.OwnerID)
 			if err != nil {
 				c.JSON(http.StatusInternalServerError, map[string]any{
 					"ok":    false,
@@ -77,7 +77,7 @@ func Search(c *context.APIContext) {
 }
 
 func listUserRepositories(c *context.APIContext, username string) {
-	user, err := database.Users.GetByUsername(c.Req.Context(), username)
+	user, err := database.Handle.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 := database.Users.GetByID(c.Req.Context(), f.Uid)
+		org, err := database.Handle.Users().GetByID(c.Req.Context(), f.Uid)
 		if err != nil {
 			if database.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) (*database.User, *database.Repository) {
-	owner, err := database.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
+	owner, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":username"))
 	if err != nil {
 		if database.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 := database.Users.GetByID(c.Req.Context(), r.PublisherID)
+		publisher, err := database.Handle.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

@@ -17,7 +17,7 @@ import (
 )
 
 func ListEmails(c *context.APIContext) {
-	emails, err := database.Users.ListEmails(c.Req.Context(), c.User.ID)
+	emails, err := database.Handle.Users().ListEmails(c.Req.Context(), c.User.ID)
 	if err != nil {
 		c.Error(err, "get email addresses")
 		return
@@ -37,7 +37,7 @@ func AddEmail(c *context.APIContext, form api.CreateEmailOption) {
 
 	apiEmails := make([]*api.Email, 0, len(form.Emails))
 	for _, email := range form.Emails {
-		err := database.Users.AddEmail(c.Req.Context(), c.User.ID, email, !conf.Auth.RequireEmailConfirmation)
+		err := database.Handle.Users().AddEmail(c.Req.Context(), c.User.ID, email, !conf.Auth.RequireEmailConfirmation)
 		if err != nil {
 			if database.IsErrEmailAlreadyUsed(err) {
 				c.ErrorStatus(http.StatusUnprocessableEntity, errors.Errorf("email address has been used: %s", err.(database.ErrEmailAlreadyUsed).Email()))
@@ -64,7 +64,7 @@ func DeleteEmail(c *context.APIContext, form api.CreateEmailOption) {
 			return
 		}
 
-		err := database.Users.DeleteEmail(c.Req.Context(), c.User.ID, email)
+		err := database.Handle.Users().DeleteEmail(c.Req.Context(), c.User.ID, email)
 		if err != nil {
 			c.Error(err, "delete email addresses")
 			return

+ 5 - 5
internal/route/api/v1/user/follower.go

@@ -20,7 +20,7 @@ func responseApiUsers(c *context.APIContext, users []*database.User) {
 }
 
 func listUserFollowers(c *context.APIContext, u *database.User) {
-	users, err := database.Users.ListFollowers(c.Req.Context(), u.ID, c.QueryInt("page"), database.ItemsPerPage)
+	users, err := database.Handle.Users().ListFollowers(c.Req.Context(), u.ID, c.QueryInt("page"), database.ItemsPerPage)
 	if err != nil {
 		c.Error(err, "list followers")
 		return
@@ -41,7 +41,7 @@ func ListFollowers(c *context.APIContext) {
 }
 
 func listUserFollowing(c *context.APIContext, u *database.User) {
-	users, err := database.Users.ListFollowings(c.Req.Context(), u.ID, c.QueryInt("page"), database.ItemsPerPage)
+	users, err := database.Handle.Users().ListFollowings(c.Req.Context(), u.ID, c.QueryInt("page"), database.ItemsPerPage)
 	if err != nil {
 		c.Error(err, "list followings")
 		return
@@ -62,7 +62,7 @@ func ListFollowing(c *context.APIContext) {
 }
 
 func checkUserFollowing(c *context.APIContext, u *database.User, followID int64) {
-	if database.Users.IsFollowing(c.Req.Context(), u.ID, followID) {
+	if database.Handle.Users().IsFollowing(c.Req.Context(), u.ID, followID) {
 		c.NoContent()
 	} else {
 		c.NotFound()
@@ -94,7 +94,7 @@ func Follow(c *context.APIContext) {
 	if c.Written() {
 		return
 	}
-	if err := database.Users.Follow(c.Req.Context(), c.User.ID, target.ID); err != nil {
+	if err := database.Handle.Users().Follow(c.Req.Context(), c.User.ID, target.ID); err != nil {
 		c.Error(err, "follow user")
 		return
 	}
@@ -106,7 +106,7 @@ func Unfollow(c *context.APIContext) {
 	if c.Written() {
 		return
 	}
-	if err := database.Users.Unfollow(c.Req.Context(), c.User.ID, target.ID); err != nil {
+	if err := database.Handle.Users().Unfollow(c.Req.Context(), c.User.ID, target.ID); err != nil {
 		c.Error(err, "unfollow user")
 		return
 	}

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

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

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

@@ -19,7 +19,7 @@ func Search(c *context.APIContext) {
 	if pageSize <= 0 {
 		pageSize = 10
 	}
-	users, _, err := database.Users.SearchByName(c.Req.Context(), c.Query("q"), 1, pageSize, "")
+	users, _, err := database.Handle.Users().SearchByName(c.Req.Context(), c.Query("q"), 1, pageSize, "")
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, map[string]any{
 			"ok":    false,
@@ -48,7 +48,7 @@ func Search(c *context.APIContext) {
 }
 
 func GetInfo(c *context.APIContext) {
-	u, err := database.Users.GetByUsername(c.Req.Context(), c.Params(":username"))
+	u, err := database.Handle.Users().GetByUsername(c.Req.Context(), c.Params(":username"))
 	if err != nil {
 		c.NotFoundOrError(err, "get user by name")
 		return

+ 3 - 3
internal/route/home.go

@@ -113,7 +113,7 @@ func RenderUserSearch(c *context.Context, opts *UserSearchOptions) {
 		}
 		count = opts.Counter(c.Req.Context())
 	} else {
-		search := database.Users.SearchByName
+		search := database.Handle.Users().SearchByName
 		if opts.Type == database.UserTypeOrganization {
 			search = database.Handle.Organizations().SearchByName
 		}
@@ -138,8 +138,8 @@ func ExploreUsers(c *context.Context) {
 
 	RenderUserSearch(c, &UserSearchOptions{
 		Type:     database.UserTypeIndividual,
-		Counter:  database.Users.Count,
-		Ranger:   database.Users.List,
+		Counter:  database.Handle.Users().Count,
+		Ranger:   database.Handle.Users().List,
 		PageSize: conf.UI.ExplorePagingNum,
 		OrderBy:  "updated_unix DESC",
 		TplName:  EXPLORE_USERS,

+ 2 - 2
internal/route/install.go

@@ -391,7 +391,7 @@ func InstallPost(c *context.Context, f form.Install) {
 
 	// Create admin account
 	if len(f.AdminName) > 0 {
-		user, err := database.Users.Create(
+		user, err := database.Handle.Users().Create(
 			c.Req.Context(),
 			f.AdminName,
 			f.AdminEmail,
@@ -410,7 +410,7 @@ func InstallPost(c *context.Context, f form.Install) {
 			}
 
 			log.Info("Admin account already exist")
-			user, err = database.Users.GetByUsername(c.Req.Context(), f.AdminName)
+			user, err = database.Handle.Users().GetByUsername(c.Req.Context(), f.AdminName)
 			if err != nil {
 				c.Error(err, "get user by name")
 				return

File diff suppressed because it is too large
+ 171 - 3524
internal/route/lfs/mocks_test.go


+ 2 - 2
internal/route/lfs/route.go

@@ -62,7 +62,7 @@ func authenticate(store Store) macaron.Handler {
 			return
 		}
 
-		user, err := database.Users.Authenticate(c.Req.Context(), username, password, -1)
+		user, err := store.AuthenticateUser(c.Req.Context(), username, password, -1)
 		if err != nil && !auth.IsErrBadCredentials(err) {
 			internalServerError(c.Resp)
 			log.Error("Failed to authenticate user [name: %s]: %v", username, err)
@@ -109,7 +109,7 @@ func authorize(store Store, mode database.AccessMode) macaron.Handler {
 		username := c.Params(":username")
 		reponame := strings.TrimSuffix(c.Params(":reponame"), ".git")
 
-		owner, err := database.Users.GetByUsername(c.Req.Context(), username)
+		owner, err := store.GetUserByUsername(c.Req.Context(), username)
 		if err != nil {
 			if database.IsErrUserNotExist(err) {
 				c.Status(http.StatusNotFound)

+ 31 - 71
internal/route/lfs/route_test.go

@@ -22,13 +22,12 @@ import (
 
 func TestAuthenticate(t *testing.T) {
 	tests := []struct {
-		name           string
-		header         http.Header
-		mockUsersStore func() database.UsersStore
-		mockStore      func() *MockStore
-		expStatusCode  int
-		expHeader      http.Header
-		expBody        string
+		name          string
+		header        http.Header
+		mockStore     func() *MockStore
+		expStatusCode int
+		expHeader     http.Header
+		expBody       string
 	}{
 		{
 			name:          "no authorization",
@@ -44,14 +43,10 @@ func TestAuthenticate(t *testing.T) {
 			header: http.Header{
 				"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
 			},
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.AuthenticateFunc.SetDefaultReturn(&database.User{}, nil)
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.IsTwoFactorEnabledFunc.SetDefaultReturn(true)
+				mockStore.AuthenticateUserFunc.SetDefaultReturn(&database.User{}, nil)
 				return mockStore
 			},
 			expStatusCode: http.StatusBadRequest,
@@ -63,14 +58,10 @@ func TestAuthenticate(t *testing.T) {
 			header: http.Header{
 				"Authorization": []string{"Basic dXNlcm5hbWU="},
 			},
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{})
+				mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
 				return mockStore
 			},
 			expStatusCode: http.StatusUnauthorized,
@@ -86,14 +77,10 @@ func TestAuthenticate(t *testing.T) {
 			header: http.Header{
 				"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
 			},
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.AuthenticateFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.IsTwoFactorEnabledFunc.SetDefaultReturn(false)
+				mockStore.AuthenticateUserFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
 				return mockStore
 			},
 			expStatusCode: http.StatusOK,
@@ -105,15 +92,11 @@ func TestAuthenticate(t *testing.T) {
 			header: http.Header{
 				"Authorization": []string{"Basic dXNlcm5hbWU="},
 			},
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
-				mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil)
+				mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
+				mockStore.GetUserByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
 				return mockStore
 			},
 			expStatusCode: http.StatusOK,
@@ -125,12 +108,6 @@ func TestAuthenticate(t *testing.T) {
 			header: http.Header{
 				"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
 			},
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
-				mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.GetAccessTokenBySHA1Func.SetDefaultHook(func(_ context.Context, sha1 string) (*database.AccessToken, error) {
@@ -139,6 +116,8 @@ func TestAuthenticate(t *testing.T) {
 					}
 					return nil, database.ErrAccessTokenNotExist{}
 				})
+				mockStore.AuthenticateUserFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
+				mockStore.GetUserByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
 				return mockStore
 			},
 			expStatusCode: http.StatusOK,
@@ -148,9 +127,6 @@ func TestAuthenticate(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			if test.mockUsersStore != nil {
-				database.SetMockUsersStore(t, test.mockUsersStore())
-			}
 			if test.mockStore == nil {
 				test.mockStore = NewMockStore
 			}
@@ -185,36 +161,31 @@ func TestAuthenticate(t *testing.T) {
 
 func TestAuthorize(t *testing.T) {
 	tests := []struct {
-		name           string
-		accessMode     database.AccessMode
-		mockUsersStore func() database.UsersStore
-		mockStore      func() *MockStore
-		expStatusCode  int
-		expBody        string
+		name          string
+		accessMode    database.AccessMode
+		mockStore     func() *MockStore
+		expStatusCode int
+		expBody       string
 	}{
 		{
 			name:       "user does not exist",
 			accessMode: database.AccessModeNone,
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.GetByUsernameFunc.SetDefaultReturn(nil, database.ErrUserNotExist{})
-				return mock
+			mockStore: func() *MockStore {
+				mockStore := NewMockStore()
+				mockStore.GetUserByUsernameFunc.SetDefaultReturn(nil, database.ErrUserNotExist{})
+				return mockStore
 			},
 			expStatusCode: http.StatusNotFound,
 		},
 		{
 			name:       "repository does not exist",
 			accessMode: database.AccessModeNone,
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
-					return &database.User{Name: username}, nil
-				})
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.GetRepositoryByNameFunc.SetDefaultReturn(nil, database.ErrRepoNotExist{})
+				mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
+					return &database.User{Name: username}, nil
+				})
 				return mockStore
 			},
 			expStatusCode: http.StatusNotFound,
@@ -222,13 +193,6 @@ func TestAuthorize(t *testing.T) {
 		{
 			name:       "actor is not authorized",
 			accessMode: database.AccessModeWrite,
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
-					return &database.User{Name: username}, nil
-				})
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.AuthorizeRepositoryAccessFunc.SetDefaultHook(func(_ context.Context, _ int64, _ int64, desired database.AccessMode, _ database.AccessModeOptions) bool {
@@ -237,6 +201,9 @@ func TestAuthorize(t *testing.T) {
 				mockStore.GetRepositoryByNameFunc.SetDefaultHook(func(ctx context.Context, ownerID int64, name string) (*database.Repository, error) {
 					return &database.Repository{Name: name}, nil
 				})
+				mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
+					return &database.User{Name: username}, nil
+				})
 				return mockStore
 			},
 			expStatusCode: http.StatusNotFound,
@@ -245,13 +212,6 @@ func TestAuthorize(t *testing.T) {
 		{
 			name:       "actor is authorized",
 			accessMode: database.AccessModeRead,
-			mockUsersStore: func() database.UsersStore {
-				mock := NewMockUsersStore()
-				mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
-					return &database.User{Name: username}, nil
-				})
-				return mock
-			},
 			mockStore: func() *MockStore {
 				mockStore := NewMockStore()
 				mockStore.AuthorizeRepositoryAccessFunc.SetDefaultHook(func(_ context.Context, _ int64, _ int64, desired database.AccessMode, _ database.AccessModeOptions) bool {
@@ -260,6 +220,9 @@ func TestAuthorize(t *testing.T) {
 				mockStore.GetRepositoryByNameFunc.SetDefaultHook(func(ctx context.Context, ownerID int64, name string) (*database.Repository, error) {
 					return &database.Repository{Name: name}, nil
 				})
+				mockStore.GetUserByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*database.User, error) {
+					return &database.User{Name: username}, nil
+				})
 				return mockStore
 			},
 			expStatusCode: http.StatusOK,
@@ -268,9 +231,6 @@ func TestAuthorize(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			if test.mockUsersStore != nil {
-				database.SetMockUsersStore(t, test.mockUsersStore())
-			}
 			mockStore := NewMockStore()
 			if test.mockStore != nil {
 				mockStore = test.mockStore()

+ 42 - 0
internal/route/lfs/store.go

@@ -37,6 +37,32 @@ type Store interface {
 
 	// IsTwoFactorEnabled returns true if the user has enabled 2FA.
 	IsTwoFactorEnabled(ctx context.Context, userID int64) bool
+
+	// GetUserByID returns the user with given ID. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByID(ctx context.Context, id int64) (*database.User, error)
+	// GetUserByUsername returns the user with given username. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByUsername(ctx context.Context, username string) (*database.User, error)
+	// CreateUser creates a new user and persists to database. It returns
+	// database.ErrNameNotAllowed if the given name or pattern of the name is not
+	// allowed as a username, or database.ErrUserAlreadyExist when a user with same
+	// name already exists, or database.ErrEmailAlreadyUsed if the email has been
+	// verified by another user.
+	CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error)
+	// AuthenticateUser validates username and password via given login source ID.
+	// It returns database.ErrUserNotExist when the user was not found.
+	//
+	// When the "loginSourceID" is negative, it aborts the process and returns
+	// database.ErrUserNotExist if the user was not found in the database.
+	//
+	// When the "loginSourceID" is non-negative, it returns
+	// database.ErrLoginSourceMismatch if the user has different login source ID
+	// than the "loginSourceID".
+	//
+	// 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.
+	AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error)
 }
 
 type store struct{}
@@ -77,3 +103,19 @@ func (*store) GetRepositoryByName(ctx context.Context, ownerID int64, name strin
 func (*store) IsTwoFactorEnabled(ctx context.Context, userID int64) bool {
 	return database.Handle.TwoFactors().IsEnabled(ctx, userID)
 }
+
+func (*store) GetUserByID(ctx context.Context, id int64) (*database.User, error) {
+	return database.Handle.Users().GetByID(ctx, id)
+}
+
+func (*store) GetUserByUsername(ctx context.Context, username string) (*database.User, error) {
+	return database.Handle.Users().GetByUsername(ctx, username)
+}
+
+func (*store) CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error) {
+	return database.Handle.Users().Create(ctx, username, email, opts)
+}
+
+func (*store) AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error) {
+	return database.Handle.Users().Authenticate(ctx, login, password, loginSourceID)
+}

+ 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 := database.Users.GetByUsername(c.Req.Context(), uname)
+		u, err := database.Handle.Users().GetByUsername(c.Req.Context(), uname)
 		if err != nil {
 			if database.IsErrUserNotExist(err) {
 				c.Flash.Error(c.Tr("form.user_not_exist"))

+ 4 - 4
internal/route/org/setting.go

@@ -39,7 +39,7 @@ func SettingsPost(c *context.Context, f form.UpdateOrgSetting) {
 
 	// Check if the organization username (including cases) had been changed
 	if org.Name != f.Name {
-		err := database.Users.ChangeUsername(c.Req.Context(), c.Org.Organization.ID, f.Name)
+		err := database.Handle.Users().ChangeUsername(c.Req.Context(), c.Org.Organization.ID, f.Name)
 		if err != nil {
 			c.Data["OrgName"] = true
 			var msg string
@@ -71,7 +71,7 @@ func SettingsPost(c *context.Context, f form.UpdateOrgSetting) {
 	if c.User.IsAdmin {
 		opts.MaxRepoCreation = &f.MaxRepoCreation
 	}
-	err := database.Users.Update(c.Req.Context(), c.Org.Organization.ID, opts)
+	err := database.Handle.Users().Update(c.Req.Context(), c.Org.Organization.ID, opts)
 	if err != nil {
 		c.Error(err, "update organization")
 		return
@@ -93,7 +93,7 @@ func SettingsAvatar(c *context.Context, f form.Avatar) {
 }
 
 func SettingsDeleteAvatar(c *context.Context) {
-	if err := database.Users.DeleteCustomAvatar(c.Req.Context(), c.Org.Organization.ID); err != nil {
+	if err := database.Handle.Users().DeleteCustomAvatar(c.Req.Context(), c.Org.Organization.ID); err != nil {
 		c.Flash.Error(err.Error())
 	}
 
@@ -106,7 +106,7 @@ func SettingsDelete(c *context.Context) {
 
 	org := c.Org.Organization
 	if c.Req.Method == "POST" {
-		if _, err := database.Users.Authenticate(c.Req.Context(), c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+		if _, err := database.Handle.Users().Authenticate(c.Req.Context(), c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if auth.IsErrBadCredentials(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {

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

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

+ 3 - 2
internal/route/repo/commit.go

@@ -114,7 +114,7 @@ func FileHistory(c *context.Context) {
 // tryGetUserByEmail returns a non-nil value if the email is corresponding to an
 // existing user.
 func tryGetUserByEmail(ctx gocontext.Context, email string) *database.User {
-	user, _ := database.Users.GetByEmail(ctx, email)
+	user, _ := database.Handle.Users().GetByEmail(ctx, email)
 	return user
 }
 
@@ -197,10 +197,11 @@ type userCommit struct {
 func matchUsersWithCommitEmails(ctx gocontext.Context, oldCommits []*git.Commit) []*userCommit {
 	emailToUsers := make(map[string]*database.User)
 	newCommits := make([]*userCommit, len(oldCommits))
+	usersStore := database.Handle.Users()
 	for i := range oldCommits {
 		var u *database.User
 		if v, ok := emailToUsers[oldCommits[i].Author.Email]; !ok {
-			u, _ = database.Users.GetByEmail(ctx, oldCommits[i].Author.Email)
+			u, _ = usersStore.GetByEmail(ctx, oldCommits[i].Author.Email)
 			emailToUsers[oldCommits[i].Author.Email] = u
 		} else {
 			u = v

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

@@ -66,7 +66,7 @@ func HTTPContexter(store Store) macaron.Handler {
 			strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") ||
 			c.Req.Method == "GET"
 
-		owner, err := database.Users.GetByUsername(c.Req.Context(), ownerName)
+		owner, err := store.GetUserByUsername(c.Req.Context(), ownerName)
 		if err != nil {
 			if database.IsErrUserNotExist(err) {
 				c.Status(http.StatusNotFound)
@@ -124,7 +124,7 @@ func HTTPContexter(store Store) macaron.Handler {
 			return
 		}
 
-		authUser, err := database.Users.Authenticate(c.Req.Context(), authUsername, authPassword, -1)
+		authUser, err := store.AuthenticateUser(c.Req.Context(), authUsername, authPassword, -1)
 		if err != nil && !auth.IsErrBadCredentials(err) {
 			c.Status(http.StatusInternalServerError)
 			log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)

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

@@ -466,7 +466,7 @@ func ParseCompareInfo(c *context.Context) (*database.User, *database.Repository,
 		headBranch = headInfos[0]
 
 	} else if len(headInfos) == 2 {
-		headUser, err = database.Users.GetByUsername(c.Req.Context(), headInfos[0])
+		headUser, err = database.Handle.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) *database.User {
 		return c.User
 	}
 
-	org, err := database.Users.GetByID(c.Req.Context(), uid)
+	org, err := database.Handle.Users().GetByID(c.Req.Context(), uid)
 	if database.IsErrUserNotExist(err) {
 		return c.User
 	}

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

@@ -225,7 +225,7 @@ func SettingsPost(c *context.Context, f form.RepoSetting) {
 		}
 
 		newOwner := c.Query("new_owner_name")
-		if !database.Users.IsUsernameUsed(c.Req.Context(), newOwner, c.Repo.Owner.ID) {
+		if !database.Handle.Users().IsUsernameUsed(c.Req.Context(), newOwner, c.Repo.Owner.ID) {
 			c.RenderWithErr(c.Tr("form.enterred_invalid_owner_name"), SETTINGS_OPTIONS, nil)
 			return
 		}
@@ -380,7 +380,7 @@ func SettingsCollaborationPost(c *context.Context) {
 		return
 	}
 
-	u, err := database.Users.GetByUsername(c.Req.Context(), name)
+	u, err := database.Handle.Users().GetByUsername(c.Req.Context(), name)
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			c.Flash.Error(c.Tr("form.user_not_exist"))

+ 42 - 0
internal/route/repo/store.go

@@ -23,6 +23,32 @@ type Store interface {
 
 	// IsTwoFactorEnabled returns true if the user has enabled 2FA.
 	IsTwoFactorEnabled(ctx context.Context, userID int64) bool
+
+	// GetUserByID returns the user with given ID. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByID(ctx context.Context, id int64) (*database.User, error)
+	// GetUserByUsername returns the user with given username. It returns
+	// database.ErrUserNotExist when not found.
+	GetUserByUsername(ctx context.Context, username string) (*database.User, error)
+	// CreateUser creates a new user and persists to database. It returns
+	// database.ErrNameNotAllowed if the given name or pattern of the name is not
+	// allowed as a username, or database.ErrUserAlreadyExist when a user with same
+	// name already exists, or database.ErrEmailAlreadyUsed if the email has been
+	// verified by another user.
+	CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error)
+	// AuthenticateUser validates username and password via given login source ID.
+	// It returns database.ErrUserNotExist when the user was not found.
+	//
+	// When the "loginSourceID" is negative, it aborts the process and returns
+	// database.ErrUserNotExist if the user was not found in the database.
+	//
+	// When the "loginSourceID" is non-negative, it returns
+	// database.ErrLoginSourceMismatch if the user has different login source ID
+	// than the "loginSourceID".
+	//
+	// 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.
+	AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error)
 }
 
 type store struct{}
@@ -47,3 +73,19 @@ func (*store) GetRepositoryByName(ctx context.Context, ownerID int64, name strin
 func (*store) IsTwoFactorEnabled(ctx context.Context, userID int64) bool {
 	return database.Handle.TwoFactors().IsEnabled(ctx, userID)
 }
+
+func (*store) GetUserByID(ctx context.Context, id int64) (*database.User, error) {
+	return database.Handle.Users().GetByID(ctx, id)
+}
+
+func (*store) GetUserByUsername(ctx context.Context, username string) (*database.User, error) {
+	return database.Handle.Users().GetByUsername(ctx, username)
+}
+
+func (*store) CreateUser(ctx context.Context, username, email string, opts database.CreateUserOptions) (*database.User, error) {
+	return database.Handle.Users().Create(ctx, username, email, opts)
+}
+
+func (*store) AuthenticateUser(ctx context.Context, login, password string, loginSourceID int64) (*database.User, error) {
+	return database.Handle.Users().Authenticate(ctx, login, password, loginSourceID)
+}

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

@@ -26,7 +26,7 @@ func TriggerTask(c *macaron.Context) {
 	username := c.Params(":username")
 	reponame := c.Params(":reponame")
 
-	owner, err := database.Users.GetByUsername(c.Req.Context(), username)
+	owner, err := database.Handle.Users().GetByUsername(c.Req.Context(), username)
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			c.Error(http.StatusBadRequest, "Owner does not exist")
@@ -55,7 +55,7 @@ func TriggerTask(c *macaron.Context) {
 		return
 	}
 
-	pusher, err := database.Users.GetByID(c.Req.Context(), pusherID)
+	pusher, err := database.Handle.Users().GetByID(c.Req.Context(), pusherID)
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			c.Error(http.StatusBadRequest, "Pusher does 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 := database.Users.GetByEmail(c.Req.Context(), c.Repo.Commit.Author.Email)
+		author, err := database.Handle.Users().GetByEmail(c.Req.Context(), c.Repo.Commit.Author.Email)
 		if err == nil {
 			authorUsername = author.Name
 		} else if !database.IsErrUserNotExist(err) {
@@ -501,7 +501,7 @@ func TestWebhook(c *context.Context) {
 			return
 		}
 
-		user, err := database.Users.GetByEmail(c.Req.Context(), c.Repo.Commit.Committer.Email)
+		user, err := database.Handle.Users().GetByEmail(c.Req.Context(), c.Repo.Commit.Committer.Email)
 		if err == nil {
 			committerUsername = user.Name
 		} else if !database.IsErrUserNotExist(err) {

+ 13 - 13
internal/route/user/auth.go

@@ -56,7 +56,7 @@ func AutoLogin(c *context.Context) (bool, error) {
 		}
 	}()
 
-	u, err := database.Users.GetByUsername(c.Req.Context(), uname)
+	u, err := database.Handle.Users().GetByUsername(c.Req.Context(), uname)
 	if err != nil {
 		if !database.IsErrUserNotExist(err) {
 			return false, fmt.Errorf("get user by name: %v", err)
@@ -165,7 +165,7 @@ func LoginPost(c *context.Context, f form.SignIn) {
 		return
 	}
 
-	u, err := database.Users.Authenticate(c.Req.Context(), f.UserName, f.Password, f.LoginSource)
+	u, err := database.Handle.Users().Authenticate(c.Req.Context(), f.UserName, f.Password, f.LoginSource)
 	if err != nil {
 		switch {
 		case auth.IsErrBadCredentials(err):
@@ -231,7 +231,7 @@ func LoginTwoFactorPost(c *context.Context) {
 		return
 	}
 
-	u, err := database.Users.GetByID(c.Req.Context(), userID)
+	u, err := database.Handle.Users().GetByID(c.Req.Context(), userID)
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return
@@ -277,7 +277,7 @@ func LoginTwoFactorRecoveryCodePost(c *context.Context) {
 		return
 	}
 
-	u, err := database.Users.GetByID(c.Req.Context(), userID)
+	u, err := database.Handle.Users().GetByID(c.Req.Context(), userID)
 	if err != nil {
 		c.Error(err, "get user by ID")
 		return
@@ -335,7 +335,7 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
 		return
 	}
 
-	user, err := database.Users.Create(
+	user, err := database.Handle.Users().Create(
 		c.Req.Context(),
 		f.UserName,
 		f.Email,
@@ -366,9 +366,9 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
 	// should have a dedicate method to check whether the "user" table is empty.
 	//
 	// Auto-set admin for the only user.
-	if database.Users.Count(c.Req.Context()) == 1 {
+	if database.Handle.Users().Count(c.Req.Context()) == 1 {
 		v := true
-		err := database.Users.Update(
+		err := database.Handle.Users().Update(
 			c.Req.Context(),
 			user.ID,
 			database.UpdateUserOptions{
@@ -409,7 +409,7 @@ func parseUserFromCode(code string) (user *database.User) {
 	// 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 = database.Users.GetByUsername(gocontext.TODO(), string(b)); user != nil {
+		if user, err = database.Handle.Users().GetByUsername(gocontext.TODO(), string(b)); user != nil {
 			return user
 		} else if !database.IsErrUserNotExist(err) {
 			log.Error("Failed to get user by name %q: %v", string(b), err)
@@ -445,7 +445,7 @@ func verifyActiveEmailCode(code, email string) *database.EmailAddress {
 		data := com.ToStr(user.ID) + email + user.LowerName + user.Password + user.Rands
 
 		if tool.VerifyTimeLimitCode(data, minutes, prefix) {
-			emailAddress, err := database.Users.GetEmail(gocontext.TODO(), user.ID, email, false)
+			emailAddress, err := database.Handle.Users().GetEmail(gocontext.TODO(), user.ID, email, false)
 			if err == nil {
 				return emailAddress
 			}
@@ -484,7 +484,7 @@ func Activate(c *context.Context) {
 	// Verify code.
 	if user := verifyUserActiveCode(code); user != nil {
 		v := true
-		err := database.Users.Update(
+		err := database.Handle.Users().Update(
 			c.Req.Context(),
 			user.ID,
 			database.UpdateUserOptions{
@@ -515,7 +515,7 @@ func ActivateEmail(c *context.Context) {
 
 	// Verify code.
 	if email := verifyActiveEmailCode(code, emailAddr); email != nil {
-		err := database.Users.MarkEmailActivated(c.Req.Context(), email.UserID, email.Email)
+		err := database.Handle.Users().MarkEmailActivated(c.Req.Context(), email.UserID, email.Email)
 		if err != nil {
 			c.Error(err, "activate email")
 			return
@@ -553,7 +553,7 @@ func ForgotPasswdPost(c *context.Context) {
 	emailAddr := c.Query("email")
 	c.Data["Email"] = emailAddr
 
-	u, err := database.Users.GetByEmail(c.Req.Context(), emailAddr)
+	u, err := database.Handle.Users().GetByEmail(c.Req.Context(), emailAddr)
 	if err != nil {
 		if database.IsErrUserNotExist(err) {
 			c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
@@ -621,7 +621,7 @@ func ResetPasswdPost(c *context.Context) {
 			return
 		}
 
-		err := database.Users.Update(c.Req.Context(), u.ID, database.UpdateUserOptions{Password: &password})
+		err := database.Handle.Users().Update(c.Req.Context(), u.ID, database.UpdateUserOptions{Password: &password})
 		if err != nil {
 			c.Error(err, "update user")
 			return

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

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

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

@@ -92,7 +92,7 @@ func Followers(c *context.Context, puser *context.ParamsUser) {
 		c,
 		puser.NumFollowers,
 		func(page int) ([]*database.User, error) {
-			return database.Users.ListFollowers(c.Req.Context(), puser.ID, page, database.ItemsPerPage)
+			return database.Handle.Users().ListFollowers(c.Req.Context(), puser.ID, page, database.ItemsPerPage)
 		},
 		FOLLOWERS,
 	)
@@ -107,7 +107,7 @@ func Following(c *context.Context, puser *context.ParamsUser) {
 		c,
 		puser.NumFollowing,
 		func(page int) ([]*database.User, error) {
-			return database.Users.ListFollowings(c.Req.Context(), puser.ID, page, database.ItemsPerPage)
+			return database.Handle.Users().ListFollowings(c.Req.Context(), puser.ID, page, database.ItemsPerPage)
 		},
 		FOLLOWERS,
 	)
@@ -120,9 +120,9 @@ func Action(c *context.Context, puser *context.ParamsUser) {
 	var err error
 	switch c.Params(":action") {
 	case "follow":
-		err = database.Users.Follow(c.Req.Context(), c.UserID(), puser.ID)
+		err = database.Handle.Users().Follow(c.Req.Context(), c.UserID(), puser.ID)
 	case "unfollow":
-		err = database.Users.Unfollow(c.Req.Context(), c.UserID(), puser.ID)
+		err = database.Handle.Users().Unfollow(c.Req.Context(), c.UserID(), puser.ID)
 	}
 
 	if err != nil {

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

@@ -84,7 +84,7 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
 	if c.User.IsLocal() {
 		// Check if the username (including cases) had been changed
 		if c.User.Name != f.Name {
-			err := database.Users.ChangeUsername(c.Req.Context(), c.User.ID, f.Name)
+			err := database.Handle.Users().ChangeUsername(c.Req.Context(), c.User.ID, f.Name)
 			if err != nil {
 				c.FormErr("Name")
 				var msg string
@@ -106,7 +106,7 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
 		}
 	}
 
-	err := database.Users.Update(
+	err := database.Handle.Users().Update(
 		c.Req.Context(),
 		c.User.ID,
 		database.UpdateUserOptions{
@@ -128,7 +128,7 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
 func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *database.User) error {
 	if f.Source == form.AvatarLookup && f.Gravatar != "" {
 		avatar := cryptoutil.MD5(f.Gravatar)
-		err := database.Users.Update(
+		err := database.Handle.Users().Update(
 			c.Req.Context(),
 			ctxUser.ID,
 			database.UpdateUserOptions{
@@ -140,7 +140,7 @@ func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *database.Us
 			return errors.Wrap(err, "update user")
 		}
 
-		err = database.Users.DeleteCustomAvatar(c.Req.Context(), c.User.ID)
+		err = database.Handle.Users().DeleteCustomAvatar(c.Req.Context(), c.User.ID)
 		if err != nil {
 			return errors.Wrap(err, "delete custom avatar")
 		}
@@ -162,7 +162,7 @@ func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *database.Us
 			return errors.New(c.Tr("settings.uploaded_avatar_not_a_image"))
 		}
 
-		err = database.Users.UseCustomAvatar(c.Req.Context(), ctxUser.ID, data)
+		err = database.Handle.Users().UseCustomAvatar(c.Req.Context(), ctxUser.ID, data)
 		if err != nil {
 			return errors.Wrap(err, "save avatar")
 		}
@@ -188,7 +188,7 @@ func SettingsAvatarPost(c *context.Context, f form.Avatar) {
 }
 
 func SettingsDeleteAvatar(c *context.Context) {
-	err := database.Users.DeleteCustomAvatar(c.Req.Context(), c.User.ID)
+	err := database.Handle.Users().DeleteCustomAvatar(c.Req.Context(), c.User.ID)
 	if err != nil {
 		c.Flash.Error(fmt.Sprintf("Failed to delete avatar: %v", err))
 	}
@@ -216,7 +216,7 @@ func SettingsPasswordPost(c *context.Context, f form.ChangePassword) {
 	} else if f.Password != f.Retype {
 		c.Flash.Error(c.Tr("form.password_not_match"))
 	} else {
-		err := database.Users.Update(
+		err := database.Handle.Users().Update(
 			c.Req.Context(),
 			c.User.ID,
 			database.UpdateUserOptions{
@@ -237,7 +237,7 @@ func SettingsEmails(c *context.Context) {
 	c.Title("settings.emails")
 	c.PageIs("SettingsEmails")
 
-	emails, err := database.Users.ListEmails(c.Req.Context(), c.User.ID)
+	emails, err := database.Handle.Users().ListEmails(c.Req.Context(), c.User.ID)
 	if err != nil {
 		c.Errorf(err, "get email addresses")
 		return
@@ -252,7 +252,7 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 	c.PageIs("SettingsEmails")
 
 	if c.Query("_method") == "PRIMARY" {
-		err := database.Users.MarkEmailPrimary(c.Req.Context(), c.User.ID, c.Query("email"))
+		err := database.Handle.Users().MarkEmailPrimary(c.Req.Context(), c.User.ID, c.Query("email"))
 		if err != nil {
 			c.Errorf(err, "make email primary")
 			return
@@ -263,7 +263,7 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 	}
 
 	// Add Email address.
-	emails, err := database.Users.ListEmails(c.Req.Context(), c.User.ID)
+	emails, err := database.Handle.Users().ListEmails(c.Req.Context(), c.User.ID)
 	if err != nil {
 		c.Errorf(err, "get email addresses")
 		return
@@ -275,7 +275,7 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 		return
 	}
 
-	err = database.Users.AddEmail(c.Req.Context(), c.User.ID, f.Email, !conf.Auth.RequireEmailConfirmation)
+	err = database.Handle.Users().AddEmail(c.Req.Context(), c.User.ID, f.Email, !conf.Auth.RequireEmailConfirmation)
 	if err != nil {
 		if database.IsErrEmailAlreadyUsed(err) {
 			c.RenderWithErr(c.Tr("form.email_been_used"), SETTINGS_EMAILS, &f)
@@ -310,7 +310,7 @@ func DeleteEmail(c *context.Context) {
 		return
 	}
 
-	err := database.Users.DeleteEmail(c.Req.Context(), c.User.ID, email)
+	err := database.Handle.Users().DeleteEmail(c.Req.Context(), c.User.ID, email)
 	if err != nil {
 		c.Error(err, "delete email address")
 		return
@@ -663,7 +663,7 @@ func SettingsDelete(c *context.Context) {
 	c.PageIs("SettingsDelete")
 
 	if c.Req.Method == "POST" {
-		if _, err := database.Users.Authenticate(c.Req.Context(), c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+		if _, err := database.Handle.Users().Authenticate(c.Req.Context(), c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if auth.IsErrBadCredentials(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {
@@ -672,7 +672,7 @@ func SettingsDelete(c *context.Context) {
 			return
 		}
 
-		if err := database.Users.DeleteByID(c.Req.Context(), c.User.ID, false); err != nil {
+		if err := database.Handle.Users().DeleteByID(c.Req.Context(), c.User.ID, false); err != nil {
 			switch {
 			case database.IsErrUserOwnRepos(err):
 				c.Flash.Error(c.Tr("form.still_own_repo"))

+ 0 - 3
mockgen.yaml

@@ -34,9 +34,6 @@ mocks:
           - Provider
   - filename: internal/route/lfs/mocks_test.go
     sources:
-      - path: gogs.io/gogs/internal/database
-        interfaces:
-          - UsersStore
       - path: gogs.io/gogs/internal/route/lfs
         interfaces:
           - Store

Some files were not shown because too many files changed in this diff