diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml
index 3d42597c5..f38b5344b 100644
--- a/models/fixtures/project.yml
+++ b/models/fixtures/project.yml
@@ -24,3 +24,12 @@
   creator_id: 5
   board_type: 1
   type: 2
+
+-
+  id: 4
+  title: project on user2
+  owner_id: 2
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml
index 9e06e8c23..dc4f9cf56 100644
--- a/models/fixtures/project_board.yml
+++ b/models/fixtures/project_board.yml
@@ -21,3 +21,11 @@
   creator_id: 2
   created_unix: 1588117528
   updated_unix: 1588117528
+
+-
+  id: 4
+  project_id: 4
+  title: Done
+  creator_id: 2
+  created_unix: 1588117528
+  updated_unix: 1588117528
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 4a8ab0682..dc9e5c5ac 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -1098,7 +1098,7 @@ func GetIssueWithAttrsByID(id int64) (*Issue, error) {
 }
 
 // GetIssuesByIDs return issues with the given IDs.
-func GetIssuesByIDs(ctx context.Context, issueIDs []int64) ([]*Issue, error) {
+func GetIssuesByIDs(ctx context.Context, issueIDs []int64) (IssueList, error) {
 	issues := make([]*Issue, 0, 10)
 	return issues, db.GetEngine(ctx).In("id", issueIDs).Find(&issues)
 }
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index 8e559f13c..c9f4c9f53 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -125,13 +125,17 @@ func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64
 func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
 	oldProjectID := issue.projectID(ctx)
 
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
 	// Only check if we add a new project and not remove it.
 	if newProjectID > 0 {
 		newProject, err := project_model.GetProjectByID(ctx, newProjectID)
 		if err != nil {
 			return err
 		}
-		if newProject.RepoID != issue.RepoID {
+		if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
 			return fmt.Errorf("issue's repository is not the same as project's repository")
 		}
 	}
@@ -140,10 +144,6 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
 		return err
 	}
 
-	if err := issue.LoadRepo(ctx); err != nil {
-		return err
-	}
-
 	if oldProjectID > 0 || newProjectID > 0 {
 		if _, err := CreateComment(ctx, &CreateCommentOptions{
 			Type:         CommentTypeProject,
diff --git a/models/organization/team.go b/models/organization/team.go
index 55d3f1727..0c2577dab 100644
--- a/models/organization/team.go
+++ b/models/organization/team.go
@@ -16,8 +16,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
-
-	"xorm.io/builder"
 )
 
 // ___________
@@ -96,59 +94,6 @@ func init() {
 	db.RegisterModel(new(TeamInvite))
 }
 
-// SearchTeamOptions holds the search options
-type SearchTeamOptions struct {
-	db.ListOptions
-	UserID      int64
-	Keyword     string
-	OrgID       int64
-	IncludeDesc bool
-}
-
-func (opts *SearchTeamOptions) toCond() builder.Cond {
-	cond := builder.NewCond()
-
-	if len(opts.Keyword) > 0 {
-		lowerKeyword := strings.ToLower(opts.Keyword)
-		var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
-		if opts.IncludeDesc {
-			keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
-		}
-		cond = cond.And(keywordCond)
-	}
-
-	if opts.OrgID > 0 {
-		cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
-	}
-
-	if opts.UserID > 0 {
-		cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
-	}
-
-	return cond
-}
-
-// SearchTeam search for teams. Caller is responsible to check permissions.
-func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
-	sess := db.GetEngine(db.DefaultContext)
-
-	opts.SetDefaultValues()
-	cond := opts.toCond()
-
-	if opts.UserID > 0 {
-		sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
-	}
-	sess = db.SetSessionPagination(sess, opts)
-
-	teams := make([]*Team, 0, opts.PageSize)
-	count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
-	if err != nil {
-		return nil, 0, err
-	}
-
-	return teams, count, nil
-}
-
 // ColorFormat provides a basic color format for a Team
 func (t *Team) ColorFormat(s fmt.State) {
 	if t == nil {
@@ -335,16 +280,6 @@ func GetTeamNamesByID(teamIDs []int64) ([]string, error) {
 	return teamNames, err
 }
 
-// GetRepoTeams gets the list of teams that has access to the repository
-func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams []*Team, err error) {
-	return teams, db.GetEngine(ctx).
-		Join("INNER", "team_repo", "team_repo.team_id = team.id").
-		Where("team.org_id = ?", repo.OwnerID).
-		And("team_repo.repo_id=?", repo.ID).
-		OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
-		Find(&teams)
-}
-
 // IncrTeamRepoNum increases the number of repos for the given team by 1
 func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
 	_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
diff --git a/models/organization/team_list.go b/models/organization/team_list.go
new file mode 100644
index 000000000..5d3bd555c
--- /dev/null
+++ b/models/organization/team_list.go
@@ -0,0 +1,128 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package organization
+
+import (
+	"context"
+	"strings"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/perm"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+
+	"xorm.io/builder"
+)
+
+type TeamList []*Team
+
+func (t TeamList) LoadUnits(ctx context.Context) error {
+	for _, team := range t {
+		if err := team.getUnits(ctx); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (t TeamList) UnitMaxAccess(tp unit.Type) perm.AccessMode {
+	maxAccess := perm.AccessModeNone
+	for _, team := range t {
+		if team.IsOwnerTeam() {
+			return perm.AccessModeOwner
+		}
+		for _, teamUnit := range team.Units {
+			if teamUnit.Type != tp {
+				continue
+			}
+			if teamUnit.AccessMode > maxAccess {
+				maxAccess = teamUnit.AccessMode
+			}
+		}
+	}
+	return maxAccess
+}
+
+// SearchTeamOptions holds the search options
+type SearchTeamOptions struct {
+	db.ListOptions
+	UserID      int64
+	Keyword     string
+	OrgID       int64
+	IncludeDesc bool
+}
+
+func (opts *SearchTeamOptions) toCond() builder.Cond {
+	cond := builder.NewCond()
+
+	if len(opts.Keyword) > 0 {
+		lowerKeyword := strings.ToLower(opts.Keyword)
+		var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
+		if opts.IncludeDesc {
+			keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
+		}
+		cond = cond.And(keywordCond)
+	}
+
+	if opts.OrgID > 0 {
+		cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
+	}
+
+	if opts.UserID > 0 {
+		cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
+	}
+
+	return cond
+}
+
+// SearchTeam search for teams. Caller is responsible to check permissions.
+func SearchTeam(opts *SearchTeamOptions) (TeamList, int64, error) {
+	sess := db.GetEngine(db.DefaultContext)
+
+	opts.SetDefaultValues()
+	cond := opts.toCond()
+
+	if opts.UserID > 0 {
+		sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
+	}
+	sess = db.SetSessionPagination(sess, opts)
+
+	teams := make([]*Team, 0, opts.PageSize)
+	count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	return teams, count, nil
+}
+
+// GetRepoTeams gets the list of teams that has access to the repository
+func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams TeamList, err error) {
+	return teams, db.GetEngine(ctx).
+		Join("INNER", "team_repo", "team_repo.team_id = team.id").
+		Where("team.org_id = ?", repo.OwnerID).
+		And("team_repo.repo_id=?", repo.ID).
+		OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
+		Find(&teams)
+}
+
+// GetUserOrgTeams returns all teams that user belongs to in given organization.
+func GetUserOrgTeams(ctx context.Context, orgID, userID int64) (teams TeamList, err error) {
+	return teams, db.GetEngine(ctx).
+		Join("INNER", "team_user", "team_user.team_id = team.id").
+		Where("team.org_id = ?", orgID).
+		And("team_user.uid=?", userID).
+		Find(&teams)
+}
+
+// GetUserRepoTeams returns user repo's teams
+func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams TeamList, err error) {
+	return teams, db.GetEngine(ctx).
+		Join("INNER", "team_user", "team_user.team_id = team.id").
+		Join("INNER", "team_repo", "team_repo.team_id = team.id").
+		Where("team.org_id = ?", orgID).
+		And("team_user.uid=?", userID).
+		And("team_repo.repo_id=?", repoID).
+		Find(&teams)
+}
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index 7a024f1c6..816daf3d3 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -72,26 +72,6 @@ func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_mo
 	return members, nil
 }
 
-// GetUserOrgTeams returns all teams that user belongs to in given organization.
-func GetUserOrgTeams(ctx context.Context, orgID, userID int64) (teams []*Team, err error) {
-	return teams, db.GetEngine(ctx).
-		Join("INNER", "team_user", "team_user.team_id = team.id").
-		Where("team.org_id = ?", orgID).
-		And("team_user.uid=?", userID).
-		Find(&teams)
-}
-
-// GetUserRepoTeams returns user repo's teams
-func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams []*Team, err error) {
-	return teams, db.GetEngine(ctx).
-		Join("INNER", "team_user", "team_user.team_id = team.id").
-		Join("INNER", "team_repo", "team_repo.team_id = team.id").
-		Where("team.org_id = ?", orgID).
-		And("team_user.uid=?", userID).
-		And("team_repo.repo_id=?", repoID).
-		Find(&teams)
-}
-
 // IsUserInTeams returns if a user in some teams
 func IsUserInTeams(ctx context.Context, userID int64, teamIDs []int64) (bool, error) {
 	return db.GetEngine(ctx).Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
diff --git a/models/project/project.go b/models/project/project.go
index f432d0bc4..8bac9115b 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -8,6 +8,9 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -78,12 +81,15 @@ func (err ErrProjectBoardNotExist) Unwrap() error {
 
 // Project represents a project board
 type Project struct {
-	ID          int64  `xorm:"pk autoincr"`
-	Title       string `xorm:"INDEX NOT NULL"`
-	Description string `xorm:"TEXT"`
-	RepoID      int64  `xorm:"INDEX"`
-	CreatorID   int64  `xorm:"NOT NULL"`
-	IsClosed    bool   `xorm:"INDEX"`
+	ID          int64                  `xorm:"pk autoincr"`
+	Title       string                 `xorm:"INDEX NOT NULL"`
+	Description string                 `xorm:"TEXT"`
+	OwnerID     int64                  `xorm:"INDEX"`
+	Owner       *user_model.User       `xorm:"-"`
+	RepoID      int64                  `xorm:"INDEX"`
+	Repo        *repo_model.Repository `xorm:"-"`
+	CreatorID   int64                  `xorm:"NOT NULL"`
+	IsClosed    bool                   `xorm:"INDEX"`
 	BoardType   BoardType
 	Type        Type
 
@@ -94,6 +100,46 @@ type Project struct {
 	ClosedDateUnix timeutil.TimeStamp
 }
 
+func (p *Project) LoadOwner(ctx context.Context) (err error) {
+	if p.Owner != nil {
+		return nil
+	}
+	p.Owner, err = user_model.GetUserByID(ctx, p.OwnerID)
+	return err
+}
+
+func (p *Project) LoadRepo(ctx context.Context) (err error) {
+	if p.RepoID == 0 || p.Repo != nil {
+		return nil
+	}
+	p.Repo, err = repo_model.GetRepositoryByID(ctx, p.RepoID)
+	return err
+}
+
+func (p *Project) Link() string {
+	if p.OwnerID > 0 {
+		err := p.LoadOwner(db.DefaultContext)
+		if err != nil {
+			log.Error("LoadOwner: %v", err)
+			return ""
+		}
+		return fmt.Sprintf("/%s/-/projects/%d", p.Owner.Name, p.ID)
+	}
+	if p.RepoID > 0 {
+		err := p.LoadRepo(db.DefaultContext)
+		if err != nil {
+			log.Error("LoadRepo: %v", err)
+			return ""
+		}
+		return fmt.Sprintf("/%s/projects/%d", p.Repo.RepoPath(), p.ID)
+	}
+	return ""
+}
+
+func (p *Project) IsOrganizationProject() bool {
+	return p.Type == TypeOrganization
+}
+
 func init() {
 	db.RegisterModel(new(Project))
 }
@@ -110,7 +156,7 @@ func GetProjectsConfig() []ProjectsConfig {
 // IsTypeValid checks if a project type is valid
 func IsTypeValid(p Type) bool {
 	switch p {
-	case TypeRepository:
+	case TypeRepository, TypeOrganization:
 		return true
 	default:
 		return false
@@ -119,6 +165,7 @@ func IsTypeValid(p Type) bool {
 
 // SearchOptions are options for GetProjects
 type SearchOptions struct {
+	OwnerID  int64
 	RepoID   int64
 	Page     int
 	IsClosed util.OptionalBool
@@ -126,12 +173,11 @@ type SearchOptions struct {
 	Type     Type
 }
 
-// GetProjects returns a list of all projects that have been created in the repository
-func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) {
-	e := db.GetEngine(ctx)
-	projects := make([]*Project, 0, setting.UI.IssuePagingNum)
-
-	var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID}
+func (opts *SearchOptions) toConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.RepoID > 0 {
+		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+	}
 	switch opts.IsClosed {
 	case util.OptionalBoolTrue:
 		cond = cond.And(builder.Eq{"is_closed": true})
@@ -142,6 +188,22 @@ func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, er
 	if opts.Type > 0 {
 		cond = cond.And(builder.Eq{"type": opts.Type})
 	}
+	if opts.OwnerID > 0 {
+		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+	}
+	return cond
+}
+
+// CountProjects counts projects
+func CountProjects(ctx context.Context, opts SearchOptions) (int64, error) {
+	return db.GetEngine(ctx).Where(opts.toConds()).Count(new(Project))
+}
+
+// FindProjects returns a list of all projects that have been created in the repository
+func FindProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) {
+	e := db.GetEngine(ctx)
+	projects := make([]*Project, 0, setting.UI.IssuePagingNum)
+	cond := opts.toConds()
 
 	count, err := e.Where(cond).Count(new(Project))
 	if err != nil {
@@ -188,8 +250,10 @@ func NewProject(p *Project) error {
 		return err
 	}
 
-	if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
-		return err
+	if p.RepoID > 0 {
+		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
+			return err
+		}
 	}
 
 	if err := createBoardsForProjectsType(ctx, p); err != nil {
diff --git a/models/project/project_test.go b/models/project/project_test.go
index 4fde0fc7c..c2d9005c4 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -22,7 +22,7 @@ func TestIsProjectTypeValid(t *testing.T) {
 	}{
 		{TypeIndividual, false},
 		{TypeRepository, true},
-		{TypeOrganization, false},
+		{TypeOrganization, true},
 		{UnknownType, false},
 	}
 
@@ -34,13 +34,13 @@ func TestIsProjectTypeValid(t *testing.T) {
 func TestGetProjects(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	projects, _, err := GetProjects(db.DefaultContext, SearchOptions{RepoID: 1})
+	projects, _, err := FindProjects(db.DefaultContext, SearchOptions{RepoID: 1})
 	assert.NoError(t, err)
 
 	// 1 value for this repo exists in the fixtures
 	assert.Len(t, projects, 1)
 
-	projects, _, err = GetProjects(db.DefaultContext, SearchOptions{RepoID: 3})
+	projects, _, err = FindProjects(db.DefaultContext, SearchOptions{RepoID: 3})
 	assert.NoError(t, err)
 
 	// 1 value for this repo exists in the fixtures
diff --git a/modules/context/org.go b/modules/context/org.go
index 39df29a86..ff3a5ae7e 100644
--- a/modules/context/org.go
+++ b/modules/context/org.go
@@ -9,7 +9,9 @@ import (
 
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
+	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 )
@@ -28,6 +30,32 @@ type Organization struct {
 	Teams []*organization.Team
 }
 
+func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool {
+	if ctx.Doer == nil {
+		return false
+	}
+	return org.UnitPermission(ctx, ctx.Doer.ID, unitType) >= perm.AccessModeWrite
+}
+
+func (org *Organization) UnitPermission(ctx *Context, doerID int64, unitType unit.Type) perm.AccessMode {
+	if doerID > 0 {
+		teams, err := organization.GetUserOrgTeams(ctx, org.Organization.ID, doerID)
+		if err != nil {
+			log.Error("GetUserOrgTeams: %v", err)
+			return perm.AccessModeNone
+		}
+		if len(teams) > 0 {
+			return teams.UnitMaxAccess(unitType)
+		}
+	}
+
+	if org.Organization.Visibility == structs.VisibleTypePublic {
+		return perm.AccessModeRead
+	}
+
+	return perm.AccessModeNone
+}
+
 // HandleOrgAssignment handles organization assignment
 func HandleOrgAssignment(ctx *Context, args ...bool) {
 	var (
diff --git a/routers/web/org/main_test.go b/routers/web/org/main_test.go
new file mode 100644
index 000000000..41323a360
--- /dev/null
+++ b/routers/web/org/main_test.go
@@ -0,0 +1,17 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+	"path/filepath"
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m, &unittest.TestOptions{
+		GiteaRootPath: filepath.Join("..", "..", ".."),
+	})
+}
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
new file mode 100644
index 000000000..1ce44d486
--- /dev/null
+++ b/routers/web/org/projects.go
@@ -0,0 +1,670 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	project_model "code.gitea.io/gitea/models/project"
+	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/forms"
+)
+
+const (
+	tplProjects           base.TplName = "org/projects/list"
+	tplProjectsNew        base.TplName = "org/projects/new"
+	tplProjectsView       base.TplName = "org/projects/view"
+	tplGenericProjectsNew base.TplName = "user/project"
+)
+
+// MustEnableProjects check if projects are enabled in settings
+func MustEnableProjects(ctx *context.Context) {
+	if unit.TypeProjects.UnitGlobalDisabled() {
+		ctx.NotFound("EnableKanbanBoard", nil)
+		return
+	}
+}
+
+// Projects renders the home page of projects
+func Projects(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.project_board")
+
+	sortType := ctx.FormTrim("sort")
+
+	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
+	page := ctx.FormInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	projects, total, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+		OwnerID:  ctx.ContextUser.ID,
+		Page:     page,
+		IsClosed: util.OptionalBoolOf(isShowClosed),
+		SortType: sortType,
+		Type:     project_model.TypeOrganization,
+	})
+	if err != nil {
+		ctx.ServerError("FindProjects", err)
+		return
+	}
+
+	opTotal, err := project_model.CountProjects(ctx, project_model.SearchOptions{
+		OwnerID:  ctx.ContextUser.ID,
+		IsClosed: util.OptionalBoolOf(!isShowClosed),
+		Type:     project_model.TypeOrganization,
+	})
+	if err != nil {
+		ctx.ServerError("CountProjects", err)
+		return
+	}
+
+	if isShowClosed {
+		ctx.Data["OpenCount"] = opTotal
+		ctx.Data["ClosedCount"] = total
+	} else {
+		ctx.Data["OpenCount"] = total
+		ctx.Data["ClosedCount"] = opTotal
+	}
+
+	ctx.Data["Projects"] = projects
+	shared_user.RenderUserHeader(ctx)
+
+	if isShowClosed {
+		ctx.Data["State"] = "closed"
+	} else {
+		ctx.Data["State"] = "open"
+	}
+
+	for _, project := range projects {
+		project.RenderedContent = project.Description
+	}
+
+	numPages := 0
+	if total > 0 {
+		numPages = (int(total) - 1/setting.UI.IssuePagingNum)
+	}
+
+	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
+	pager.AddParam(ctx, "state", "State")
+	ctx.Data["Page"] = pager
+
+	ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+	ctx.Data["IsShowClosed"] = isShowClosed
+	ctx.Data["PageIsViewProjects"] = true
+	ctx.Data["SortType"] = sortType
+
+	ctx.HTML(http.StatusOK, tplProjects)
+}
+
+func canWriteUnit(ctx *context.Context) bool {
+	if ctx.ContextUser.IsOrganization() {
+		return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects)
+	}
+	return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
+}
+
+// NewProject render creating a project page
+func NewProject(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+	ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
+	ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+	ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
+	shared_user.RenderUserHeader(ctx)
+	ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// NewProjectPost creates a new project
+func NewProjectPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.CreateProjectForm)
+	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+	shared_user.RenderUserHeader(ctx)
+
+	if ctx.HasError() {
+		ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+		ctx.Data["PageIsViewProjects"] = true
+		ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
+		ctx.HTML(http.StatusOK, tplProjectsNew)
+		return
+	}
+
+	if err := project_model.NewProject(&project_model.Project{
+		OwnerID:     ctx.ContextUser.ID,
+		Title:       form.Title,
+		Description: form.Content,
+		CreatorID:   ctx.Doer.ID,
+		BoardType:   form.BoardType,
+		Type:        project_model.TypeOrganization,
+	}); err != nil {
+		ctx.ServerError("NewProject", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
+	ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
+}
+
+// ChangeProjectStatus updates the status of a project between "open" and "close"
+func ChangeProjectStatus(ctx *context.Context) {
+	toClose := false
+	switch ctx.Params(":action") {
+	case "open":
+		toClose = false
+	case "close":
+		toClose = true
+	default:
+		ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+	}
+	id := ctx.ParamsInt64(":id")
+
+	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", err)
+		} else {
+			ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
+		}
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + url.QueryEscape(ctx.Params(":action")))
+}
+
+// DeleteProject delete a project
+func DeleteProject(ctx *context.Context) {
+	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+	if p.RepoID != ctx.Repo.Repository.ID {
+		ctx.NotFound("", nil)
+		return
+	}
+
+	if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
+		ctx.Flash.Error("DeleteProjectByID: " + err.Error())
+	} else {
+		ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"redirect": ctx.Repo.RepoLink + "/projects",
+	})
+}
+
+// EditProject allows a project to be edited
+func EditProject(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+	ctx.Data["PageIsEditProjects"] = true
+	ctx.Data["PageIsViewProjects"] = true
+	ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+	shared_user.RenderUserHeader(ctx)
+
+	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+	if p.RepoID != ctx.Repo.Repository.ID {
+		ctx.NotFound("", nil)
+		return
+	}
+
+	ctx.Data["title"] = p.Title
+	ctx.Data["content"] = p.Description
+
+	ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// EditProjectPost response for editing a project
+func EditProjectPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.CreateProjectForm)
+	ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+	ctx.Data["PageIsEditProjects"] = true
+	ctx.Data["PageIsViewProjects"] = true
+	ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+	shared_user.RenderUserHeader(ctx)
+
+	if ctx.HasError() {
+		ctx.HTML(http.StatusOK, tplProjectsNew)
+		return
+	}
+
+	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+	if p.RepoID != ctx.Repo.Repository.ID {
+		ctx.NotFound("", nil)
+		return
+	}
+
+	p.Title = form.Title
+	p.Description = form.Content
+	if err = project_model.UpdateProject(ctx, p); err != nil {
+		ctx.ServerError("UpdateProjects", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
+	ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+}
+
+// ViewProject renders the project board for a project
+func ViewProject(ctx *context.Context) {
+	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+	if project.OwnerID != ctx.ContextUser.ID {
+		ctx.NotFound("", nil)
+		return
+	}
+
+	boards, err := project_model.GetBoards(ctx, project.ID)
+	if err != nil {
+		ctx.ServerError("GetProjectBoards", err)
+		return
+	}
+
+	if boards[0].ID == 0 {
+		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+	}
+
+	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
+	if err != nil {
+		ctx.ServerError("LoadIssuesOfBoards", err)
+		return
+	}
+
+	linkedPrsMap := make(map[int64][]*issues_model.Issue)
+	for _, issuesList := range issuesMap {
+		for _, issue := range issuesList {
+			var referencedIds []int64
+			for _, comment := range issue.Comments {
+				if comment.RefIssueID != 0 && comment.RefIsPull {
+					referencedIds = append(referencedIds, comment.RefIssueID)
+				}
+			}
+
+			if len(referencedIds) > 0 {
+				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
+					IssueIDs: referencedIds,
+					IsPull:   util.OptionalBoolTrue,
+				}); err == nil {
+					linkedPrsMap[issue.ID] = linkedPrs
+				}
+			}
+		}
+	}
+
+	project.RenderedContent = project.Description
+	ctx.Data["LinkedPRs"] = linkedPrsMap
+	ctx.Data["PageIsViewProjects"] = true
+	ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+	ctx.Data["Project"] = project
+	ctx.Data["IssuesMap"] = issuesMap
+	ctx.Data["Boards"] = boards
+	shared_user.RenderUserHeader(ctx)
+
+	ctx.HTML(http.StatusOK, tplProjectsView)
+}
+
+func getActionIssues(ctx *context.Context) []*issues_model.Issue {
+	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
+	if len(commaSeparatedIssueIDs) == 0 {
+		return nil
+	}
+	issueIDs := make([]int64, 0, 10)
+	for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+		issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
+		if err != nil {
+			ctx.ServerError("ParseInt", err)
+			return nil
+		}
+		issueIDs = append(issueIDs, issueID)
+	}
+	issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
+	if err != nil {
+		ctx.ServerError("GetIssuesByIDs", err)
+		return nil
+	}
+	// Check access rights for all issues
+	issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
+	prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
+	for _, issue := range issues {
+		if issue.RepoID != ctx.Repo.Repository.ID {
+			ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
+			return nil
+		}
+		if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
+			ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+			return nil
+		}
+		if err = issue.LoadAttributes(ctx); err != nil {
+			ctx.ServerError("LoadAttributes", err)
+			return nil
+		}
+	}
+	return issues
+}
+
+// UpdateIssueProject change an issue's project
+func UpdateIssueProject(ctx *context.Context) {
+	issues := getActionIssues(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	projectID := ctx.FormInt64("id")
+	for _, issue := range issues {
+		oldProjectID := issue.ProjectID()
+		if oldProjectID == projectID {
+			continue
+		}
+
+		if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+			ctx.ServerError("ChangeProjectAssign", err)
+			return
+		}
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
+
+// DeleteProjectBoard allows for the deletion of a project board
+func DeleteProjectBoard(ctx *context.Context) {
+	if ctx.Doer == nil {
+		ctx.JSON(http.StatusForbidden, map[string]string{
+			"message": "Only signed in users are allowed to perform this action.",
+		})
+		return
+	}
+
+	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+
+	pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		ctx.ServerError("GetProjectBoard", err)
+		return
+	}
+	if pb.ProjectID != ctx.ParamsInt64(":id") {
+		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+		})
+		return
+	}
+
+	if project.OwnerID != ctx.ContextUser.ID {
+		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+			"message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
+		})
+		return
+	}
+
+	if err := project_model.DeleteBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
+		ctx.ServerError("DeleteProjectBoardByID", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
+
+// AddBoardToProjectPost allows a new board to be added to a project.
+func AddBoardToProjectPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+
+	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+
+	if err := project_model.NewBoard(&project_model.Board{
+		ProjectID: project.ID,
+		Title:     form.Title,
+		Color:     form.Color,
+		CreatorID: ctx.Doer.ID,
+	}); err != nil {
+		ctx.ServerError("NewProjectBoard", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
+
+// CheckProjectBoardChangePermissions check permission
+func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
+	if ctx.Doer == nil {
+		ctx.JSON(http.StatusForbidden, map[string]string{
+			"message": "Only signed in users are allowed to perform this action.",
+		})
+		return nil, nil
+	}
+
+	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return nil, nil
+	}
+
+	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		ctx.ServerError("GetProjectBoard", err)
+		return nil, nil
+	}
+	if board.ProjectID != ctx.ParamsInt64(":id") {
+		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
+		})
+		return nil, nil
+	}
+
+	if project.OwnerID != ctx.ContextUser.ID {
+		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+			"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
+		})
+		return nil, nil
+	}
+	return project, board
+}
+
+// EditProjectBoard allows a project board's to be updated
+func EditProjectBoard(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+	_, board := CheckProjectBoardChangePermissions(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	if form.Title != "" {
+		board.Title = form.Title
+	}
+
+	board.Color = form.Color
+
+	if form.Sorting != 0 {
+		board.Sorting = form.Sorting
+	}
+
+	if err := project_model.UpdateBoard(ctx, board); err != nil {
+		ctx.ServerError("UpdateProjectBoard", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
+
+// SetDefaultProjectBoard set default board for uncategorized issues/pulls
+func SetDefaultProjectBoard(ctx *context.Context) {
+	project, board := CheckProjectBoardChangePermissions(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	if err := project_model.SetDefaultBoard(project.ID, board.ID); err != nil {
+		ctx.ServerError("SetDefaultBoard", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
+
+// MoveIssues moves or keeps issues in a column and sorts them inside that column
+func MoveIssues(ctx *context.Context) {
+	if ctx.Doer == nil {
+		ctx.JSON(http.StatusForbidden, map[string]string{
+			"message": "Only signed in users are allowed to perform this action.",
+		})
+		return
+	}
+
+	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+	if err != nil {
+		if project_model.IsErrProjectNotExist(err) {
+			ctx.NotFound("ProjectNotExist", nil)
+		} else {
+			ctx.ServerError("GetProjectByID", err)
+		}
+		return
+	}
+	if project.OwnerID != ctx.ContextUser.ID {
+		ctx.NotFound("InvalidRepoID", nil)
+		return
+	}
+
+	var board *project_model.Board
+
+	if ctx.ParamsInt64(":boardID") == 0 {
+		board = &project_model.Board{
+			ID:        0,
+			ProjectID: project.ID,
+			Title:     ctx.Tr("repo.projects.type.uncategorized"),
+		}
+	} else {
+		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+		if err != nil {
+			if project_model.IsErrProjectBoardNotExist(err) {
+				ctx.NotFound("ProjectBoardNotExist", nil)
+			} else {
+				ctx.ServerError("GetProjectBoard", err)
+			}
+			return
+		}
+		if board.ProjectID != project.ID {
+			ctx.NotFound("BoardNotInProject", nil)
+			return
+		}
+	}
+
+	type movedIssuesForm struct {
+		Issues []struct {
+			IssueID int64 `json:"issueID"`
+			Sorting int64 `json:"sorting"`
+		} `json:"issues"`
+	}
+
+	form := &movedIssuesForm{}
+	if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
+		ctx.ServerError("DecodeMovedIssuesForm", err)
+	}
+
+	issueIDs := make([]int64, 0, len(form.Issues))
+	sortedIssueIDs := make(map[int64]int64)
+	for _, issue := range form.Issues {
+		issueIDs = append(issueIDs, issue.IssueID)
+		sortedIssueIDs[issue.Sorting] = issue.IssueID
+	}
+	movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
+	if err != nil {
+		if issues_model.IsErrIssueNotExist(err) {
+			ctx.NotFound("IssueNotExisting", nil)
+		} else {
+			ctx.ServerError("GetIssueByID", err)
+		}
+		return
+	}
+
+	if len(movedIssues) != len(form.Issues) {
+		ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
+		return
+	}
+
+	if _, err = movedIssues.LoadRepositories(ctx); err != nil {
+		ctx.ServerError("LoadRepositories", err)
+		return
+	}
+
+	for _, issue := range movedIssues {
+		if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID {
+			ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID"))
+			return
+		}
+	}
+
+	if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
+		ctx.ServerError("MoveIssuesOnProjectBoard", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, map[string]interface{}{
+		"ok": true,
+	})
+}
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
new file mode 100644
index 000000000..3450fa8e7
--- /dev/null
+++ b/routers/web/org/projects_test.go
@@ -0,0 +1,28 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/routers/web/org"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCheckProjectBoardChangePermissions(t *testing.T) {
+	unittest.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "user2/-/projects/4/4")
+	test.LoadUser(t, ctx, 2)
+	ctx.ContextUser = ctx.Doer // user2
+	ctx.SetParams(":id", "4")
+	ctx.SetParams(":boardID", "4")
+
+	project, board := org.CheckProjectBoardChangePermissions(ctx)
+	assert.NotNil(t, project)
+	assert.NotNil(t, board)
+	assert.False(t, ctx.Written())
+}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 59ab717a1..44ac81f65 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -363,7 +363,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	}
 
 	if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") {
-		projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{
+		projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
 			RepoID:   repo.ID,
 			Type:     project_model.TypeRepository,
 			IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -474,8 +474,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
 
 func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	var err error
-
-	ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+	projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
 		RepoID:   repo.ID,
 		Page:     -1,
 		IsClosed: util.OptionalBoolFalse,
@@ -485,8 +484,20 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 		ctx.ServerError("GetProjects", err)
 		return
 	}
+	projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+		OwnerID:  repo.OwnerID,
+		Page:     -1,
+		IsClosed: util.OptionalBoolFalse,
+		Type:     project_model.TypeOrganization,
+	})
+	if err != nil {
+		ctx.ServerError("GetProjects", err)
+		return
+	}
 
-	ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+	ctx.Data["OpenProjects"] = append(projects, projects2...)
+
+	projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
 		RepoID:   repo.ID,
 		Page:     -1,
 		IsClosed: util.OptionalBoolTrue,
@@ -496,6 +507,18 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 		ctx.ServerError("GetProjects", err)
 		return
 	}
+	projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
+		OwnerID:  repo.OwnerID,
+		Page:     -1,
+		IsClosed: util.OptionalBoolTrue,
+		Type:     project_model.TypeOrganization,
+	})
+	if err != nil {
+		ctx.ServerError("GetProjects", err)
+		return
+	}
+
+	ctx.Data["ClosedProjects"] = append(projects, projects2...)
 }
 
 // repoReviewerSelection items to bee shown
@@ -988,7 +1011,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
 			ctx.ServerError("GetProjectByID", err)
 			return nil, nil, 0, 0
 		}
-		if p.RepoID != ctx.Repo.Repository.ID {
+		if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
 			ctx.NotFound("", nil)
 			return nil, nil, 0, 0
 		}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 75cd290b8..3becf799c 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -70,7 +70,7 @@ func Projects(ctx *context.Context) {
 		total = repo.NumClosedProjects
 	}
 
-	projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{
+	projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
 		RepoID:   repo.ID,
 		Page:     page,
 		IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -112,7 +112,7 @@ func Projects(ctx *context.Context) {
 	pager.AddParam(ctx, "state", "State")
 	ctx.Data["Page"] = pager
 
-	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
+	ctx.Data["CanWriteProjects"] = true
 	ctx.Data["IsShowClosed"] = isShowClosed
 	ctx.Data["IsProjectsPage"] = true
 	ctx.Data["SortType"] = sortType
@@ -653,47 +653,3 @@ func MoveIssues(ctx *context.Context) {
 		"ok": true,
 	})
 }
-
-// CreateProject renders the generic project creation page
-func CreateProject(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-	ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
-	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
-
-	ctx.HTML(http.StatusOK, tplGenericProjectsNew)
-}
-
-// CreateProjectPost creates an individual and/or organization project
-func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
-	user := checkContextUser(ctx, form.UID)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.Data["ContextUser"] = user
-
-	if ctx.HasError() {
-		ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
-		ctx.HTML(http.StatusOK, tplGenericProjectsNew)
-		return
-	}
-
-	projectType := project_model.TypeIndividual
-	if user.IsOrganization() {
-		projectType = project_model.TypeOrganization
-	}
-
-	if err := project_model.NewProject(&project_model.Project{
-		Title:       form.Title,
-		Description: form.Content,
-		CreatorID:   user.ID,
-		BoardType:   form.BoardType,
-		Type:        projectType,
-	}); err != nil {
-		ctx.ServerError("NewProject", err)
-		return
-	}
-
-	ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
-	ctx.Redirect(setting.AppSubURL + "/")
-}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
new file mode 100644
index 000000000..94e59e2a4
--- /dev/null
+++ b/routers/web/shared/user/header.go
@@ -0,0 +1,14 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+func RenderUserHeader(ctx *context.Context) {
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	ctx.Data["ContextUser"] = ctx.ContextUser
+}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index c0aba7583..ed4f0dd79 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	"code.gitea.io/gitea/services/forms"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
@@ -83,10 +84,10 @@ func ListPackages(ctx *context.Context) {
 		return
 	}
 
+	shared_user.RenderUserHeader(ctx)
+
 	ctx.Data["Title"] = ctx.Tr("packages.title")
 	ctx.Data["IsPackagesPage"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["Query"] = query
 	ctx.Data["PackageType"] = packageType
 	ctx.Data["AvailableTypes"] = packages_model.TypeList
@@ -156,10 +157,10 @@ func RedirectToLastVersion(ctx *context.Context) {
 func ViewPackageVersion(ctx *context.Context) {
 	pd := ctx.Package.Descriptor
 
+	shared_user.RenderUserHeader(ctx)
+
 	ctx.Data["Title"] = pd.Package.Name
 	ctx.Data["IsPackagesPage"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = pd
 
 	var (
@@ -235,10 +236,10 @@ func ListPackageVersions(ctx *context.Context) {
 	query := ctx.FormTrim("q")
 	sort := ctx.FormTrim("sort")
 
+	shared_user.RenderUserHeader(ctx)
+
 	ctx.Data["Title"] = ctx.Tr("packages.title")
 	ctx.Data["IsPackagesPage"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{
 		Package: p,
 		Owner:   ctx.Package.Owner,
@@ -311,10 +312,10 @@ func ListPackageVersions(ctx *context.Context) {
 func PackageSettings(ctx *context.Context) {
 	pd := ctx.Package.Descriptor
 
+	shared_user.RenderUserHeader(ctx)
+
 	ctx.Data["Title"] = pd.Package.Name
 	ctx.Data["IsPackagesPage"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = pd
 
 	repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 0002d56de..0e342991d 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -224,7 +224,7 @@ func Profile(ctx *context.Context) {
 
 		total = int(count)
 	case "projects":
-		ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+		ctx.Data["OpenProjects"], _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
 			Page:     -1,
 			IsClosed: util.OptionalBoolFalse,
 			Type:     project_model.TypeIndividual,
diff --git a/routers/web/web.go b/routers/web/web.go
index f0fedd071..d37d82820 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -835,6 +835,46 @@ func RegisterRoutes(m *web.Route) {
 				})
 			}, ignSignIn, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
 		}
+
+		m.Group("/projects", func() {
+			m.Get("", org.Projects)
+			m.Get("/{id}", org.ViewProject)
+			m.Group("", func() { //nolint:dupl
+				m.Get("/new", org.NewProject)
+				m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
+				m.Group("/{id}", func() {
+					m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
+					m.Post("/delete", org.DeleteProject)
+
+					m.Get("/edit", org.EditProject)
+					m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
+					m.Post("/{action:open|close}", org.ChangeProjectStatus)
+
+					m.Group("/{boardID}", func() {
+						m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
+						m.Delete("", org.DeleteProjectBoard)
+						m.Post("/default", org.SetDefaultProjectBoard)
+
+						m.Post("/move", org.MoveIssues)
+					})
+				})
+			}, reqSignIn, func(ctx *context.Context) {
+				if ctx.ContextUser == nil {
+					ctx.NotFound("NewProject", nil)
+					return
+				}
+				if ctx.ContextUser.IsOrganization() {
+					if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) {
+						ctx.NotFound("NewProject", nil)
+						return
+					}
+				} else if ctx.ContextUser.ID != ctx.Doer.ID {
+					ctx.NotFound("NewProject", nil)
+					return
+				}
+			})
+		}, repo.MustEnableProjects)
+
 		m.Get("/code", user.CodeSearch)
 	}, context_service.UserAssignmentWeb())
 
@@ -1168,7 +1208,7 @@ func RegisterRoutes(m *web.Route) {
 		m.Group("/projects", func() {
 			m.Get("", repo.Projects)
 			m.Get("/{id}", repo.ViewProject)
-			m.Group("", func() {
+			m.Group("", func() { //nolint:dupl
 				m.Get("/new", repo.NewProject)
 				m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
 				m.Group("/{id}", func() {
diff --git a/services/context/user.go b/services/context/user.go
index 9dc84c3ac..7642cba4e 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"strings"
 
+	org_model "code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
 )
@@ -56,6 +57,14 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{}))
 			} else {
 				errCb(http.StatusInternalServerError, "GetUserByName", err)
 			}
+		} else {
+			if ctx.ContextUser.IsOrganization() {
+				if ctx.Org == nil {
+					ctx.Org = &context.Organization{}
+				}
+				ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser)
+				ctx.Data["Org"] = ctx.Org.Organization
+			}
 		}
 	}
 }
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 87242b94d..5f543424f 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -3,6 +3,9 @@
 		
 			{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
 		
+		
+			{{svg "octicon-project"}} {{.locale.Tr "user.projects"}}
+		
 		{{if .IsPackageEnabled}}
 		
 			{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
new file mode 100644
index 000000000..544ed3874
--- /dev/null
+++ b/templates/org/projects/list.tmpl
@@ -0,0 +1,6 @@
+{{template "base/head" .}}
+ {{.locale.Tr "repo.projects.deletion_desc"}} {{.locale.Tr "repo.projects.deletion_desc"}}
+			{{if .PageIsEditProjects}}
+			{{.locale.Tr "repo.projects.edit"}}
+			
+		{{template "base/alert" .}}
+		
+	{{$.Project.Title}}
+