From 10fd9e9e38555b6caecb7d37ba1638f2b4262c9c Mon Sep 17 00:00:00 2001
From: Joe Chen <jc@unknwon.io>
Date: Thu, 24 Aug 2023 01:00:45 -0400
Subject: [PATCH] db: migrate `org.go` to `orgs.go` with GORM

---
 .github/workflows/go.yml                      |  10 +-
 docs/dev/database_schema.md                   |  19 +
 internal/context/org.go                       |   4 +-
 internal/db/access_tokens.go                  |   2 +-
 internal/db/actions.go                        |  23 +-
 internal/db/backup_test.go                    |  19 +-
 internal/db/db.go                             |   1 +
 internal/db/error.go                          |  20 -
 internal/db/lfs.go                            |   3 +-
 internal/db/login_sources.go                  |   2 +-
 internal/db/migrations/migrations.go          |   2 +-
 internal/db/models.go                         |   4 +-
 internal/db/org.go                            | 373 +----------------
 internal/db/org_team.go                       | 143 +++----
 internal/db/orgs.go                           | 390 +++++++++++++++++-
 internal/db/repo.go                           |  49 ++-
 internal/db/repos.go                          |   7 +-
 .../db/testdata/backup/OrgUser.golden.json    |   2 +
 internal/db/two_factors.go                    |   2 +-
 internal/db/users.go                          |  49 ++-
 internal/db/users_test.go                     |  23 +-
 internal/route/admin/orgs.go                  |   6 +-
 internal/route/api/v1/org/org.go              |   2 +-
 internal/route/api/v1/repo/repo.go            |  11 +-
 internal/route/home.go                        |   6 +-
 internal/route/org/members.go                 |  17 +-
 internal/route/user/home.go                   |  34 +-
 internal/route/user/setting.go                |   2 +-
 28 files changed, 663 insertions(+), 562 deletions(-)
 create mode 100644 internal/db/testdata/backup/OrgUser.golden.json

diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index c9feee4e2..dae14e837 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -61,7 +61,7 @@ jobs:
     name: Test
     strategy:
       matrix:
-        go-version: [ 1.20.x, 1.21.x ]
+        go-version: [ 1.21.x ]
         platform: [ ubuntu-latest, macos-latest ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -101,7 +101,7 @@ jobs:
     name: Test Windows
     strategy:
       matrix:
-        go-version: [ 1.20.x, 1.21.x ]
+        go-version: [ 1.21.x ]
         platform: [ windows-latest ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -139,7 +139,7 @@ jobs:
     name: Postgres
     strategy:
       matrix:
-        go-version: [ 1.20.x, 1.21.x ]
+        go-version: [ 1.21.x ]
         platform: [ ubuntu-latest ]
     runs-on: ${{ matrix.platform }}
     services:
@@ -175,7 +175,7 @@ jobs:
     name: MySQL
     strategy:
       matrix:
-        go-version: [ 1.20.x, 1.21.x ]
+        go-version: [ 1.21.x ]
         platform: [ ubuntu-20.04 ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -200,7 +200,7 @@ jobs:
     name: SQLite - Go
     strategy:
       matrix:
-        go-version: [ 1.20.x, 1.21.x ]
+        go-version: [ 1.21.x ]
         platform: [ ubuntu-latest ]
     runs-on: ${{ matrix.platform }}
     steps:
diff --git a/docs/dev/database_schema.md b/docs/dev/database_schema.md
index b49cca3e6..613f1831e 100644
--- a/docs/dev/database_schema.md
+++ b/docs/dev/database_schema.md
@@ -129,3 +129,22 @@ Primary keys: id
 Primary keys: id
 ```
 
+# Table "org_user"
+
+```
+   FIELD   |  COLUMN   |           POSTGRESQL           |             MYSQL              |            SQLITE3              
+-----------+-----------+--------------------------------+--------------------------------+---------------------------------
+  ID       | id        | BIGSERIAL                      | BIGINT AUTO_INCREMENT          | INTEGER                         
+  UserID   | uid       | BIGINT NOT NULL                | BIGINT NOT NULL                | INTEGER NOT NULL                
+  OrgID    | org_id    | BIGINT NOT NULL                | BIGINT NOT NULL                | INTEGER NOT NULL                
+  IsPublic | is_public | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE  
+  IsOwner  | is_owner  | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE  
+  NumTeams | num_teams | BIGINT NOT NULL DEFAULT 0      | BIGINT NOT NULL DEFAULT 0      | INTEGER NOT NULL DEFAULT 0      
+
+Primary keys: id
+Indexes: 
+	"idx_org_user_org_id" (org_id)
+	"idx_org_user_user_id" (uid)
+	"org_user_user_org_unique" UNIQUE (uid, org_id)
+```
+
diff --git a/internal/context/org.go b/internal/context/org.go
index 389677af9..51256358a 100644
--- a/internal/context/org.go
+++ b/internal/context/org.go
@@ -73,8 +73,8 @@ func HandleOrgAssignment(c *Context, args ...bool) {
 			c.Org.IsMember = true
 			c.Org.IsTeamMember = true
 			c.Org.IsTeamAdmin = true
-		} else if org.IsOrgMember(c.User.ID) {
-			c.Org.IsMember = true
+		} else {
+			c.Org.IsMember, _ = db.Orgs.HasMember(c.Req.Context(), org.ID, c.User.ID)
 		}
 	} else {
 		// Fake data.
diff --git a/internal/db/access_tokens.go b/internal/db/access_tokens.go
index 825cfa87a..f9b63e154 100644
--- a/internal/db/access_tokens.go
+++ b/internal/db/access_tokens.go
@@ -154,7 +154,7 @@ func (db *accessTokens) GetBySHA1(ctx context.Context, sha1 string) (*AccessToke
 	token := new(AccessToken)
 	err := db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
 		}
 		return nil, err
diff --git a/internal/db/actions.go b/internal/db/actions.go
index 48d080b3d..a67cb0b8d 100644
--- a/internal/db/actions.go
+++ b/internal/db/actions.go
@@ -86,19 +86,20 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after
 	/*
 		Equivalent SQL for PostgreSQL:
 
-		SELECT * FROM "action"
+		<SELECT * FROM "action">
 		WHERE
 			user_id = @userID
 		AND (@skipAfter OR id < @afterID)
 		AND repo_id IN (
 			SELECT repository.id FROM "repository"
 			JOIN team_repo ON repository.id = team_repo.repo_id
-			WHERE team_repo.team_id IN (
+			WHERE
+				team_repo.team_id IN (
 					SELECT team_id FROM "team_user"
-					WHERE
-						team_user.org_id = @orgID AND uid = @actorID)
-					OR  (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
-			)
+					WHERE team_user.org_id = @orgID AND uid = @actorID)
+				)
+			OR  (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
+		)
 		ORDER BY id DESC
 		LIMIT @limit
 	*/
@@ -120,8 +121,8 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after
 			).
 			Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
 		).
-		Limit(conf.UI.User.NewsFeedPagingNum).
-		Order("id DESC")
+		Order("id DESC").
+		Limit(conf.UI.User.NewsFeedPagingNum)
 }
 
 func (db *actions) ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error) {
@@ -133,7 +134,7 @@ func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int6
 	/*
 		Equivalent SQL for PostgreSQL:
 
-		SELECT * FROM "action"
+		<SELECT * FROM "action">
 		WHERE
 			user_id = @userID
 		AND (@skipAfter OR id < @afterID)
@@ -153,8 +154,8 @@ func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int6
 			Where("?", !isProfile || actorID == userID).
 			Or("is_private = ? AND act_user_id = ?", false, userID),
 		).
-		Limit(conf.UI.User.NewsFeedPagingNum).
-		Order("id DESC")
+		Order("id DESC").
+		Limit(conf.UI.User.NewsFeedPagingNum)
 }
 
 func (db *actions) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) {
diff --git a/internal/db/backup_test.go b/internal/db/backup_test.go
index 9d2d9403a..391af791a 100644
--- a/internal/db/backup_test.go
+++ b/internal/db/backup_test.go
@@ -31,7 +31,7 @@ func TestDumpAndImport(t *testing.T) {
 	}
 	t.Parallel()
 
-	const wantTables = 8
+	const wantTables = 9
 	if len(Tables) != wantTables {
 		t.Fatalf("New table has added (want %d got %d), please add new tests for the table and update this check", wantTables, len(Tables))
 	}
@@ -197,6 +197,23 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
 			Description: "This is a notice",
 			CreatedUnix: 1588568886,
 		},
+
+		&OrgUser{
+			ID:       1,
+			UserID:   1,
+			OrgID:    11,
+			IsPublic: true,
+			IsOwner:  true,
+			NumTeams: 3,
+		},
+		&OrgUser{
+			ID:       2,
+			UserID:   2,
+			OrgID:    11,
+			IsPublic: false,
+			IsOwner:  false,
+			NumTeams: 0,
+		},
 	}
 	for _, val := range vals {
 		err := db.Create(val).Error
diff --git a/internal/db/db.go b/internal/db/db.go
index 9878032d3..e7dc31119 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -46,6 +46,7 @@ var Tables = []any{
 	new(Follow),
 	new(LFSObject), new(LoginSource),
 	new(Notice),
+	new(OrgUser),
 }
 
 // Init initializes the database with given logger.
diff --git a/internal/db/error.go b/internal/db/error.go
index 8436aa99e..1c831cf86 100644
--- a/internal/db/error.go
+++ b/internal/db/error.go
@@ -133,26 +133,6 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string {
 	return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
 }
 
-// ________                            .__                __  .__
-// \_____  \_______  _________    ____ |__|____________ _/  |_|__| ____   ____
-//  /   |   \_  __ \/ ___\__  \  /    \|  \___   /\__  \\   __\  |/  _ \ /    \
-// /    |    \  | \/ /_/  > __ \|   |  \  |/    /  / __ \|  | |  (  <_> )   |  \
-// \_______  /__|  \___  (____  /___|  /__/_____ \(____  /__| |__|\____/|___|  /
-//         \/     /_____/     \/     \/         \/     \/                    \/
-
-type ErrLastOrgOwner struct {
-	UID int64
-}
-
-func IsErrLastOrgOwner(err error) bool {
-	_, ok := err.(ErrLastOrgOwner)
-	return ok
-}
-
-func (err ErrLastOrgOwner) Error() string {
-	return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
-}
-
 // __________                           .__  __
 // \______   \ ____ ______   ____  _____|__|/  |_  ___________ ___.__.
 //  |       _// __ \\____ \ /  _ \/  ___/  \   __\/  _ \_  __ <   |  |
diff --git a/internal/db/lfs.go b/internal/db/lfs.go
index bff18efd1..6f9116614 100644
--- a/internal/db/lfs.go
+++ b/internal/db/lfs.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/pkg/errors"
 	"gorm.io/gorm"
 
 	"gogs.io/gogs/internal/errutil"
@@ -75,7 +76,7 @@ func (db *lfs) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID
 	object := new(LFSObject)
 	err := db.WithContext(ctx).Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": oid}}
 		}
 		return nil, err
diff --git a/internal/db/login_sources.go b/internal/db/login_sources.go
index 9469a3f08..c5a7b974d 100644
--- a/internal/db/login_sources.go
+++ b/internal/db/login_sources.go
@@ -264,7 +264,7 @@ func (db *loginSources) GetByID(ctx context.Context, id int64) (*LoginSource, er
 	source := new(LoginSource)
 	err := db.WithContext(ctx).Where("id = ?", id).First(source).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return db.files.GetByID(id)
 		}
 		return nil, err
diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go
index 04c1e3636..d6cfe6f94 100644
--- a/internal/db/migrations/migrations.go
+++ b/internal/db/migrations/migrations.go
@@ -80,7 +80,7 @@ func Migrate(db *gorm.DB) error {
 
 	var current Version
 	err := db.Where("id = ?", 1).First(&current).Error
-	if err == gorm.ErrRecordNotFound {
+	if errors.Is(err, gorm.ErrRecordNotFound) {
 		err = db.Create(
 			&Version{
 				ID:      1,
diff --git a/internal/db/models.go b/internal/db/models.go
index 1e3b8d9b7..ebbe5136d 100644
--- a/internal/db/models.go
+++ b/internal/db/models.go
@@ -57,7 +57,7 @@ func init() {
 		new(Label), new(IssueLabel), new(Milestone),
 		new(Mirror), new(Release), new(Webhook), new(HookTask),
 		new(ProtectBranch), new(ProtectBranchWhitelist),
-		new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
+		new(Team), new(TeamUser), new(TeamRepo),
 	)
 
 	gonicNames := []string{"SSL"}
@@ -211,7 +211,7 @@ type Statistic struct {
 
 func GetStatistic(ctx context.Context) (stats Statistic) {
 	stats.Counter.User = Users.Count(ctx)
-	stats.Counter.Org = CountOrganizations()
+	stats.Counter.Org = Orgs.Count(ctx)
 	stats.Counter.PublicKey, _ = x.Count(new(PublicKey))
 	stats.Counter.Repo = CountRepositories(true)
 	stats.Counter.Watch, _ = x.Count(new(Watch))
diff --git a/internal/db/org.go b/internal/db/org.go
index 8bc16701e..4b780cdc6 100644
--- a/internal/db/org.go
+++ b/internal/db/org.go
@@ -6,7 +6,6 @@ package db
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"os"
 	"strings"
@@ -19,87 +18,6 @@ import (
 	"gogs.io/gogs/internal/userutil"
 )
 
-var ErrOrgNotExist = errors.New("Organization does not exist")
-
-// IsOwnedBy returns true if given user is in the owner team.
-func (org *User) IsOwnedBy(userID int64) bool {
-	return IsOrganizationOwner(org.ID, userID)
-}
-
-// IsOrgMember returns true if given user is member of organization.
-func (org *User) IsOrgMember(uid int64) bool {
-	return org.IsOrganization() && IsOrganizationMember(org.ID, uid)
-}
-
-func (org *User) getTeam(e Engine, name string) (*Team, error) {
-	return getTeamOfOrgByName(e, org.ID, name)
-}
-
-// GetTeamOfOrgByName returns named team of organization.
-func (org *User) GetTeam(name string) (*Team, error) {
-	return org.getTeam(x, name)
-}
-
-func (org *User) getOwnerTeam(e Engine) (*Team, error) {
-	return org.getTeam(e, OWNER_TEAM)
-}
-
-// GetOwnerTeam returns owner team of organization.
-func (org *User) GetOwnerTeam() (*Team, error) {
-	return org.getOwnerTeam(x)
-}
-
-func (org *User) getTeams(e Engine) (err error) {
-	org.Teams, err = getTeamsByOrgID(e, org.ID)
-	return err
-}
-
-// GetTeams returns all teams that belong to organization.
-func (org *User) GetTeams() error {
-	return org.getTeams(x)
-}
-
-// TeamsHaveAccessToRepo returns all teams that have given access level to the repository.
-func (org *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
-	return GetTeamsHaveAccessToRepo(org.ID, repoID, mode)
-}
-
-// GetMembers returns all members of organization.
-func (org *User) GetMembers(limit int) error {
-	ous, err := GetOrgUsersByOrgID(org.ID, limit)
-	if err != nil {
-		return err
-	}
-
-	org.Members = make([]*User, len(ous))
-	for i, ou := range ous {
-		org.Members[i], err = Users.GetByID(context.TODO(), ou.Uid)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// AddMember adds new member to organization.
-func (org *User) AddMember(uid int64) error {
-	return AddOrgUser(org.ID, uid)
-}
-
-// RemoveMember removes member from organization.
-func (org *User) RemoveMember(uid int64) error {
-	return RemoveOrgUser(org.ID, uid)
-}
-
-func (org *User) removeOrgRepo(e Engine, repoID int64) error {
-	return removeOrgRepo(e, org.ID, repoID)
-}
-
-// RemoveOrgRepo removes all team-repository relations of organization.
-func (org *User) RemoveOrgRepo(repoID int64) error {
-	return org.removeOrgRepo(x, repoID)
-}
-
 // CreateOrganization creates record of a new organization.
 func CreateOrganization(org, owner *User) (err error) {
 	if err = isUsernameAllowed(org.Name); err != nil {
@@ -139,7 +57,7 @@ func CreateOrganization(org, owner *User) (err error) {
 
 	// Add initial creator to organization and owner team.
 	if _, err = sess.Insert(&OrgUser{
-		Uid:      owner.ID,
+		UserID:   owner.ID,
 		OrgID:    org.ID,
 		IsOwner:  true,
 		NumTeams: 1,
@@ -150,8 +68,8 @@ func CreateOrganization(org, owner *User) (err error) {
 	// Create default owner team.
 	t := &Team{
 		OrgID:      org.ID,
-		LowerName:  strings.ToLower(OWNER_TEAM),
-		Name:       OWNER_TEAM,
+		LowerName:  strings.ToLower(TeamNameOwners),
+		Name:       TeamNameOwners,
 		Authorize:  AccessModeOwner,
 		NumMembers: 1,
 	}
@@ -174,30 +92,6 @@ func CreateOrganization(org, owner *User) (err error) {
 	return sess.Commit()
 }
 
-// GetOrgByName returns organization by given name.
-func GetOrgByName(name string) (*User, error) {
-	if name == "" {
-		return nil, ErrOrgNotExist
-	}
-	u := &User{
-		LowerName: strings.ToLower(name),
-		Type:      UserTypeOrganization,
-	}
-	has, err := x.Get(u)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrOrgNotExist
-	}
-	return u, nil
-}
-
-// CountOrganizations returns number of organizations.
-func CountOrganizations() int64 {
-	count, _ := x.Where("type=1").Count(new(User))
-	return count
-}
-
 // Organizations returns number of organizations in given page.
 func Organizations(page, pageSize int) ([]*User, error) {
 	orgs := make([]*User, 0, pageSize)
@@ -237,41 +131,6 @@ func DeleteOrganization(org *User) error {
 	return sess.Commit()
 }
 
-// ________                ____ ___
-// \_____  \_______  ____ |    |   \______ ___________
-//  /   |   \_  __ \/ ___\|    |   /  ___// __ \_  __ \
-// /    |    \  | \/ /_/  >    |  /\___ \\  ___/|  | \/
-// \_______  /__|  \___  /|______//____  >\___  >__|
-//         \/     /_____/              \/     \/
-
-// OrgUser represents relations of organizations and their members.
-type OrgUser struct {
-	ID       int64 `gorm:"primaryKey"`
-	Uid      int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
-	OrgID    int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
-	IsPublic bool  `gorm:"not null;default:FALSE"`
-	IsOwner  bool  `gorm:"not null;default:FALSE"`
-	NumTeams int   `gorm:"not null;default:0"`
-}
-
-// IsOrganizationOwner returns true if given user is in the owner team.
-func IsOrganizationOwner(orgID, userID int64) bool {
-	has, _ := x.Where("is_owner = ?", true).And("uid = ?", userID).And("org_id = ?", orgID).Get(new(OrgUser))
-	return has
-}
-
-// IsOrganizationMember returns true if given user is member of organization.
-func IsOrganizationMember(orgId, uid int64) bool {
-	has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser))
-	return has
-}
-
-// IsPublicMembership returns true if given user public his/her membership.
-func IsPublicMembership(orgId, uid int64) bool {
-	has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser))
-	return has
-}
-
 func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) {
 	orgs := make([]*User, 0, 10)
 	if !showAll {
@@ -287,18 +146,13 @@ func GetOrgsByUserID(userID int64, showAll bool) ([]*User, error) {
 	return getOrgsByUserID(x.NewSession(), userID, showAll)
 }
 
+// getOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
 func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
 	orgs := make([]*User, 0, 10)
 	return orgs, sess.Where("`org_user`.uid=?", userID).And("`org_user`.is_owner=?", true).
 		Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs)
 }
 
-// GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
-func GetOwnedOrgsByUserID(userID int64) ([]*User, error) {
-	sess := x.NewSession()
-	return getOwnedOrgsByUserID(sess, userID)
-}
-
 // GetOwnedOrganizationsByUserIDDesc returns a list of organizations are owned by
 // given user ID, ordered descending by the given condition.
 func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
@@ -306,6 +160,7 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
 	return getOwnedOrgsByUserID(sess.Desc(desc), userID)
 }
 
+// getOrgUsersByOrgID returns all organization-user relations by organization ID.
 func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) {
 	orgUsers := make([]*OrgUser, 0, 10)
 
@@ -316,221 +171,9 @@ func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) {
 	return orgUsers, sess.Find(&orgUsers)
 }
 
-// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
-func GetOrgUsersByOrgID(orgID int64, limit int) ([]*OrgUser, error) {
-	return getOrgUsersByOrgID(x, orgID, limit)
-}
-
-// ChangeOrgUserStatus changes public or private membership status.
-func ChangeOrgUserStatus(orgID, uid int64, public bool) error {
-	ou := new(OrgUser)
-	has, err := x.Where("uid=?", uid).And("org_id=?", orgID).Get(ou)
-	if err != nil {
-		return err
-	} else if !has {
-		return nil
-	}
-
-	ou.IsPublic = public
-	_, err = x.Id(ou.ID).AllCols().Update(ou)
-	return err
-}
-
-// AddOrgUser adds new user to given organization.
-func AddOrgUser(orgID, uid int64) error {
-	if IsOrganizationMember(orgID, uid) {
-		return nil
-	}
-
-	sess := x.NewSession()
-	defer sess.Close()
-	if err := sess.Begin(); err != nil {
-		return err
-	}
-
-	ou := &OrgUser{
-		Uid:   uid,
-		OrgID: orgID,
-	}
-
-	if _, err := sess.Insert(ou); err != nil {
-		return err
-	} else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil {
-		return err
-	}
-
-	return sess.Commit()
-}
-
-// RemoveOrgUser removes user from given organization.
-func RemoveOrgUser(orgID, userID int64) error {
-	ou := new(OrgUser)
-
-	has, err := x.Where("uid=?", userID).And("org_id=?", orgID).Get(ou)
-	if err != nil {
-		return fmt.Errorf("get org-user: %v", err)
-	} else if !has {
-		return nil
-	}
-
-	user, err := Users.GetByID(context.TODO(), userID)
-	if err != nil {
-		return fmt.Errorf("GetUserByID [%d]: %v", userID, err)
-	}
-	org, err := Users.GetByID(context.TODO(), orgID)
-	if err != nil {
-		return fmt.Errorf("GetUserByID [%d]: %v", orgID, err)
-	}
-
-	// FIXME: only need to get IDs here, not all fields of repository.
-	repos, _, err := org.GetUserRepositories(user.ID, 1, org.NumRepos)
-	if err != nil {
-		return fmt.Errorf("GetUserRepositories [%d]: %v", user.ID, err)
-	}
-
-	// Check if the user to delete is the last member in owner team.
-	if IsOrganizationOwner(orgID, userID) {
-		t, err := org.GetOwnerTeam()
-		if err != nil {
-			return err
-		}
-		if t.NumMembers == 1 {
-			return ErrLastOrgOwner{UID: userID}
-		}
-	}
-
-	sess := x.NewSession()
-	defer sess.Close()
-	if err := sess.Begin(); err != nil {
-		return err
-	}
-
-	if _, err := sess.ID(ou.ID).Delete(ou); err != nil {
-		return err
-	} else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
-		return err
-	}
-
-	// Delete all repository accesses and unwatch them.
-	repoIDs := make([]int64, len(repos))
-	for i := range repos {
-		repoIDs = append(repoIDs, repos[i].ID)
-		if err = watchRepo(sess, user.ID, repos[i].ID, false); err != nil {
-			return err
-		}
-	}
-
-	if len(repoIDs) > 0 {
-		if _, err = sess.Where("user_id = ?", user.ID).In("repo_id", repoIDs).Delete(new(Access)); err != nil {
-			return err
-		}
-	}
-
-	// Delete member in his/her teams.
-	teams, err := getUserTeams(sess, org.ID, user.ID)
-	if err != nil {
-		return err
-	}
-	for _, t := range teams {
-		if err = removeTeamMember(sess, org.ID, t.ID, user.ID); err != nil {
-			return err
-		}
-	}
-
-	return sess.Commit()
-}
-
-func removeOrgRepo(e Engine, orgID, repoID int64) error {
-	_, err := e.Delete(&TeamRepo{
-		OrgID:  orgID,
-		RepoID: repoID,
-	})
-	return err
-}
-
-// RemoveOrgRepo removes all team-repository relations of given organization.
-func RemoveOrgRepo(orgID, repoID int64) error {
-	return removeOrgRepo(x, orgID, repoID)
-}
-
-func (org *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) {
-	teams := make([]*Team, 0, org.NumTeams)
-	return teams, e.Where("team_user.org_id = ?", org.ID).
-		And("team_user.uid = ?", userID).
-		Join("INNER", "team_user", "team_user.team_id = team.id").
-		Cols(cols...).Find(&teams)
-}
-
-// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
-func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) {
-	teams, err := org.getUserTeams(x, userID, "team.id")
-	if err != nil {
-		return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err)
-	}
-
-	teamIDs := make([]int64, len(teams))
-	for i := range teams {
-		teamIDs[i] = teams[i].ID
-	}
-	return teamIDs, nil
-}
-
-// GetTeams returns all teams that belong to organization,
-// and that the user has joined.
-func (org *User) GetUserTeams(userID int64) ([]*Team, error) {
-	return org.getUserTeams(x, userID)
-}
-
-// GetUserRepositories returns a range of repositories in organization which the user has access to,
-// and total number of records based on given condition.
-func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repository, int64, error) {
-	teamIDs, err := org.GetUserTeamIDs(userID)
-	if err != nil {
-		return nil, 0, fmt.Errorf("GetUserTeamIDs: %v", err)
-	}
-	if len(teamIDs) == 0 {
-		// user has no team but "IN ()" is invalid SQL
-		teamIDs = []int64{-1} // there is no team with id=-1
-	}
-
-	var teamRepoIDs []int64
-	if err = x.Table("team_repo").In("team_id", teamIDs).Distinct("repo_id").Find(&teamRepoIDs); err != nil {
-		return nil, 0, fmt.Errorf("get team repository IDs: %v", err)
-	}
-	if len(teamRepoIDs) == 0 {
-		// team has no repo but "IN ()" is invalid SQL
-		teamRepoIDs = []int64{-1} // there is no repo with id=-1
-	}
-
-	if page <= 0 {
-		page = 1
-	}
-	repos := make([]*Repository, 0, pageSize)
-	if err = x.Where("owner_id = ?", org.ID).
-		And(builder.Or(
-			builder.And(builder.Expr("is_private = ?", false), builder.Expr("is_unlisted = ?", false)),
-			builder.In("id", teamRepoIDs))).
-		Desc("updated_unix").
-		Limit(pageSize, (page-1)*pageSize).
-		Find(&repos); err != nil {
-		return nil, 0, fmt.Errorf("get user repositories: %v", err)
-	}
-
-	repoCount, err := x.Where("owner_id = ?", org.ID).
-		And(builder.Or(
-			builder.Expr("is_private = ?", false),
-			builder.In("id", teamRepoIDs))).
-		Count(new(Repository))
-	if err != nil {
-		return nil, 0, fmt.Errorf("count user repositories: %v", err)
-	}
-
-	return repos, repoCount, nil
-}
-
 // GetUserMirrorRepositories returns mirror repositories of the organization which the user has access to.
-func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
-	teamIDs, err := org.GetUserTeamIDs(userID)
+func (u *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
+	teamIDs, err := u.GetUserTeamIDs(userID)
 	if err != nil {
 		return nil, fmt.Errorf("GetUserTeamIDs: %v", err)
 	}
@@ -549,7 +192,7 @@ func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error)
 	}
 
 	repos := make([]*Repository, 0, 10)
-	if err = x.Where("owner_id = ?", org.ID).
+	if err = x.Where("owner_id = ?", u.ID).
 		And("is_private = ?", false).
 		Or(builder.In("id", teamRepoIDs)).
 		And("is_mirror = ?", true). // Don't move up because it's an independent condition
diff --git a/internal/db/org_team.go b/internal/db/org_team.go
index d737167e4..c79a38f33 100644
--- a/internal/db/org_team.go
+++ b/internal/db/org_team.go
@@ -9,24 +9,24 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
 	"xorm.io/xorm"
 
-	"gogs.io/gogs/internal/db/errors"
-	"gogs.io/gogs/internal/errutil"
+	dberrors "gogs.io/gogs/internal/db/errors"
 )
 
-const OWNER_TEAM = "Owners"
+const TeamNameOwners = "Owners"
 
 // Team represents a organization team.
 type Team struct {
-	ID          int64
-	OrgID       int64 `xorm:"INDEX"`
+	ID          int64 `gorm:"primaryKey"`
+	OrgID       int64 `xorm:"INDEX" gorm:"index"`
 	LowerName   string
 	Name        string
 	Description string
 	Authorize   AccessMode
-	Repos       []*Repository `xorm:"-" json:"-"`
-	Members     []*User       `xorm:"-" json:"-"`
+	Repos       []*Repository `xorm:"-" json:"-" gorm:"-"`
+	Members     []*User       `xorm:"-" json:"-" gorm:"-"`
 	NumRepos    int
 	NumMembers  int
 }
@@ -43,7 +43,7 @@ func (t *Team) AfterSet(colName string, _ xorm.Cell) {
 
 // IsOwnerTeam returns true if team is owner team.
 func (t *Team) IsOwnerTeam() bool {
-	return t.Name == OWNER_TEAM
+	return t.Name == TeamNameOwners
 }
 
 // HasWriteAccess returns true if team has at least write level access mode.
@@ -136,7 +136,7 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) {
 // AddRepository adds new repository to team of organization.
 func (t *Team) AddRepository(repo *Repository) (err error) {
 	if repo.OwnerID != t.OrgID {
-		return errors.New("Repository does not belong to organization")
+		return dberrors.New("Repository does not belong to organization")
 	} else if t.HasRepository(repo.ID) {
 		return nil
 	}
@@ -259,9 +259,9 @@ func IsUsableTeamName(name string) error {
 // It's caller's responsibility to assign organization ID.
 func NewTeam(t *Team) error {
 	if t.Name == "" {
-		return errors.New("empty team name")
+		return dberrors.New("empty team name")
 	} else if t.OrgID == 0 {
-		return errors.New("OrgID is not assigned")
+		return dberrors.New("OrgID is not assigned")
 	}
 
 	if err := IsUsableTeamName(t.Name); err != nil {
@@ -272,7 +272,7 @@ func NewTeam(t *Team) error {
 	if err != nil {
 		return err
 	} else if !has {
-		return ErrOrgNotExist
+		return errors.New("organization does not exist")
 	}
 
 	t.LowerName = strings.ToLower(t.Name)
@@ -301,44 +301,6 @@ func NewTeam(t *Team) error {
 	return sess.Commit()
 }
 
-var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
-
-type ErrTeamNotExist struct {
-	args map[string]any
-}
-
-func IsErrTeamNotExist(err error) bool {
-	_, ok := err.(ErrTeamNotExist)
-	return ok
-}
-
-func (err ErrTeamNotExist) Error() string {
-	return fmt.Sprintf("team does not exist: %v", err.args)
-}
-
-func (ErrTeamNotExist) NotFound() bool {
-	return true
-}
-
-func getTeamOfOrgByName(e Engine, orgID int64, name string) (*Team, error) {
-	t := &Team{
-		OrgID:     orgID,
-		LowerName: strings.ToLower(name),
-	}
-	has, err := e.Get(t)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
-	}
-	return t, nil
-}
-
-// GetTeamOfOrgByName returns team by given team name and organization.
-func GetTeamOfOrgByName(orgID int64, name string) (*Team, error) {
-	return getTeamOfOrgByName(x, orgID, name)
-}
-
 func getTeamByID(e Engine, teamID int64) (*Team, error) {
 	t := new(Team)
 	has, err := e.ID(teamID).Get(t)
@@ -355,20 +317,16 @@ func GetTeamByID(teamID int64) (*Team, error) {
 	return getTeamByID(x, teamID)
 }
 
+// getTeamsByOrgID returns all teams belong to given organization.
 func getTeamsByOrgID(e Engine, orgID int64) ([]*Team, error) {
 	teams := make([]*Team, 0, 3)
 	return teams, e.Where("org_id = ?", orgID).Find(&teams)
 }
 
-// GetTeamsByOrgID returns all teams belong to given organization.
-func GetTeamsByOrgID(orgID int64) ([]*Team, error) {
-	return getTeamsByOrgID(x, orgID)
-}
-
 // UpdateTeam updates information of team.
 func UpdateTeam(t *Team, authChanged bool) (err error) {
 	if t.Name == "" {
-		return errors.New("empty team name")
+		return dberrors.New("empty team name")
 	}
 
 	if len(t.Description) > 255 {
@@ -528,7 +486,7 @@ func AddTeamMember(orgID, teamID, userID int64) error {
 		return nil
 	}
 
-	if err := AddOrgUser(orgID, userID); err != nil {
+	if err := Orgs.AddMember(context.TODO(), orgID, userID); err != nil {
 		return err
 	}
 
@@ -583,8 +541,8 @@ func AddTeamMember(orgID, teamID, userID int64) error {
 	return sess.Commit()
 }
 
-func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
-	if !isTeamMember(e, orgID, teamID, uid) {
+func removeTeamMember(e Engine, orgID, teamID, userID int64) error {
+	if !isTeamMember(e, orgID, teamID, userID) {
 		return nil
 	}
 
@@ -596,7 +554,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
 
 	// Check if the user to delete is the last member in owner team.
 	if t.IsOwnerTeam() && t.NumMembers == 1 {
-		return ErrLastOrgOwner{UID: uid}
+		return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}}
 	}
 
 	t.NumMembers--
@@ -612,7 +570,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
 	}
 
 	tu := &TeamUser{
-		UID:    uid,
+		UID:    userID,
 		OrgID:  orgID,
 		TeamID: teamID,
 	}
@@ -631,7 +589,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
 
 	// This must exist.
 	ou := new(OrgUser)
-	_, err = e.Where("uid = ?", uid).And("org_id = ?", org.ID).Get(ou)
+	_, err = e.Where("uid = ?", userID).And("org_id = ?", org.ID).Get(ou)
 	if err != nil {
 		return err
 	}
@@ -673,16 +631,13 @@ type TeamRepo struct {
 	RepoID int64 `xorm:"UNIQUE(s)"`
 }
 
+// hasTeamRepo returns true if given team has access to the repository of the organization.
 func hasTeamRepo(e Engine, orgID, teamID, repoID int64) bool {
 	has, _ := e.Where("org_id = ?", orgID).And("team_id = ?", teamID).And("repo_id = ?", repoID).Get(new(TeamRepo))
 	return has
 }
 
-// HasTeamRepo returns true if given team has access to the repository of the organization.
-func HasTeamRepo(orgID, teamID, repoID int64) bool {
-	return hasTeamRepo(x, orgID, teamID, repoID)
-}
-
+// addTeamRepo adds new repository relation to team.
 func addTeamRepo(e Engine, orgID, teamID, repoID int64) error {
 	_, err := e.InsertOne(&TeamRepo{
 		OrgID:  orgID,
@@ -692,11 +647,7 @@ func addTeamRepo(e Engine, orgID, teamID, repoID int64) error {
 	return err
 }
 
-// AddTeamRepo adds new repository relation to team.
-func AddTeamRepo(orgID, teamID, repoID int64) error {
-	return addTeamRepo(x, orgID, teamID, repoID)
-}
-
+// removeTeamRepo deletes repository relation to team.
 func removeTeamRepo(e Engine, teamID, repoID int64) error {
 	_, err := e.Delete(&TeamRepo{
 		TeamID: teamID,
@@ -705,11 +656,6 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error {
 	return err
 }
 
-// RemoveTeamRepo deletes repository relation to team.
-func RemoveTeamRepo(teamID, repoID int64) error {
-	return removeTeamRepo(x, teamID, repoID)
-}
-
 // GetTeamsHaveAccessToRepo returns all teams in an organization that have given access level to the repository.
 func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
 	teams := make([]*Team, 0, 5)
@@ -719,3 +665,46 @@ func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, er
 		And("team_repo.repo_id = ?", repoID).
 		Find(&teams)
 }
+
+func (u *User) getTeams(e Engine) (err error) {
+	u.Teams, err = getTeamsByOrgID(e, u.ID)
+	return err
+}
+
+// GetTeams returns all teams that belong to organization.
+func (u *User) GetTeams() error {
+	return u.getTeams(x)
+}
+
+// TeamsHaveAccessToRepo returns all teams that have given access level to the repository.
+func (u *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
+	return GetTeamsHaveAccessToRepo(u.ID, repoID, mode)
+}
+
+func (u *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) {
+	teams := make([]*Team, 0, u.NumTeams)
+	return teams, e.Where("team_user.org_id = ?", u.ID).
+		And("team_user.uid = ?", userID).
+		Join("INNER", "team_user", "team_user.team_id = team.id").
+		Cols(cols...).Find(&teams)
+}
+
+// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
+func (u *User) GetUserTeamIDs(userID int64) ([]int64, error) {
+	teams, err := u.getUserTeams(x, userID, "team.id")
+	if err != nil {
+		return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err)
+	}
+
+	teamIDs := make([]int64, len(teams))
+	for i := range teams {
+		teamIDs[i] = teams[i].ID
+	}
+	return teamIDs, nil
+}
+
+// GetTeams returns all teams that belong to organization,
+// and that the user has joined.
+func (u *User) GetUserTeams(userID int64) ([]*Team, error) {
+	return u.getUserTeams(x, userID)
+}
diff --git a/internal/db/orgs.go b/internal/db/orgs.go
index 753d81209..0550d07a3 100644
--- a/internal/db/orgs.go
+++ b/internal/db/orgs.go
@@ -6,26 +6,57 @@ package db
 
 import (
 	"context"
+	"fmt"
+	"strings"
 
 	"github.com/pkg/errors"
 	"gorm.io/gorm"
 
 	"gogs.io/gogs/internal/dbutil"
+	"gogs.io/gogs/internal/errutil"
 )
 
 // OrgsStore is the persistent interface for organizations.
 type OrgsStore interface {
-	// List returns a list of organizations filtered by options.
-	List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
+	// AddMember adds a new member to the given organization.
+	AddMember(ctx context.Context, orgID, userID int64) error
+	// RemoveMember removes a member from the given organization.
+	RemoveMember(ctx context.Context, orgID, userID int64) error
+	// HasMember returns whether the given user is a member of the organization
+	// (first), and whether the organization membership is public (second).
+	HasMember(ctx context.Context, orgID, userID int64) (bool, bool)
+	// ListMembers returns all members of the given organization, and sorted by the
+	// given order (e.g. "id ASC").
+	ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error)
+	// IsOwnedBy returns true if the given user is an owner of the organization.
+	IsOwnedBy(ctx context.Context, orgID, userID int64) bool
+	// SetMemberVisibility sets the visibility of the given user in the organization.
+	SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error
+
+	// GetByName returns the organization with given name.
+	GetByName(ctx context.Context, name string) (*Organization, error)
 	// SearchByName returns a list of organizations 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) ([]*Organization, int64, error)
-
+	// List returns a list of organizations filtered by options.
+	List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
 	// CountByUser returns the number of organizations the user is a member of.
 	CountByUser(ctx context.Context, userID int64) (int64, error)
+	// Count returns the total number of organizations.
+	Count(ctx context.Context) int64
+
+	// GetTeamByName returns the team with given name under the given organization.
+	// It returns ErrTeamNotExist whe not found.
+	GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error)
+
+	// AccessibleRepositoriesByUser returns a range of repositories in the
+	// organization that the user has access to and the total number of it. Results
+	// are paginated by given page and page size, and sorted by the given order
+	// (e.g. "updated_unix DESC").
+	AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error)
 }
 
 var Orgs OrgsStore
@@ -42,6 +73,260 @@ func NewOrgsStore(db *gorm.DB) OrgsStore {
 	return &orgs{DB: db}
 }
 
+func (*orgs) recountMembers(tx *gorm.DB, orgID int64) error {
+	/*
+		Equivalent SQL for PostgreSQL:
+
+		UPDATE "user"
+		SET num_members = (
+			SELECT COUNT(*) FROM org_user WHERE org_id = @orgID
+		)
+		WHERE id = @orgID
+	*/
+	err := tx.Model(&User{}).
+		Where("id = ?", orgID).
+		Update(
+			"num_members",
+			tx.Model(&OrgUser{}).Select("COUNT(*)").Where("org_id = ?", orgID),
+		).
+		Error
+	if err != nil {
+		return errors.Wrap(err, `update "user.num_members"`)
+	}
+	return nil
+}
+
+func (db *orgs) AddMember(ctx context.Context, orgID, userID int64) error {
+	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		ou := &OrgUser{
+			UserID: userID,
+			OrgID:  orgID,
+		}
+		result := tx.FirstOrCreate(ou, ou)
+		if result.Error != nil {
+			return errors.Wrap(result.Error, "upsert")
+		} else if result.RowsAffected <= 0 {
+			return nil // Relation already exists
+		}
+		return db.recountMembers(tx, orgID)
+	})
+}
+
+type ErrLastOrgOwner struct {
+	args map[string]any
+}
+
+func IsErrLastOrgOwner(err error) bool {
+	return errors.As(err, &ErrLastOrgOwner{})
+}
+
+func (err ErrLastOrgOwner) Error() string {
+	return fmt.Sprintf("user is the last owner of the organization: %v", err.args)
+}
+
+func (db *orgs) RemoveMember(ctx context.Context, orgID, userID int64) error {
+	ou, err := db.getOrgUser(ctx, orgID, userID)
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil // Not a member
+		}
+		return errors.Wrap(err, "check organization membership")
+	}
+
+	// Check if the member to remove is the last owner.
+	if ou.IsOwner {
+		t, err := db.GetTeamByName(ctx, orgID, TeamNameOwners)
+		if err != nil {
+			return errors.Wrap(err, "get owners team")
+		} else if t.NumMembers == 1 {
+			return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}}
+		}
+	}
+
+	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		repoIDsConds := db.accessibleRepositoriesByUser(tx, orgID, userID, accessibleRepositoriesByUserOptions{}).Select("repository.id")
+
+		err := tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Watch{}).Error
+		if err != nil {
+			return errors.Wrap(err, "unwatch repositories")
+		}
+
+		err = tx.Table("repository").
+			Where("id IN (?)", repoIDsConds).
+			UpdateColumn("num_watches", gorm.Expr("num_watches - 1")).
+			Error
+		if err != nil {
+			return errors.Wrap(err, `decrease "repository.num_watches"`)
+		}
+
+		err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Access{}).Error
+		if err != nil {
+			return errors.Wrap(err, "delete repository accesses")
+		}
+
+		err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Collaboration{}).Error
+		if err != nil {
+			return errors.Wrap(err, "delete repository collaborations")
+		}
+
+		/*
+			Equivalent SQL for PostgreSQL:
+
+			UPDATE "team"
+			SET num_members = num_members - 1
+			WHERE id IN (
+				SELECT team_id FROM "team_user"
+				WHERE team_user.org_id = @orgID AND uid = @userID)
+			)
+		*/
+		err = tx.Table("team").
+			Where(`id IN (?)`, tx.
+				Select("team_id").
+				Table("team_user").
+				Where("org_id = ? AND uid = ?", orgID, userID),
+			).
+			UpdateColumn("num_members", gorm.Expr("num_members - 1")).
+			Error
+		if err != nil {
+			return errors.Wrap(err, `decrease "team.num_members"`)
+		}
+
+		err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&TeamUser{}).Error
+		if err != nil {
+			return errors.Wrap(err, "delete team membership")
+		}
+
+		err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&OrgUser{}).Error
+		if err != nil {
+			return errors.Wrap(err, "delete organization membership")
+		}
+		return db.recountMembers(tx, orgID)
+	})
+}
+
+type accessibleRepositoriesByUserOptions struct {
+	orderBy  string
+	page     int
+	pageSize int
+}
+
+func (*orgs) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID int64, opts accessibleRepositoriesByUserOptions) *gorm.DB {
+	/*
+		Equivalent SQL for PostgreSQL:
+
+		<SELECT * FROM "repository">
+		JOIN team_repo ON repository.id = team_repo.repo_id
+		WHERE
+			owner_id = @orgID
+		AND (
+				team_repo.team_id IN (
+					SELECT team_id FROM "team_user"
+					WHERE team_user.org_id = @orgID AND uid = @userID)
+				)
+			OR  (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
+		)
+		[ORDER BY updated_unix DESC]
+		[LIMIT @limit OFFSET @offset]
+	*/
+	conds := tx.
+		Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
+		Where("owner_id = ? AND (?)", orgID, tx.
+			Where("team_repo.team_id IN (?)", tx.
+				Select("team_id").
+				Table("team_user").
+				Where("team_user.org_id = ? AND uid = ?", orgID, userID),
+			).
+			Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
+		)
+	if opts.orderBy != "" {
+		conds.Order(opts.orderBy)
+	}
+	if opts.page > 0 && opts.pageSize > 0 {
+		conds.Limit(opts.pageSize).Offset((opts.page - 1) * opts.pageSize)
+	}
+	return conds
+}
+
+type AccessibleRepositoriesByUserOptions struct {
+	// Whether to skip counting the total number of repositories.
+	SkipCount bool
+}
+
+func (db *orgs) AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) {
+	conds := db.accessibleRepositoriesByUser(
+		db.DB,
+		orgID,
+		userID,
+		accessibleRepositoriesByUserOptions{
+			orderBy:  "updated_unix DESC",
+			page:     page,
+			pageSize: pageSize,
+		},
+	).WithContext(ctx)
+
+	repos := make([]*Repository, 0, pageSize)
+	err := conds.Find(&repos).Error
+	if err != nil {
+		return nil, 0, errors.Wrap(err, "list repositories")
+	}
+
+	if opts.SkipCount {
+		return repos, 0, nil
+	}
+	var count int64
+	err = conds.Model(&Repository{}).Count(&count).Error
+	if err != nil {
+		return nil, 0, errors.Wrap(err, "count repositories")
+	}
+	return repos, count, nil
+}
+
+func (db *orgs) getOrgUser(ctx context.Context, orgID, userID int64) (*OrgUser, error) {
+	var ou OrgUser
+	return &ou, db.WithContext(ctx).Where("org_id = ? AND uid = ?", orgID, userID).First(&ou).Error
+}
+
+func (db *orgs) IsOwnedBy(ctx context.Context, orgID, userID int64) bool {
+	ou, err := db.getOrgUser(ctx, orgID, userID)
+	return err == nil && ou.IsOwner
+}
+
+func (db *orgs) SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error {
+	return db.Table("org_user").Where("org_id = ? AND uid = ?", orgID, userID).UpdateColumn("is_public", public).Error
+}
+
+func (db *orgs) HasMember(ctx context.Context, orgID, userID int64) (bool, bool) {
+	ou, err := db.getOrgUser(ctx, orgID, userID)
+	return err == nil, ou != nil && ou.IsPublic
+}
+
+type ListOrgMembersOptions struct {
+	// The maximum number of members to return.
+	Limit int
+}
+
+func (db *orgs) ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) {
+	/*
+		Equivalent SQL for PostgreSQL:
+
+		SELECT * FROM "user"
+		JOIN org_user ON org_user.uid = user.id
+		WHERE
+			org_user.org_id = @orgID
+		ORDER BY user.id ASC
+		[LIMIT @limit]
+	*/
+	conds := db.WithContext(ctx).
+		Joins(dbutil.Quote("JOIN org_user ON org_user.uid = %s.id", "user")).
+		Where("org_user.org_id = ?", orgID).
+		Order(dbutil.Quote("%s.id ASC", "user"))
+	if opts.Limit > 0 {
+		conds.Limit(opts.Limit)
+	}
+	var users []*User
+	return users, conds.Find(&users).Error
+}
+
 type ListOrgsOptions struct {
 	// Filter by the membership with the given user ID.
 	MemberID int64
@@ -57,23 +342,54 @@ func (db *orgs) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization
 	/*
 		Equivalent SQL for PostgreSQL:
 
-		SELECT * FROM "org"
-		JOIN org_user ON org_user.org_id = org.id
+		SELECT * FROM "user"
+		JOIN org_user ON org_user.org_id = user.id
 		WHERE
 			org_user.uid = @memberID
 		[AND org_user.is_public = @includePrivateMembers]
-		ORDER BY org.id ASC
+		ORDER BY user.id ASC
 	*/
-	tx := db.WithContext(ctx).
+	conds := db.WithContext(ctx).
 		Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")).
 		Where("org_user.uid = ?", opts.MemberID).
 		Order(dbutil.Quote("%s.id ASC", "user"))
 	if !opts.IncludePrivateMembers {
-		tx = tx.Where("org_user.is_public = ?", true)
+		conds.Where("org_user.is_public = ?", true)
 	}
 
 	var orgs []*Organization
-	return orgs, tx.Find(&orgs).Error
+	return orgs, conds.Find(&orgs).Error
+}
+
+var _ errutil.NotFound = (*ErrUserNotExist)(nil)
+
+type ErrOrganizationNotExist struct {
+	args errutil.Args
+}
+
+// IsErrOrganizationNotExist returns true if the underlying error has the type
+// ErrOrganizationNotExist.
+func IsErrOrganizationNotExist(err error) bool {
+	return errors.As(err, &ErrOrganizationNotExist{})
+}
+
+func (err ErrOrganizationNotExist) Error() string {
+	return fmt.Sprintf("organization does not exist: %v", err.args)
+}
+
+func (ErrOrganizationNotExist) NotFound() bool {
+	return true
+}
+
+func (db *orgs) GetByName(ctx context.Context, name string) (*Organization, error) {
+	org, err := getUserByUsername(ctx, db.DB, UserTypeOrganization, name)
+	if err != nil {
+		if IsErrUserNotExist(err) {
+			return nil, ErrOrganizationNotExist{args: map[string]any{"name": name}}
+		}
+		return nil, errors.Wrap(err, "get organization by name")
+	}
+	return org, nil
 }
 
 func (db *orgs) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) {
@@ -85,8 +401,62 @@ func (db *orgs) CountByUser(ctx context.Context, userID int64) (int64, error) {
 	return count, db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
 }
 
+func (db *orgs) Count(ctx context.Context) int64 {
+	var count int64
+	db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeOrganization).Count(&count)
+	return count
+}
+
+var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
+
+type ErrTeamNotExist struct {
+	args map[string]any
+}
+
+func IsErrTeamNotExist(err error) bool {
+	return errors.As(err, &ErrTeamNotExist{})
+}
+
+func (err ErrTeamNotExist) Error() string {
+	return fmt.Sprintf("team does not exist: %v", err.args)
+}
+
+func (ErrTeamNotExist) NotFound() bool {
+	return true
+}
+
+func (db *orgs) GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) {
+	var team Team
+	err := db.WithContext(ctx).Where("org_id = ? AND lower_name = ?", orgID, strings.ToLower(name)).First(&team).Error
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
+		}
+		return nil, errors.Wrap(err, "get team by name")
+	}
+	return &team, nil
+}
+
 type Organization = User
 
-func (o *Organization) TableName() string {
+func (u *Organization) TableName() string {
 	return "user"
 }
+
+// IsOwnedBy returns true if the given user is an owner of the organization.
+//
+// TODO(unknwon): This is also used in templates, which should be fixed by
+// having a dedicated type `template.Organization`.
+func (u *Organization) IsOwnedBy(userID int64) bool {
+	return Orgs.IsOwnedBy(context.TODO(), u.ID, userID)
+}
+
+// OrgUser represents relations of organizations and their members.
+type OrgUser struct {
+	ID       int64 `gorm:"primaryKey"`
+	UserID   int64 `xorm:"uid INDEX UNIQUE(s)" gorm:"column:uid;uniqueIndex:org_user_user_org_unique;index;not null" json:"Uid"`
+	OrgID    int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
+	IsPublic bool  `gorm:"not null;default:FALSE"`
+	IsOwner  bool  `gorm:"not null;default:FALSE"`
+	NumTeams int   `gorm:"not null;default:0"`
+}
diff --git a/internal/db/repo.go b/internal/db/repo.go
index 8f15ea2b9..08040e5c9 100644
--- a/internal/db/repo.go
+++ b/internal/db/repo.go
@@ -798,7 +798,7 @@ func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository,
 	wikiPath := WikiPath(owner.Name, opts.Name)
 
 	if owner.IsOrganization() {
-		t, err := owner.GetOwnerTeam()
+		t, err := Orgs.GetTeamByName(context.TODO(), owner.ID, TeamNameOwners)
 		if err != nil {
 			return nil, err
 		}
@@ -1149,7 +1149,24 @@ func createRepository(e *xorm.Session, doer, owner *User, repo *Repository) (err
 
 	// Give access to all members in owner team.
 	if owner.IsOrganization() {
-		t, err := owner.getOwnerTeam(e)
+		// FIXME: This is identical to Orgs.GetTeamByName but we are not yet able to
+		// wrap transaction with different ORM objects, should delete this once migrated
+		// to GORM for this part of logic.
+		getTeamOfOrgByName := func(e Engine, orgID int64, name string) (*Team, error) {
+			t := &Team{
+				OrgID:     orgID,
+				LowerName: strings.ToLower(name),
+			}
+			has, err := e.Get(t)
+			if err != nil {
+				return nil, err
+			} else if !has {
+				return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
+			}
+			return t, nil
+		}
+
+		t, err := getTeamOfOrgByName(e, owner.ID, TeamNameOwners)
 		if err != nil {
 			return fmt.Errorf("getOwnerTeam: %v", err)
 		} else if err = t.addRepository(e, repo); err != nil {
@@ -1393,7 +1410,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
 	collaboration := &Collaboration{RepoID: repo.ID}
 	for _, c := range collaborators {
 		collaboration.UserID = c.ID
-		if c.ID == newOwner.ID || newOwner.IsOrgMember(c.ID) {
+		if c.ID == newOwner.ID ||
+			(newOwner.IsOrganization() && func() bool { member, _ := Orgs.HasMember(context.TODO(), newOwner.ID, c.ID); return member }()) {
 			if _, err = sess.Delete(collaboration); err != nil {
 				return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
 			}
@@ -1416,13 +1434,34 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
 			}
 		}
 
-		if err = owner.removeOrgRepo(sess, repo.ID); err != nil {
+		_, err := sess.Delete(&TeamRepo{
+			OrgID:  owner.ID,
+			RepoID: repo.ID,
+		})
+		if err != nil {
 			return fmt.Errorf("removeOrgRepo: %v", err)
 		}
 	}
 
 	if newOwner.IsOrganization() {
-		t, err := newOwner.getOwnerTeam(sess)
+		// FIXME: This is identical to Orgs.GetTeamByName but we are not yet able to
+		// wrap transaction with different ORM objects, should delete this once migrated
+		// to GORM for this part of logic.
+		getTeamOfOrgByName := func(e Engine, orgID int64, name string) (*Team, error) {
+			t := &Team{
+				OrgID:     orgID,
+				LowerName: strings.ToLower(name),
+			}
+			has, err := e.Get(t)
+			if err != nil {
+				return nil, err
+			} else if !has {
+				return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
+			}
+			return t, nil
+		}
+
+		t, err := getTeamOfOrgByName(sess, owner.ID, TeamNameOwners)
 		if err != nil {
 			return fmt.Errorf("getOwnerTeam: %v", err)
 		} else if err = t.addRepository(sess, repo); err != nil {
diff --git a/internal/db/repos.go b/internal/db/repos.go
index 28a38148c..d7f53d612 100644
--- a/internal/db/repos.go
+++ b/internal/db/repos.go
@@ -263,8 +263,7 @@ type ErrRepoNotExist struct {
 }
 
 func IsErrRepoNotExist(err error) bool {
-	_, ok := err.(ErrRepoNotExist)
-	return ok
+	return errors.As(err, &ErrRepoNotExist{})
 }
 
 func (err ErrRepoNotExist) Error() string {
@@ -279,7 +278,7 @@ func (db *repos) GetByID(ctx context.Context, id int64) (*Repository, error) {
 	repo := new(Repository)
 	err := db.WithContext(ctx).Where("id = ?", id).First(repo).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrRepoNotExist{errutil.Args{"repoID": id}}
 		}
 		return nil, err
@@ -294,7 +293,7 @@ func (db *repos) GetByName(ctx context.Context, ownerID int64, name string) (*Re
 		First(repo).
 		Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrRepoNotExist{
 				args: errutil.Args{
 					"ownerID": ownerID,
diff --git a/internal/db/testdata/backup/OrgUser.golden.json b/internal/db/testdata/backup/OrgUser.golden.json
new file mode 100644
index 000000000..d27c3cee4
--- /dev/null
+++ b/internal/db/testdata/backup/OrgUser.golden.json
@@ -0,0 +1,2 @@
+{"ID":1,"Uid":1,"OrgID":11,"IsPublic":true,"IsOwner":true,"NumTeams":3}
+{"ID":2,"Uid":2,"OrgID":11,"IsPublic":false,"IsOwner":false,"NumTeams":0}
diff --git a/internal/db/two_factors.go b/internal/db/two_factors.go
index 741a2ff79..901191576 100644
--- a/internal/db/two_factors.go
+++ b/internal/db/two_factors.go
@@ -104,7 +104,7 @@ func (db *twoFactors) GetByUserID(ctx context.Context, userID int64) (*TwoFactor
 	tf := new(TwoFactor)
 	err := db.WithContext(ctx).Where("user_id = ?", userID).First(tf).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrTwoFactorNotFound{args: errutil.Args{"userID": userID}}
 		}
 		return nil, err
diff --git a/internal/db/users.go b/internal/db/users.go
index c39f9f39c..d82d8727c 100644
--- a/internal/db/users.go
+++ b/internal/db/users.go
@@ -738,7 +738,6 @@ func (db *users) Follow(ctx context.Context, userID, followID int64) error {
 		} else if result.RowsAffected <= 0 {
 			return nil // Relation already exists
 		}
-
 		return db.recountFollows(tx, userID, followID)
 	})
 }
@@ -770,8 +769,7 @@ type ErrUserNotExist struct {
 // IsErrUserNotExist returns true if the underlying error has the type
 // ErrUserNotExist.
 func IsErrUserNotExist(err error) bool {
-	_, ok := errors.Cause(err).(ErrUserNotExist)
-	return ok
+	return errors.As(err, &ErrUserNotExist{})
 }
 
 func (err ErrUserNotExist) Error() string {
@@ -811,7 +809,7 @@ func (db *users) GetByEmail(ctx context.Context, email string) (*User, error) {
 		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
@@ -823,7 +821,7 @@ func (db *users) GetByID(ctx context.Context, id int64) (*User, error) {
 	user := new(User)
 	err := 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 +829,17 @@ func (db *users) GetByID(ctx context.Context, id int64) (*User, error) {
 	return user, nil
 }
 
-func (db *users) GetByUsername(ctx context.Context, username string) (*User, error) {
+func getUserByUsername(ctx context.Context, db *gorm.DB, userType UserType, username string) (*User, error) {
+	if username == "" {
+		return nil, ErrUserNotExist{args: errutil.Args{"name": username}}
+	}
 	user := new(User)
-	err := db.WithContext(ctx).Where("lower_name = ?", strings.ToLower(username)).First(user).Error
+	err := db.WithContext(ctx).
+		Where("type = ? AND lower_name = ?", userType, 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,6 +847,10 @@ func (db *users) GetByUsername(ctx context.Context, username string) (*User, err
 	return user, nil
 }
 
+func (db *users) GetByUsername(ctx context.Context, username string) (*User, error) {
+	return getUserByUsername(ctx, db.DB, UserTypeIndividual, username)
+}
+
 func (db *users) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
 	user := new(User)
 	err := db.WithContext(ctx).
@@ -851,7 +859,7 @@ func (db *users) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
 		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
@@ -1126,15 +1134,15 @@ func (ErrEmailNotExist) NotFound() bool {
 }
 
 func (db *users) GetEmail(ctx context.Context, userID int64, email string, needsActivated bool) (*EmailAddress, error) {
-	tx := db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
+	conds := db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
 	if needsActivated {
-		tx = tx.Where("is_activated = ?", true)
+		conds.Where("is_activated = ?", true)
 	}
 
 	emailAddress := new(EmailAddress)
-	err := tx.First(emailAddress).Error
+	err := conds.First(emailAddress).Error
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrEmailNotExist{
 				args: errutil.Args{
 					"email": email,
@@ -1213,7 +1221,7 @@ func (db *users) MarkEmailPrimary(ctx context.Context, userID int64, email strin
 	var emailAddress EmailAddress
 	err := 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")
@@ -1317,7 +1325,6 @@ type User struct {
 	NumTeams    int
 	NumMembers  int
 	Teams       []*Team `xorm:"-" gorm:"-" json:"-"`
-	Members     []*User `xorm:"-" gorm:"-" json:"-"`
 }
 
 // BeforeCreate implements the GORM create hook.
@@ -1467,13 +1474,12 @@ func (u *User) IsFollowing(followID int64) bool {
 	return Users.IsFollowing(context.TODO(), u.ID, followID)
 }
 
-// IsUserOrgOwner returns true if the user is in the owner team of the given
-// organization.
+// IsUserOrgOwner returns true if the user is an owner of the organization.
 //
 // TODO(unknwon): This is also used in templates, which should be fixed by
 // having a dedicated type `template.User`.
-func (u *User) IsUserOrgOwner(orgId int64) bool {
-	return IsOrganizationOwner(orgId, u.ID)
+func (u *User) IsUserOrgOwner(orgID int64) bool {
+	return Orgs.IsOwnedBy(context.TODO(), orgID, u.ID)
 }
 
 // IsPublicMember returns true if the user has public membership of the given
@@ -1481,8 +1487,9 @@ func (u *User) IsUserOrgOwner(orgId int64) bool {
 //
 // TODO(unknwon): This is also used in templates, which should be fixed by
 // having a dedicated type `template.User`.
-func (u *User) IsPublicMember(orgId int64) bool {
-	return IsPublicMembership(orgId, u.ID)
+func (u *User) IsPublicMember(orgID int64) bool {
+	_, public := Orgs.HasMember(context.TODO(), orgID, u.ID)
+	return public
 }
 
 // GetOrganizationCount returns the count of organization membership that the
diff --git a/internal/db/users_test.go b/internal/db/users_test.go
index bb0273a58..d31254b74 100644
--- a/internal/db/users_test.go
+++ b/internal/db/users_test.go
@@ -801,16 +801,23 @@ func usersGetByID(t *testing.T, ctx context.Context, db *users) {
 }
 
 func usersGetByUsername(t *testing.T, ctx context.Context, db *users) {
-	alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
-	require.NoError(t, err)
+	t.Run("correct user type", func(t *testing.T) {
+		alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
+		require.NoError(t, err)
 
-	user, err := db.GetByUsername(ctx, alice.Name)
-	require.NoError(t, err)
-	assert.Equal(t, alice.Name, user.Name)
+		user, err := db.GetByUsername(ctx, alice.Name)
+		require.NoError(t, err)
+		assert.Equal(t, alice.Name, user.Name)
+
+		_, err = db.GetByUsername(ctx, "bad_username")
+		wantErr := ErrUserNotExist{args: errutil.Args{"name": "bad_username"}}
+		assert.Equal(t, wantErr, err)
+	})
+
+	t.Run("wrong user type", func(t *testing.T) {
+		// org1,err:=NewOrgsStore(db.DB).Create(ctx,"org1","// TODO: Use Orgs.Create
+	})
 
-	_, err = db.GetByUsername(ctx, "bad_username")
-	wantErr := ErrUserNotExist{args: errutil.Args{"name": "bad_username"}}
-	assert.Equal(t, wantErr, err)
 }
 
 func usersGetByKeyID(t *testing.T, ctx context.Context, db *users) {
diff --git a/internal/route/admin/orgs.go b/internal/route/admin/orgs.go
index 922d7f863..46a26b422 100644
--- a/internal/route/admin/orgs.go
+++ b/internal/route/admin/orgs.go
@@ -23,10 +23,8 @@ func Organizations(c *context.Context) {
 	c.Data["PageIsAdminOrganizations"] = true
 
 	route.RenderUserSearch(c, &route.UserSearchOptions{
-		Type: db.UserTypeOrganization,
-		Counter: func(gocontext.Context) int64 {
-			return db.CountOrganizations()
-		},
+		Type:    db.UserTypeOrganization,
+		Counter: db.Orgs.Count,
 		Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
 			return db.Organizations(page, pageSize)
 		},
diff --git a/internal/route/api/v1/org/org.go b/internal/route/api/v1/org/org.go
index 9b99bead7..e6f2a192c 100644
--- a/internal/route/api/v1/org/org.go
+++ b/internal/route/api/v1/org/org.go
@@ -104,7 +104,7 @@ func Edit(c *context.APIContext, form api.EditOrgOption) {
 		return
 	}
 
-	org, err = db.GetOrgByName(org.Name)
+	org, err = db.Orgs.GetByName(c.Req.Context(), org.Name)
 	if err != nil {
 		c.Error(err, "get organization")
 		return
diff --git a/internal/route/api/v1/repo/repo.go b/internal/route/api/v1/repo/repo.go
index a4cc36b30..d16ff84bb 100644
--- a/internal/route/api/v1/repo/repo.go
+++ b/internal/route/api/v1/repo/repo.go
@@ -87,7 +87,14 @@ func listUserRepositories(c *context.APIContext, username string) {
 	// or an organization isn't a member of.
 	var ownRepos []*db.Repository
 	if user.IsOrganization() {
-		ownRepos, _, err = user.GetUserRepositories(c.User.ID, 1, user.NumRepos)
+		ownRepos, _, err = db.Orgs.AccessibleRepositoriesByUser(
+			c.Req.Context(),
+			user.ID,
+			c.User.ID,
+			1,
+			user.NumRepos,
+			db.AccessibleRepositoriesByUserOptions{},
+		)
 	} else {
 		ownRepos, err = db.GetUserRepositories(&db.UserRepoOptions{
 			UserID:   user.ID,
@@ -191,7 +198,7 @@ func Create(c *context.APIContext, opt api.CreateRepoOption) {
 }
 
 func CreateOrgRepo(c *context.APIContext, opt api.CreateRepoOption) {
-	org, err := db.GetOrgByName(c.Params(":org"))
+	org, err := db.Orgs.GetByName(c.Req.Context(), c.Params(":org"))
 	if err != nil {
 		c.NotFoundOrError(err, "get organization by name")
 		return
diff --git a/internal/route/home.go b/internal/route/home.go
index a389c0801..183222891 100644
--- a/internal/route/home.go
+++ b/internal/route/home.go
@@ -152,10 +152,8 @@ func ExploreOrganizations(c *context.Context) {
 	c.Data["PageIsExploreOrganizations"] = true
 
 	RenderUserSearch(c, &UserSearchOptions{
-		Type: db.UserTypeOrganization,
-		Counter: func(gocontext.Context) int64 {
-			return db.CountOrganizations()
-		},
+		Type:    db.UserTypeOrganization,
+		Counter: db.Orgs.Count,
 		Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
 			return db.Organizations(page, pageSize)
 		},
diff --git a/internal/route/org/members.go b/internal/route/org/members.go
index b53034e43..a8f8417e4 100644
--- a/internal/route/org/members.go
+++ b/internal/route/org/members.go
@@ -23,11 +23,12 @@ func Members(c *context.Context) {
 	c.Data["Title"] = org.FullName
 	c.Data["PageIsOrgMembers"] = true
 
-	if err := org.GetMembers(0); err != nil {
-		c.Error(err, "get members")
+	members, err := db.Orgs.ListMembers(c.Req.Context(), org.ID, db.ListOrgMembersOptions{})
+	if err != nil {
+		c.Error(err, "list members")
 		return
 	}
-	c.Data["Members"] = org.Members
+	c.Data["Members"] = members
 
 	c.Success(MEMBERS)
 }
@@ -47,26 +48,26 @@ func MembersAction(c *context.Context) {
 			c.NotFound()
 			return
 		}
-		err = db.ChangeOrgUserStatus(org.ID, uid, false)
+		err = db.Orgs.SetMemberVisibility(c.Req.Context(), org.ID, uid, false)
 	case "public":
 		if c.User.ID != uid && !c.Org.IsOwner {
 			c.NotFound()
 			return
 		}
-		err = db.ChangeOrgUserStatus(org.ID, uid, true)
+		err = db.Orgs.SetMemberVisibility(c.Req.Context(), org.ID, uid, true)
 	case "remove":
 		if !c.Org.IsOwner {
 			c.NotFound()
 			return
 		}
-		err = org.RemoveMember(uid)
+		err = db.Orgs.RemoveMember(c.Req.Context(), org.ID, uid)
 		if db.IsErrLastOrgOwner(err) {
 			c.Flash.Error(c.Tr("form.last_org_owner"))
 			c.Redirect(c.Org.OrgLink + "/members")
 			return
 		}
 	case "leave":
-		err = org.RemoveMember(c.User.ID)
+		err = db.Orgs.RemoveMember(c.Req.Context(), org.ID, c.User.ID)
 		if db.IsErrLastOrgOwner(err) {
 			c.Flash.Error(c.Tr("form.last_org_owner"))
 			c.Redirect(c.Org.OrgLink + "/members")
@@ -108,7 +109,7 @@ func Invitation(c *context.Context) {
 			return
 		}
 
-		if err = org.AddMember(u.ID); err != nil {
+		if err = db.Orgs.AddMember(c.Req.Context(), org.ID, u.ID); err != nil {
 			c.Error(err, "add member")
 			return
 		}
diff --git a/internal/route/user/home.go b/internal/route/user/home.go
index 4521f511b..6aa3f9a55 100644
--- a/internal/route/user/home.go
+++ b/internal/route/user/home.go
@@ -140,7 +140,14 @@ func Dashboard(c *context.Context) {
 	var repos, mirrors []*db.Repository
 	var repoCount int64
 	if ctxUser.IsOrganization() {
-		repos, repoCount, err = ctxUser.GetUserRepositories(c.User.ID, 1, conf.UI.User.RepoPagingNum)
+		repos, repoCount, err = db.Orgs.AccessibleRepositoriesByUser(
+			c.Req.Context(),
+			ctxUser.ID,
+			c.User.ID,
+			1,
+			conf.UI.User.RepoPagingNum,
+			db.AccessibleRepositoriesByUserOptions{},
+		)
 		if err != nil {
 			c.Error(err, "get user repositories")
 			return
@@ -236,7 +243,14 @@ func Issues(c *context.Context) {
 		showRepos   = make([]*db.Repository, 0, 10)
 	)
 	if ctxUser.IsOrganization() {
-		repos, _, err = ctxUser.GetUserRepositories(c.User.ID, 1, ctxUser.NumRepos)
+		repos, _, err = db.Orgs.AccessibleRepositoriesByUser(
+			c.Req.Context(),
+			ctxUser.ID,
+			c.User.ID,
+			1,
+			ctxUser.NumRepos,
+			db.AccessibleRepositoriesByUserOptions{SkipCount: true},
+		)
 		if err != nil {
 			c.Error(err, "get repositories")
 			return
@@ -409,7 +423,14 @@ func showOrgProfile(c *context.Context) {
 		err   error
 	)
 	if c.IsLogged && !c.User.IsAdmin {
-		repos, count, err = org.GetUserRepositories(c.User.ID, page, conf.UI.User.RepoPagingNum)
+		repos, count, err = db.Orgs.AccessibleRepositoriesByUser(
+			c.Req.Context(),
+			org.ID,
+			c.User.ID,
+			page,
+			conf.UI.User.RepoPagingNum,
+			db.AccessibleRepositoriesByUserOptions{},
+		)
 		if err != nil {
 			c.Error(err, "get user repositories")
 			return
@@ -432,11 +453,12 @@ func showOrgProfile(c *context.Context) {
 	}
 	c.Data["Page"] = paginater.New(int(count), conf.UI.User.RepoPagingNum, page, 5)
 
-	if err := org.GetMembers(12); err != nil {
-		c.Error(err, "get members")
+	members, err := db.Orgs.ListMembers(c.Req.Context(), org.ID, db.ListOrgMembersOptions{Limit: 12})
+	if err != nil {
+		c.Error(err, "list members")
 		return
 	}
-	c.Data["Members"] = org.Members
+	c.Data["Members"] = members
 
 	c.Data["Teams"] = org.Teams
 
diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go
index 6ee5fa4d3..b1560c8e6 100644
--- a/internal/route/user/setting.go
+++ b/internal/route/user/setting.go
@@ -566,7 +566,7 @@ func SettingsOrganizations(c *context.Context) {
 }
 
 func SettingsLeaveOrganization(c *context.Context) {
-	if err := db.RemoveOrgUser(c.QueryInt64("id"), c.User.ID); err != nil {
+	if err := db.Orgs.RemoveMember(c.Req.Context(), c.QueryInt64("id"), c.User.ID); err != nil {
 		if db.IsErrLastOrgOwner(err) {
 			c.Flash.Error(c.Tr("form.last_org_owner"))
 		} else {