summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/content/usage/blocking-users.en-us.md56
-rw-r--r--models/fixtures/access.yml50
-rw-r--r--models/fixtures/collaboration.yml12
-rw-r--r--models/fixtures/issue_assignees.yml4
-rw-r--r--models/fixtures/repo_transfer.yml16
-rw-r--r--models/fixtures/repository.yml8
-rw-r--r--models/fixtures/star.yml10
-rw-r--r--models/fixtures/user.yml2
-rw-r--r--models/fixtures/user_blocking.yml19
-rw-r--r--models/fixtures/watch.yml12
-rw-r--r--models/issues/assignees.go21
-rw-r--r--models/issues/issue_update.go9
-rw-r--r--models/issues/issue_xref.go4
-rw-r--r--models/issues/reaction.go19
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v1_22/v288.go26
-rw-r--r--models/org.go36
-rw-r--r--models/org_team.go79
-rw-r--r--models/org_team_test.go100
-rw-r--r--models/org_test.go49
-rw-r--r--models/organization/org.go1
-rw-r--r--models/organization/team_user.go8
-rw-r--r--models/perm/access/access.go4
-rw-r--r--models/repo/collaboration.go55
-rw-r--r--models/repo/collaboration_test.go37
-rw-r--r--models/repo/repo_test.go17
-rw-r--r--models/repo/star.go20
-rw-r--r--models/repo/star_test.go40
-rw-r--r--models/repo/user_repo.go97
-rw-r--r--models/repo/user_repo_test.go6
-rw-r--r--models/repo/watch.go28
-rw-r--r--models/repo/watch_test.go26
-rw-r--r--models/repo_transfer.go37
-rw-r--r--models/user/block.go123
-rw-r--r--models/user/follow.go14
-rw-r--r--models/user/user.go2
-rw-r--r--models/user/user_test.go17
-rw-r--r--modules/repository/collaborator.go8
-rw-r--r--modules/repository/create.go2
-rw-r--r--options/locale/locale_en-US.ini33
-rw-r--r--routers/api/v1/api.go20
-rw-r--r--routers/api/v1/org/block.go116
-rw-r--r--routers/api/v1/org/member.go2
-rw-r--r--routers/api/v1/org/team.go13
-rw-r--r--routers/api/v1/repo/collaborators.go24
-rw-r--r--routers/api/v1/repo/fork.go2
-rw-r--r--routers/api/v1/repo/issue.go14
-rw-r--r--routers/api/v1/repo/issue_comment.go14
-rw-r--r--routers/api/v1/repo/issue_comment_attachment.go10
-rw-r--r--routers/api/v1/repo/issue_reaction.go12
-rw-r--r--routers/api/v1/repo/pull.go10
-rw-r--r--routers/api/v1/repo/transfer.go7
-rw-r--r--routers/api/v1/shared/block.go98
-rw-r--r--routers/api/v1/user/block.go96
-rw-r--r--routers/api/v1/user/follower.go11
-rw-r--r--routers/api/v1/user/star.go27
-rw-r--r--routers/api/v1/user/watch.go27
-rw-r--r--routers/web/org/block.go38
-rw-r--r--routers/web/org/members.go22
-rw-r--r--routers/web/org/teams.go17
-rw-r--r--routers/web/repo/issue.go37
-rw-r--r--routers/web/repo/pull.go20
-rw-r--r--routers/web/repo/repo.go16
-rw-r--r--routers/web/repo/setting/collaboration.go26
-rw-r--r--routers/web/repo/setting/setting.go3
-rw-r--r--routers/web/shared/user/block.go76
-rw-r--r--routers/web/shared/user/header.go8
-rw-r--r--routers/web/user/profile.go2
-rw-r--r--routers/web/user/setting/block.go38
-rw-r--r--routers/web/web.go10
-rw-r--r--services/auth/source/source_group_sync.go4
-rw-r--r--services/forms/user_form.go11
-rw-r--r--services/issue/comments.go26
-rw-r--r--services/issue/commit.go4
-rw-r--r--services/issue/content.go13
-rw-r--r--services/issue/issue.go41
-rw-r--r--services/issue/reaction.go50
-rw-r--r--services/issue/reaction_test.go (renamed from models/issues/reaction_test.go)90
-rw-r--r--services/pull/pull.go8
-rw-r--r--services/repository/collaboration.go16
-rw-r--r--services/repository/collaboration_test.go11
-rw-r--r--services/repository/delete.go14
-rw-r--r--services/repository/fork.go8
-rw-r--r--services/repository/transfer.go12
-rw-r--r--services/user/block.go308
-rw-r--r--services/user/block_test.go66
-rw-r--r--services/user/delete.go2
-rw-r--r--services/user/user.go2
-rw-r--r--services/user/user_test.go3
-rw-r--r--templates/org/settings/blocked_users.tmpl5
-rw-r--r--templates/org/settings/navbar.tmpl3
-rw-r--r--templates/repo/diff/box.tmpl1
-rw-r--r--templates/repo/issue/view_content.tmpl1
-rw-r--r--templates/repo/issue/view_content/context_menu.tmpl35
-rw-r--r--templates/shared/user/block_user_dialog.tmpl23
-rw-r--r--templates/shared/user/blocked_users.tmpl83
-rw-r--r--templates/shared/user/profile_big_avatar.tmpl37
-rw-r--r--templates/swagger/v1_json.tmpl283
-rw-r--r--templates/user/settings/blocked_users.tmpl5
-rw-r--r--templates/user/settings/navbar.tmpl3
-rw-r--r--tests/integration/api_comment_test.go26
-rw-r--r--tests/integration/api_issue_reaction_test.go7
-rw-r--r--tests/integration/api_issue_test.go10
-rw-r--r--tests/integration/api_repo_collaborator_test.go7
-rw-r--r--tests/integration/api_user_block_test.go243
-rw-r--r--tests/integration/api_user_follow_test.go8
-rw-r--r--tests/integration/api_user_star_test.go8
-rw-r--r--tests/integration/api_user_watch_test.go8
-rw-r--r--tests/integration/auth_ldap_test.go6
109 files changed, 2873 insertions, 543 deletions
diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md
new file mode 100644
index 0000000000..b59bbe4d62
--- /dev/null
+++ b/docs/content/usage/blocking-users.en-us.md
@@ -0,0 +1,56 @@
+---
+date: "2024-01-31T00:00:00+00:00"
+title: "Blocking a user"
+slug: "blocking-user"
+sidebar_position: 25
+toc: false
+draft: false
+aliases:
+ - /en-us/webhooks
+menu:
+ sidebar:
+ parent: "usage"
+ name: "Blocking a user"
+ sidebar_position: 30
+ identifier: "blocking-user"
+---
+
+# Blocking a user
+
+Gitea supports blocking of users to restrict how they can interact with you and your content.
+
+You can block a user in your account settings, from the user's profile or from comments created by the user.
+The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
+Organization owners can block anyone who is not a member of the organization too.
+If a blocked user has admin permissions, they can still perform all actions even if blocked.
+
+### When you block a user
+
+- the user stops following you
+- you stop following the user
+- the user's stars are removed from your repositories
+- your stars are removed from their repositories
+- the user stops watching your repositories
+- you stop watching their repositories
+- the user's issue assignments are removed from your repositories
+- your issue assignments are removed from their repositories
+- the user is removed as a collaborator on your repositories
+- you are removed as a collaborator on their repositories
+- any pending repository transfers to or from the blocked user are canceled
+
+### When you block a user, the user cannot
+
+- follow you
+- watch your repositories
+- star your repositories
+- fork your repositories
+- transfer repositories to you
+- open issues or pull requests on your repositories
+- comment on issues or pull requests you've created
+- comment on issues or pull requests on your repositories
+- react to your comments on issues or pull requests
+- react to comments on issues or pull requests on your repositories
+- assign you to issues or pull requests
+- add you as a collaborator on their repositories
+- send you notifications by @mentioning your username
+- be added as team member (if blocked by an organization)
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 641c453eb7..4171e31fef 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -42,120 +42,132 @@
-
id: 8
- user_id: 15
+ user_id: 10
repo_id: 21
mode: 2
-
id: 9
+ user_id: 10
+ repo_id: 32
+ mode: 2
+
+-
+ id: 10
+ user_id: 15
+ repo_id: 21
+ mode: 2
+
+-
+ id: 11
user_id: 15
repo_id: 22
mode: 2
-
- id: 10
+ id: 12
user_id: 15
repo_id: 23
mode: 4
-
- id: 11
+ id: 13
user_id: 15
repo_id: 24
mode: 4
-
- id: 12
+ id: 14
user_id: 15
repo_id: 32
mode: 2
-
- id: 13
+ id: 15
user_id: 18
repo_id: 21
mode: 2
-
- id: 14
+ id: 16
user_id: 18
repo_id: 22
mode: 2
-
- id: 15
+ id: 17
user_id: 18
repo_id: 23
mode: 4
-
- id: 16
+ id: 18
user_id: 18
repo_id: 24
mode: 4
-
- id: 17
+ id: 19
user_id: 20
repo_id: 24
mode: 1
-
- id: 18
+ id: 20
user_id: 20
repo_id: 27
mode: 4
-
- id: 19
+ id: 21
user_id: 20
repo_id: 28
mode: 4
-
- id: 20
+ id: 22
user_id: 29
repo_id: 4
mode: 2
-
- id: 21
+ id: 23
user_id: 29
repo_id: 24
mode: 1
-
- id: 22
+ id: 24
user_id: 31
repo_id: 27
mode: 4
-
- id: 23
+ id: 25
user_id: 31
repo_id: 28
mode: 4
-
- id: 24
+ id: 26
user_id: 38
repo_id: 60
mode: 2
-
- id: 25
+ id: 27
user_id: 38
repo_id: 61
mode: 1
-
- id: 26
+ id: 28
user_id: 39
repo_id: 61
mode: 1
-
- id: 27
+ id: 29
user_id: 40
repo_id: 61
mode: 4
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index 7603bdad32..4c3ac367f6 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -51,3 +51,15 @@
repo_id: 60
user_id: 38
mode: 2 # write
+
+-
+ id: 10
+ repo_id: 21
+ user_id: 10
+ mode: 2 # write
+
+-
+ id: 11
+ repo_id: 32
+ user_id: 10
+ mode: 2 # write
diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml
index e5d36f921a..c40ecad676 100644
--- a/models/fixtures/issue_assignees.yml
+++ b/models/fixtures/issue_assignees.yml
@@ -14,3 +14,7 @@
id: 4
assignee_id: 2
issue_id: 17
+-
+ id: 5
+ assignee_id: 10
+ issue_id: 6
diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml
index b841b5e983..db92c95248 100644
--- a/models/fixtures/repo_transfer.yml
+++ b/models/fixtures/repo_transfer.yml
@@ -5,3 +5,19 @@
repo_id: 3
created_unix: 1553610671
updated_unix: 1553610671
+
+-
+ id: 2
+ doer_id: 16
+ recipient_id: 10
+ repo_id: 21
+ created_unix: 1553610671
+ updated_unix: 1553610671
+
+-
+ id: 3
+ doer_id: 3
+ recipient_id: 10
+ repo_id: 32
+ created_unix: 1553610671
+ updated_unix: 1553610671
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index d094fe82d8..e5c6224c96 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -614,8 +614,8 @@
owner_name: user16
lower_name: big_test_public_3
name: big_test_public_3
- num_watches: 0
- num_stars: 0
+ num_watches: 1
+ num_stars: 1
num_forks: 0
num_issues: 0
num_closed_issues: 0
@@ -945,8 +945,8 @@
owner_name: org3
lower_name: repo21
name: repo21
- num_watches: 0
- num_stars: 0
+ num_watches: 1
+ num_stars: 1
num_forks: 0
num_issues: 2
num_closed_issues: 0
diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml
index 860f26b8e2..39b51b3736 100644
--- a/models/fixtures/star.yml
+++ b/models/fixtures/star.yml
@@ -7,3 +7,13 @@
id: 2
uid: 2
repo_id: 4
+
+-
+ id: 3
+ uid: 10
+ repo_id: 21
+
+-
+ id: 4
+ uid: 10
+ repo_id: 32
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 16b687ae04..a3de535508 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -361,7 +361,7 @@
use_custom_avatar: false
num_followers: 0
num_following: 0
- num_stars: 0
+ num_stars: 2
num_repos: 3
num_teams: 0
num_members: 0
diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml
new file mode 100644
index 0000000000..2ec9d99df5
--- /dev/null
+++ b/models/fixtures/user_blocking.yml
@@ -0,0 +1,19 @@
+-
+ id: 1
+ blocker_id: 2
+ blockee_id: 29
+
+-
+ id: 2
+ blocker_id: 17
+ blockee_id: 28
+
+-
+ id: 3
+ blocker_id: 2
+ blockee_id: 34
+
+-
+ id: 4
+ blocker_id: 50
+ blockee_id: 34
diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml
index 1950ac99e7..18bcd2ed2b 100644
--- a/models/fixtures/watch.yml
+++ b/models/fixtures/watch.yml
@@ -27,3 +27,15 @@
user_id: 11
repo_id: 1
mode: 3 # auto
+
+-
+ id: 6
+ user_id: 10
+ repo_id: 21
+ mode: 1 # normal
+
+-
+ id: 7
+ user_id: 10
+ repo_id: 32
+ mode: 1 # normal
diff --git a/models/issues/assignees.go b/models/issues/assignees.go
index 60f32d9557..30234be07a 100644
--- a/models/issues/assignees.go
+++ b/models/issues/assignees.go
@@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
}
+type AssignedIssuesOptions struct {
+ db.ListOptions
+ AssigneeID int64
+ RepoOwnerID int64
+}
+
+func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.AssigneeID != 0 {
+ cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
+ }
+ return cond
+}
+
+func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
+ return db.FindAndCount[Issue](ctx, opts)
+}
+
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
ctx, committer, err := db.TxContext(ctx)
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index b258dc882c..ef96e1ee50 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
if err != nil {
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
}
+
+ notBlocked := make([]*user_model.User, 0, len(mentions))
+ for _, user := range mentions {
+ if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
+ notBlocked = append(notBlocked, user)
+ }
+ }
+ mentions = notBlocked
+
if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
}
diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go
index cfc3c1683c..e2e35859df 100644
--- a/models/issues/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
return nil, references.XRefActionNone, nil
}
+ if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
+ return nil, references.XRefActionNone, nil
+ }
+
// Accept close/reopening actions only if the poster is able to close the
// referenced issue manually at this moment. The only exception is
// the poster of a new PR referencing an issue on the same repo: then the merger
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index bb47cf24ca..d5448636fe 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
return reaction, nil
}
-// CreateIssueReaction creates a reaction on issue.
-func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) {
- return CreateReaction(ctx, &ReactionOptions{
- Type: content,
- DoerID: doerID,
- IssueID: issueID,
- })
-}
-
-// CreateCommentReaction creates a reaction on comment.
-func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) {
- return CreateReaction(ctx, &ReactionOptions{
- Type: content,
- DoerID: doerID,
- IssueID: issueID,
- CommentID: commentID,
- })
-}
-
// DeleteReaction deletes reaction for issue or comment.
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
reaction := &Reaction{
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 516eb53f62..9d288ec2bd 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -560,6 +560,8 @@ var migrations = []Migration{
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
// v287 -> v288
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
+ // v288 -> v289
+ NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go
new file mode 100644
index 0000000000..7c93bfcc66
--- /dev/null
+++ b/models/migrations/v1_22/v288.go
@@ -0,0 +1,26 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+type Blocking struct {
+ ID int64 `xorm:"pk autoincr"`
+ BlockerID int64 `xorm:"UNIQUE(block)"`
+ BlockeeID int64 `xorm:"UNIQUE(block)"`
+ Note string
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+ return "user_blocking"
+}
+
+func AddUserBlockingTable(x *xorm.Engine) error {
+ return x.Sync(&Blocking{})
+}
diff --git a/models/org.go b/models/org.go
index 5f61f05b16..69cc47137e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -12,15 +12,16 @@ import (
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
)
// RemoveOrgUser removes user from given organization.
-func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
+func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
ou := new(organization.OrgUser)
has, err := db.GetEngine(ctx).
- Where("uid=?", userID).
- And("org_id=?", orgID).
+ Where("uid=?", user.ID).
+ And("org_id=?", org.ID).
Get(ou)
if err != nil {
return fmt.Errorf("get org-user: %w", err)
@@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
return nil
}
- org, err := organization.GetOrgByID(ctx, orgID)
- if err != nil {
- return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
- }
-
// Check if the user to delete is the last member in owner team.
- if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
+ if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
return err
} else if isOwner {
t, err := organization.GetOwnerTeam(ctx, org.ID)
@@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
if err := t.LoadMembers(ctx); err != nil {
return err
}
- if t.Members[0].ID == userID {
- return organization.ErrLastOrgOwner{UID: userID}
+ if t.Members[0].ID == user.ID {
+ return organization.ErrLastOrgOwner{UID: user.ID}
}
}
}
@@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
return err
- } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
+ } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
return err
}
// Delete all repository accesses and unwatch them.
- env, err := organization.AccessibleReposEnv(ctx, org, userID)
+ env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
if err != nil {
return fmt.Errorf("AccessibleReposEnv: %w", err)
}
repoIDs, err := env.RepoIDs(1, org.NumRepos)
if err != nil {
- return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
+ return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
}
for _, repoID := range repoIDs {
- if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+ if err != nil {
+ return err
+ }
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
}
if len(repoIDs) > 0 {
if _, err = db.GetEngine(ctx).
- Where("user_id = ?", userID).
+ Where("user_id = ?", user.ID).
In("repo_id", repoIDs).
Delete(new(access_model.Access)); err != nil {
return err
@@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
}
// Delete member in their teams.
- teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
+ teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
if err != nil {
return err
}
for _, t := range teams {
- if err = removeTeamMember(ctx, t, userID); err != nil {
+ if err = removeTeamMember(ctx, t, user); err != nil {
return err
}
}
diff --git a/models/org_team.go b/models/org_team.go
index 1a452436c3..aecf0d80fd 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
return fmt.Errorf("getMembers: %w", err)
}
for _, u := range t.Members {
- if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
return fmt.Errorf("watchRepo: %w", err)
}
}
@@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
continue
}
- if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
@@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
}
for _, tm := range t.Members {
- if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
+ if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
return err
}
}
@@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
// AddTeamMember adds new membership of given team to given organization,
// the user will have membership to given organization automatically when needed.
-func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
- isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+ if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
+ return user_model.ErrBlockedUser
+ }
+
+ isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || isAlreadyMember {
return err
}
- if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
+ if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
return err
}
err = db.WithTx(ctx, func(ctx context.Context) error {
// check in transaction
- isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+ isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || isAlreadyMember {
return err
}
@@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
sess := db.GetEngine(ctx)
if err := db.Insert(ctx, &organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: team.OrgID,
TeamID: team.ID,
}); err != nil {
@@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
subQuery := builder.Select("repo_id").From("team_repo").
Where(builder.Eq{"team_id": team.ID})
- if _, err := sess.Where("user_id=?", userID).
+ if _, err := sess.Where("user_id=?", user.ID).
In("repo_id", subQuery).
And("mode < ?", team.AccessMode).
SetExpr("mode", team.AccessMode).
@@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
// for not exist access
var repoIDs []int64
- accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
+ accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
return fmt.Errorf("select id accesses: %w", err)
}
accesses := make([]*access_model.Access, 0, 100)
for i, repoID := range repoIDs {
- accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
+ accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
if err = db.Insert(ctx, accesses); err != nil {
return fmt.Errorf("insert new user accesses: %w", err)
@@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
if err := team.LoadRepositories(ctx); err != nil {
log.Error("team.LoadRepositories failed: %v", err)
}
+
// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
go func(repos []*repo_model.Repository) {
for _, repo := range repos {
- if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
log.Error("watch repo failed: %v", err)
}
}
@@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
return nil
}
-func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
e := db.GetEngine(ctx)
- isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+ isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
if err != nil || !isMember {
return err
}
// Check if the user to delete is the last member in owner team.
if team.IsOwnerTeam() && team.NumMembers == 1 {
- return organization.ErrLastOrgOwner{UID: userID}
+ return organization.ErrLastOrgOwner{UID: user.ID}
}
team.NumMembers--
@@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
}
if _, err := e.Delete(&organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: team.OrgID,
TeamID: team.ID,
}); err != nil {
@@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
// Delete access to team repositories.
for _, repo := range team.Repos {
- if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
+ if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
return err
}
// Remove watches from now unaccessible
- if err := ReconsiderWatches(ctx, repo, userID); err != nil {
+ if err := ReconsiderWatches(ctx, repo, user); err != nil {
return err
}
// Remove issue assignments from now unaccessible
- if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
+ if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
return err
}
}
- return removeInvalidOrgUser(ctx, userID, team.OrgID)
+ return removeInvalidOrgUser(ctx, team.OrgID, user)
}
-func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
+func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
// Check if the user is a member of any team in the organization.
if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
- UID: userID,
+ UID: user.ID,
OrgID: orgID,
}); err != nil {
return err
} else if count == 0 {
- return RemoveOrgUser(ctx, orgID, userID)
+ org, err := organization.GetOrgByID(ctx, orgID)
+ if err != nil {
+ return err
+ }
+
+ return RemoveOrgUser(ctx, org, user)
}
return nil
}
// RemoveTeamMember removes member from given team of given organization.
-func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- if err := removeTeamMember(ctx, team, userID); err != nil {
+ if err := removeTeamMember(ctx, team, user); err != nil {
return err
}
return committer.Commit()
}
-func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
- user, err := user_model.GetUserByID(ctx, uid)
- if err != nil {
- return err
- }
-
+func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
return err
}
- if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
+ if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
Delete(&issues_model.IssueAssignees{}); err != nil {
- return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
+ return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
}
return nil
}
-func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
- if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
+func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+ if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
return err
}
- if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+ if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
- return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
+ return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
}
diff --git a/models/org_team_test.go b/models/org_team_test.go
index e4b7b917e8..cf2c8be536 100644
--- a/models/org_team_test.go
+++ b/models/org_team_test.go
@@ -21,33 +21,42 @@ import (
func TestTeam_AddMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- test := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
- unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
}
- test(1, 2)
- test(1, 4)
- test(3, 2)
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
}
func TestTeam_RemoveMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
- unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
}
- testSuccess(1, 4)
- testSuccess(2, 2)
- testSuccess(3, 2)
- testSuccess(3, unittest.NonexistentID)
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
- err := RemoveTeamMember(db.DefaultContext, team, 2)
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
assert.True(t, organization.IsErrLastOrgOwner(err))
}
@@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
func TestAddTeamMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- test := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
- unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
}
- test(1, 2)
- test(1, 4)
- test(3, 2)
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
}
func TestRemoveTeamMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(teamID, userID int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
- unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
- unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
}
- testSuccess(1, 4)
- testSuccess(2, 2)
- testSuccess(3, 2)
- testSuccess(3, unittest.NonexistentID)
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
- err := RemoveTeamMember(db.DefaultContext, team, 2)
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
assert.True(t, organization.IsErrLastOrgOwner(err))
}
@@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
- has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+ has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
assert.NoError(t, err)
assert.False(t, has)
// adding user29 to team5 should add an explicit access row for repo 23
// even though repo 23 is public
- assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
- has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+ has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
assert.NoError(t, err)
assert.True(t, has)
}
diff --git a/models/org_test.go b/models/org_test.go
index d10a1dc218..247530406d 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -16,22 +16,27 @@ import (
func TestUser_RemoveMember(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
// remove a user that is a member
- unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
prevNumMembers := org.NumMembers
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
- org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.Equal(t, prevNumMembers-1, org.NumMembers)
// remove a user that is not a member
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
prevNumMembers = org.NumMembers
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
- org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.Equal(t, prevNumMembers, org.NumMembers)
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
@@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
func TestRemoveOrgUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(orgID, userID int64) {
- org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+
+ testSuccess := func(org *organization.Organization, user *user_model.User) {
expectedNumMembers := org.NumMembers
- if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
+ if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
expectedNumMembers--
}
- assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
- unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
- org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
assert.EqualValues(t, expectedNumMembers, org.NumMembers)
}
- testSuccess(3, 4)
- testSuccess(3, 4)
- err := RemoveOrgUser(db.DefaultContext, 7, 5)
+ org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ testSuccess(org3, user4)
+
+ org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ testSuccess(org3, user4)
+
+ err := RemoveOrgUser(db.DefaultContext, org7, user5)
assert.Error(t, err)
assert.True(t, organization.IsErrLastOrgOwner(err))
- unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
}
diff --git a/models/organization/org.go b/models/organization/org.go
index b4919defb4..a3082e9ac7 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
&TeamUnit{OrgID: org.ID},
&TeamInvite{OrgID: org.ID},
&secret_model.Secret{OwnerID: org.ID},
+ &user_model.Blocking{BlockerID: org.ID},
); err != nil {
return fmt.Errorf("DeleteBeans: %w", err)
}
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index ab767db200..d6d0a5054d 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
Exist()
}
-// GetTeamUsersByTeamID returns team users for a team
-func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
- teamUsers := make([]*TeamUser, 0, 10)
- return teamUsers, db.GetEngine(ctx).
- Where("team_id=?", teamID).
- Find(&teamUsers)
-}
-
// SearchMembersOptions holds the search options
type SearchMembersOptions struct {
db.ListOptions
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index 3e2568b4b4..b422a08614 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
- collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
if err != nil {
- return fmt.Errorf("getCollaborations: %w", err)
+ return fmt.Errorf("GetCollaborators: %w", err)
}
for _, c := range collaborators {
if c.User.IsGhost() {
diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go
index 7288082614..272c6ac05b 100644
--- a/models/repo/collaboration.go
+++ b/models/repo/collaboration.go
@@ -36,14 +36,44 @@ type Collaborator struct {
Collaboration *Collaboration
}
+type FindCollaborationOptions struct {
+ db.ListOptions
+ RepoID int64
+ RepoOwnerID int64
+ CollaboratorID int64
+}
+
+func (opts *FindCollaborationOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
+ }
+ if opts.CollaboratorID != 0 {
+ cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
+ }
+ return cond
+}
+
+func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
+ if opts.RepoOwnerID != 0 {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
+ return nil
+ },
+ }
+ }
+ return nil
+}
+
// GetCollaborators returns the collaborators for a repository
-func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
- collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
- ListOptions: listOptions,
- RepoID: repoID,
- })
+func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
+ collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
if err != nil {
- return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
+ return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
}
collaborators := make([]*Collaborator, 0, len(collaborations))
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
usersMap := make(map[int64]*user_model.User)
if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
- return nil, fmt.Errorf("Find users map by user ids: %w", err)
+ return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
}
for _, c := range collaborations {
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
Collaboration: c,
})
}
- return collaborators, nil
+ return collaborators, total, nil
}
// GetCollaboration get collaboration for a repository id with a user id
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
}
-type FindCollaborationOptions struct {
- db.ListOptions
- RepoID int64
-}
-
-func (opts FindCollaborationOptions) ToConds() builder.Cond {
- return builder.And(builder.Eq{"repo_id": opts.RepoID})
-}
-
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
// Discard invalid input
diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go
index 21a99dd557..639050f5fd 100644
--- a/models/repo/collaboration_test.go
+++ b/models/repo/collaboration_test.go
@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
- collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
assert.NoError(t, err)
expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
assert.NoError(t, err)
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
// Test db.ListOptions
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
- collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
+ collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{PageSize: 1, Page: 1},
+ RepoID: repo.ID,
+ })
assert.NoError(t, err)
assert.Len(t, collaborators1, 1)
- collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
+ collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{PageSize: 1, Page: 2},
+ RepoID: repo.ID,
+ })
assert.NoError(t, err)
assert.Len(t, collaborators2, 1)
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
-func TestRepository_CountCollaborators(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: repo1.ID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 2, count)
-
- repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
- count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: repo2.ID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 2, count)
-
- // Non-existent repository.
- count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
- RepoID: unittest.NonexistentID,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 0, count)
-}
-
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index 1a870224bf..c13b698abf 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
func TestWatchRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const repoID = 3
- const userID = 2
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestMetas(t *testing.T) {
diff --git a/models/repo/star.go b/models/repo/star.go
index 60737149da..4c66855525 100644
--- a/models/repo/star.go
+++ b/models/repo/star.go
@@ -24,26 +24,30 @@ func init() {
}
// StarRepo or unstar repository.
-func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
+func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- staring := IsStaring(ctx, userID, repoID)
+ staring := IsStaring(ctx, doer.ID, repo.ID)
if star {
+ if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+ return user_model.ErrBlockedUser
+ }
+
if staring {
return nil
}
- if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+ if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
return err
}
} else {
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
return nil
}
- if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+ if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
return err
}
- if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
+ if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
return err
}
}
diff --git a/models/repo/star_test.go b/models/repo/star_test.go
index 62eac4e29a..aaac89d975 100644
--- a/models/repo/star_test.go
+++ b/models/repo/star_test.go
@@ -9,21 +9,24 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestStarRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const userID = 2
- const repoID = 1
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
}
func TestIsStaring(t *testing.T) {
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
func TestClearRepoStars(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- const userID = 2
- const repoID = 1
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
- assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
- unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+ assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+
gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Len(t, gazers, 0)
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index 30c9db7474..6862247657 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -16,47 +16,82 @@ import (
"xorm.io/builder"
)
-// GetStarredRepos returns the repos starred by a particular user
-func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
- sess := db.GetEngine(ctx).
- Where("star.uid=?", userID).
- Join("LEFT", "star", "`repository`.id=`star`.repo_id")
- if !private {
- sess = sess.And("is_private=?", false)
- }
+type StarredReposOptions struct {
+ db.ListOptions
+ StarrerID int64
+ RepoOwnerID int64
+ IncludePrivate bool
+}
- if listOptions.Page != 0 {
- sess = db.SetSessionPagination(sess, &listOptions)
+func (opts *StarredReposOptions) ToConds() builder.Cond {
+ var cond builder.Cond = builder.Eq{
+ "star.uid": opts.StarrerID,
+ }
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{
+ "repository.owner_id": opts.RepoOwnerID,
+ })
+ }
+ if !opts.IncludePrivate {
+ cond = cond.And(builder.Eq{
+ "repository.is_private": false,
+ })
+ }
+ return cond
+}
- repos := make([]*Repository, 0, listOptions.PageSize)
- return repos, sess.Find(&repos)
+func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
+ return nil
+ },
}
+}
+
+// GetStarredRepos returns the repos starred by a particular user
+func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
+ return db.Find[Repository](ctx, opts)
+}
- repos := make([]*Repository, 0, 10)
- return repos, sess.Find(&repos)
+type WatchedReposOptions struct {
+ db.ListOptions
+ WatcherID int64
+ RepoOwnerID int64
+ IncludePrivate bool
}
-// GetWatchedRepos returns the repos watched by a particular user
-func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
- sess := db.GetEngine(ctx).
- Where("watch.user_id=?", userID).
- And("`watch`.mode<>?", WatchModeDont).
- Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
- if !private {
- sess = sess.And("is_private=?", false)
+func (opts *WatchedReposOptions) ToConds() builder.Cond {
+ var cond builder.Cond = builder.Eq{
+ "watch.user_id": opts.WatcherID,
}
+ if opts.RepoOwnerID != 0 {
+ cond = cond.And(builder.Eq{
+ "repository.owner_id": opts.RepoOwnerID,
+ })
+ }
+ if !opts.IncludePrivate {
+ cond = cond.And(builder.Eq{
+ "repository.is_private": false,
+ })
+ }
+ return cond.And(builder.Neq{
+ "watch.mode": WatchModeDont,
+ })
+}
- if listOptions.Page != 0 {
- sess = db.SetSessionPagination(sess, &listOptions)
-
- repos := make([]*Repository, 0, listOptions.PageSize)
- total, err := sess.FindAndCount(&repos)
- return repos, total, err
+func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
+ return nil
+ },
}
+}
- repos := make([]*Repository, 0, 10)
- total, err := sess.FindAndCount(&repos)
- return repos, total, err
+// GetWatchedRepos returns the repos watched by a particular user
+func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
+ return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index 7816b0262a..591dcea5b5 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err)
- assert.Len(t, users, 3)
- assert.Equal(t, users[0].ID, int64(15))
- assert.Equal(t, users[1].ID, int64(18))
- assert.Equal(t, users[2].ID, int64(16))
+ assert.Len(t, users, 4)
+ assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
func TestRepoGetReviewers(t *testing.T) {
diff --git a/models/repo/watch.go b/models/repo/watch.go
index 80da4030cb..a616544cae 100644
--- a/models/repo/watch.go
+++ b/models/repo/watch.go
@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
return err
}
-// WatchRepoMode watch repository in specific mode.
-func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
- var watch Watch
- if watch, err = GetWatch(ctx, userID, repoID); err != nil {
- return err
- }
- return watchRepoMode(ctx, watch, mode)
-}
-
// WatchRepo watch or unwatch repository.
-func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
- var watch Watch
- if watch, err = GetWatch(ctx, userID, repoID); err != nil {
+func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
+ watch, err := GetWatch(ctx, doer.ID, repo.ID)
+ if err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
- err = watchRepoMode(ctx, watch, WatchModeDont)
+ return watchRepoMode(ctx, watch, WatchModeDont)
} else if !doWatch {
- err = watchRepoMode(ctx, watch, WatchModeNone)
- } else {
- err = watchRepoMode(ctx, watch, WatchModeNormal)
+ return watchRepoMode(ctx, watch, WatchModeNone)
}
- return err
+
+ if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+ return user_model.ErrBlockedUser
+ }
+
+ return watchRepoMode(ctx, watch, WatchModeNormal)
}
// GetWatchers returns all watchers of given repository.
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index 7aa899291c..a95a267961 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
+
watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
+ assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
-
-func TestWatchRepoMode(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
-
- assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
- unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-}
diff --git a/models/repo_transfer.go b/models/repo_transfer.go
index 676e2dbb63..747ec2f248 100644
--- a/models/repo_transfer.go
+++ b/models/repo_transfer.go
@@ -13,6 +13,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
)
// RepoTransfer is used to manage repository transfers
@@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
return allowed
}
+type PendingRepositoryTransferOptions struct {
+ RepoID int64
+ SenderID int64
+ RecipientID int64
+}
+
+func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+ }
+ if opts.SenderID != 0 {
+ cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
+ }
+ if opts.RecipientID != 0 {
+ cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
+ }
+ return cond
+}
+
+func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
+ transfers := make([]*RepoTransfer, 0, 10)
+ return transfers, db.GetEngine(ctx).
+ Where(opts.ToConds()).
+ Find(&transfers)
+}
+
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
- transfer := new(RepoTransfer)
-
- has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
+ transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
if err != nil {
return nil, err
}
- if !has {
+ if len(transfers) != 1 {
return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
}
- return transfer, nil
+ return transfers[0], nil
}
func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
diff --git a/models/user/block.go b/models/user/block.go
new file mode 100644
index 0000000000..5f2b65a199
--- /dev/null
+++ b/models/user/block.go
@@ -0,0 +1,123 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+var (
+ ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
+ ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
+ ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
+ ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
+)
+
+type Blocking struct {
+ ID int64 `xorm:"pk autoincr"`
+ BlockerID int64 `xorm:"UNIQUE(block)"`
+ Blocker *User `xorm:"-"`
+ BlockeeID int64 `xorm:"UNIQUE(block)"`
+ Blockee *User `xorm:"-"`
+ Note string
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+ return "user_blocking"
+}
+
+func init() {
+ db.RegisterModel(new(Blocking))
+}
+
+func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
+ _, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
+ return err
+}
+
+func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
+ if len(blockerIDs) == 0 {
+ return false
+ }
+
+ if blockee.IsAdmin {
+ return false
+ }
+
+ cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
+ And(builder.In("user_blocking.blocker_id", blockerIDs))
+
+ has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
+ return has
+}
+
+type FindBlockingOptions struct {
+ db.ListOptions
+ BlockerID int64
+ BlockeeID int64
+}
+
+func (opts *FindBlockingOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.BlockerID != 0 {
+ cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
+ }
+ if opts.BlockeeID != 0 {
+ cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
+ }
+ return cond
+}
+
+func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
+ return db.FindAndCount[Blocking](ctx, opts)
+}
+
+func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
+ blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
+ BlockerID: blockerID,
+ BlockeeID: blockeeID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(blocks) == 0 {
+ return nil, nil
+ }
+ return blocks[0], nil
+}
+
+type BlockingList []*Blocking
+
+func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
+ ids := make(container.Set[int64], len(blocks)*2)
+ for _, b := range blocks {
+ ids.Add(b.BlockerID)
+ ids.Add(b.BlockeeID)
+ }
+
+ userList, err := GetUsersByIDs(ctx, ids.Values())
+ if err != nil {
+ return err
+ }
+
+ userMap := make(map[int64]*User, len(userList))
+ for _, u := range userList {
+ userMap[u.ID] = u
+ }
+
+ for _, b := range blocks {
+ b.Blocker = userMap[b.BlockerID]
+ b.Blockee = userMap[b.BlockeeID]
+ }
+
+ return nil
+}
diff --git a/models/user/follow.go b/models/user/follow.go
index f4dd2891ff..cf9672109a 100644
--- a/models/user/follow.go
+++ b/models/user/follow.go
@@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
}
// FollowUser marks someone be another's follower.
-func FollowUser(ctx context.Context, userID, followID int64) (err error) {
- if userID == followID || IsFollowing(ctx, userID, followID) {
+func FollowUser(ctx context.Context, user, follow *User) (err error) {
+ if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
return nil
}
+ if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
+ return ErrBlockedUser
+ }
+
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
- if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
+ if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
return err
}
- if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
+ if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
return err
}
- if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
+ if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
return err
}
return committer.Commit()
diff --git a/models/user/user.go b/models/user/user.go
index a898e71a2d..2e1d6af176 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -1167,7 +1167,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
return false
}
- // If they follow - they see each over
+ // If they follow - they see each other
follower := IsFollowing(ctx, u.ID, viewer.ID)
if follower {
return true
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f522f743d5..f4efd071ea 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
func TestFollowUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(followerID, followedID int64) {
- assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
- unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
+ testSuccess := func(follower, followed *user_model.User) {
+ assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
+ unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
}
- testSuccess(4, 2)
- testSuccess(5, 2)
- assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ testSuccess(user4, user2)
+ testSuccess(user5, user2)
+
+ assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
unittest.CheckConsistencyFor(t, &user_model.User{})
}
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
index ebe14e3a4c..f71c58fbdf 100644
--- a/modules/repository/collaborator.go
+++ b/modules/repository/collaborator.go
@@ -16,6 +16,14 @@ import (
)
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+ return user_model.ErrBlockedUser
+ }
+
return db.WithTx(ctx, func(ctx context.Context) error {
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
"repo_id": repo.ID,
diff --git a/modules/repository/create.go b/modules/repository/create.go
index f009c0880d..4f18b9b3fa 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -153,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
}
if setting.Service.AutoWatchNewRepos {
- if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
return fmt.Errorf("WatchRepo: %w", err)
}
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c8c8f2dfeb..255fed28ad 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -632,6 +632,30 @@ form.name_reserved = The username "%s" is reserved.
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
form.name_chars_not_allowed = User name "%s" contains invalid characters.
+block.block = Block
+block.block.user = Block user
+block.block.org = Block user for organization
+block.block.failure = Failed to block user: %s
+block.unblock = Unblock
+block.unblock.failure = Failed to unblock user: %s
+block.blocked = You have blocked this user.
+block.title = Block a user
+block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
+block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
+block.info_2 = following your account
+block.info_3 = send you notifications by @mentioning your username
+block.info_4 = inviting you as a collaborator to their repositories
+block.info_5 = starring, forking or watching on repositories
+block.info_6 = opening and commenting on issues or pull requests
+block.info_7 = reacting on your comments in issues or pull requests
+block.user_to_block = User to block
+block.note = Note
+block.note.title = Optional note:
+block.note.info = The note is not visible to the blocked user.
+block.note.edit = Edit note
+block.list = Blocked users
+block.list.none = You have not blocked any users.
+
[settings]
profile = Profile
account = Account
@@ -969,6 +993,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
fork_branch = Branch to be cloned to the fork
all_branches = All branches
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
+fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
use_template = Use this template
open_with_editor = Open with %s
download_zip = Download ZIP
@@ -1144,6 +1169,7 @@ watch = Watch
unstar = Unstar
star = Star
fork = Fork
+action.blocked_user = Cannot perform action because you are blocked by the repository owner.
download_archive = Download Repository
more_operations = More Operations
@@ -1394,6 +1420,8 @@ issues.new.assignees = Assignees
issues.new.clear_assignees = Clear assignees
issues.new.no_assignees = No Assignees
issues.new.no_reviewers = No reviewers
+issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
+issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
issues.choose.get_started = Get Started
issues.choose.open_external_link = Open
issues.choose.blank = Default
@@ -1509,6 +1537,7 @@ issues.close_comment_issue = Comment and Close
issues.reopen_issue = Reopen
issues.reopen_comment_issue = Comment and Reopen
issues.create_comment = Comment
+issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -1707,6 +1736,7 @@ compare.compare_head = compare
pulls.desc = Enable pull requests and code reviews.
pulls.new = New Pull Request
+pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
pulls.view = View Pull Request
pulls.compare_changes = New Pull Request
pulls.allow_edits_from_maintainers = Allow edits from maintainers
@@ -2120,6 +2150,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
settings.transfer = Transfer Ownership
settings.transfer.rejected = Repository transfer was rejected.
settings.transfer.success = Repository transfer was successful.
+settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
settings.transfer_abort = Cancel transfer
settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
@@ -2165,6 +2196,7 @@ settings.add_collaborator_success = The collaborator has been added.
settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
settings.add_collaborator_owner = Cannot add an owner as a collaborator.
settings.add_collaborator_duplicate = The collaborator is already added to this repository.
+settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa.
settings.delete_collaborator = Remove
settings.collaborator_deletion = Remove Collaborator
settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
@@ -2731,6 +2763,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
teams.add_duplicate_users = User is already a team member.
teams.repos.none = No repositories could be accessed by this team.
teams.members.none = No members on this team.
+teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
teams.specific_repositories = Specific repositories
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
teams.all_repositories = All repositories
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 1587d413f5..c65650c388 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1027,7 +1027,16 @@ func Routes() *web.Route {
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
- }, reqToken())
+ })
+
+ m.Group("/blocks", func() {
+ m.Get("", user.ListBlocks)
+ m.Group("/{username}", func() {
+ m.Get("", user.CheckUserBlock)
+ m.Put("", user.BlockUser)
+ m.Delete("", user.UnblockUser)
+ }, context.UserAssignmentAPI())
+ })
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
@@ -1477,6 +1486,15 @@ func Routes() *web.Route {
m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
+
+ m.Group("/blocks", func() {
+ m.Get("", org.ListBlocks)
+ m.Group("/{username}", func() {
+ m.Get("", org.CheckUserBlock)
+ m.Put("", org.BlockUser)
+ m.Delete("", org.UnblockUser)
+ })
+ }, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go
new file mode 100644
index 0000000000..69a5222a20
--- /dev/null
+++ b/routers/api/v1/org/block.go
@@ -0,0 +1,116 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
+ // ---
+ // summary: List users blocked by the organization
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/UserList"
+
+ shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
+ // ---
+ // summary: Check if a user is blocked by the organization
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to check
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
+}
+
+func BlockUser(ctx *context.APIContext) {
+ // swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
+ // ---
+ // summary: Block a user
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to block
+ // type: string
+ // required: true
+ // - name: note
+ // in: query
+ // description: optional note for the block
+ // type: string
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
+}
+
+func UnblockUser(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
+ // ---
+ // summary: Unblock a user
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to unblock
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
+}
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index fb66d4c3f5..9db9ad964b 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
+ if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index b62a386fd7..015af774e3 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -15,6 +15,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
- ctx.Error(http.StatusInternalServerError, "AddMember", err)
+ if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "AddTeamMember", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
return
}
- if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
+ if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
return
}
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 7d48d71516..4ce14f7d01 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -8,7 +8,6 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
@@ -54,16 +53,11 @@ func ListCollaborators(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
- RepoID: ctx.Repo.Repository.ID,
+ collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
- ctx.InternalServerError(err)
- return
- }
-
- collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
- if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
}
@@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
}
- ctx.SetTotalCountHeader(count)
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, users)
}
@@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
@@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
}
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
- ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "AddCollaborator", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+ }
return
}
@@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
return
}
- if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
+ if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
return
}
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 212cc7a93b..a1e3c9804b 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.Error(http.StatusConflict, "ForkRepository", err)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "ForkRepository", err)
} else {
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
}
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 1b2ecd474b..d43711e362 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -5,6 +5,7 @@
package repo
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) {
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "NewIssue", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "NewIssue", err)
}
- ctx.Error(http.StatusInternalServerError, "NewIssue", err)
return
}
@@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+ }
return
}
}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 6209e960af..21aabadf3d 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+ }
return
}
@@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form)
}
@@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
oldContent := comment.Content
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateComment", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+ }
return
}
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index e7436db798..4096cbf07b 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -4,10 +4,12 @@
package repo
import (
+ "errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
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"
api "code.gitea.io/gitea/modules/structs"
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "423":
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
- ctx.ServerError("UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateComment", err)
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
return
}
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index 799c687812..3ff3d19f13 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,11 +8,13 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
+ issue_service "code.gitea.io/gitea/services/issue"
)
// GetIssueCommentReactions list reactions of a comment from an issue
@@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
if isCreateType {
// PostIssueCommentReaction part
- reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
+ reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
if isCreateType {
// PostIssueReaction part
- reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
+ reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
Created: reaction.CreatedUnix.AsTime(),
})
} else {
- ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
+ ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
}
return
}
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 8f9848f71d..4cb94b11a2 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) {
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
@@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) {
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
}
- ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
return
}
@@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
}
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index 4f05c0df51..776b336761 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -4,6 +4,7 @@
package repo
import (
+ "errors"
"fmt"
"net/http"
@@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
return
}
- ctx.InternalServerError(err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.InternalServerError(err)
+ }
return
}
diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go
new file mode 100644
index 0000000000..a1e65625ed
--- /dev/null
+++ b/routers/api/v1/shared/block.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package shared
+
+import (
+ "errors"
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
+ blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ BlockerID: blocker.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
+ return
+ }
+
+ if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+ return
+ }
+
+ users := make([]*api.User, 0, len(blocks))
+ for _, b := range blocks {
+ users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
+ }
+
+ ctx.SetTotalCountHeader(total)
+ ctx.JSON(http.StatusOK, &users)
+}
+
+func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ status := http.StatusNotFound
+ blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
+ return
+ }
+ if blocking != nil {
+ status = http.StatusNoContent
+ }
+
+ ctx.Status(status)
+}
+
+func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
+ if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Error(http.StatusBadRequest, "BlockUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "BlockUser", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
+ if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Error(http.StatusBadRequest, "UnblockUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go
new file mode 100644
index 0000000000..7231e9add7
--- /dev/null
+++ b/routers/api/v1/user/block.go
@@ -0,0 +1,96 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+ // swagger:operation GET /user/blocks user userListBlocks
+ // ---
+ // summary: List users blocked by the authenticated user
+ // parameters:
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/UserList"
+
+ shared.ListBlocks(ctx, ctx.Doer)
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+ // swagger:operation GET /user/blocks/{username} user userCheckUserBlock
+ // ---
+ // summary: Check if a user is blocked by the authenticated user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to check
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ shared.CheckUserBlock(ctx, ctx.Doer)
+}
+
+func BlockUser(ctx *context.APIContext) {
+ // swagger:operation PUT /user/blocks/{username} user userBlockUser
+ // ---
+ // summary: Block a user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to block
+ // type: string
+ // required: true
+ // - name: note
+ // in: query
+ // description: optional note for the block
+ // type: string
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.BlockUser(ctx, ctx.Doer)
+}
+
+func UnblockUser(ctx *context.APIContext) {
+ // swagger:operation DELETE /user/blocks/{username} user userUnblockUser
+ // ---
+ // summary: Unblock a user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to unblock
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
+}
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 398c6b2567..6abb70de19 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -5,6 +5,7 @@
package user
import (
+ "errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
@@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
- ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+ if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "FollowUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index e624884db3..ad9ed9548d 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -5,10 +5,9 @@
package user
import (
- std_context "context"
+ "errors"
"net/http"
- "code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -20,8 +19,12 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has
// starred
-func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
- starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
+func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
+ starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ StarrerID: user.ID,
+ IncludePrivate: private,
+ })
if err != nil {
return nil, err
}
@@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
return
@@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.Doer, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
}
@@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
@@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 706f4cc66b..2cc23ae476 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -4,10 +4,9 @@
package user
import (
- std_context "context"
+ "errors"
"net/http"
- "code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -18,8 +17,12 @@ import (
)
// getWatchedRepos returns the repos that the user with the specified userID is watching
-func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
- watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
+func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
+ watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ WatcherID: user.ID,
+ IncludePrivate: private,
+ })
if err != nil {
return nil, 0, err
}
@@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/WatchInfo"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+ }
return
}
ctx.JSON(http.StatusOK, api.WatchInfo{
@@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
return
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
new file mode 100644
index 0000000000..d40458e250
--- /dev/null
+++ b/routers/web/org/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("user.block.list")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ shared_user.BlockedUsers(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+ shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
+}
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 9a3d60e122..63ac57cf0d 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
// MembersAction response for operation to a member of organization
func MembersAction(ctx *context.Context) {
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ }
+ if member == nil {
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
org := ctx.Org.Organization
- var err error
+
switch ctx.Params(":action") {
case "private":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
case "public":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = models.RemoveOrgUser(ctx, org.ID, uid)
+ err = models.RemoveOrgUser(ctx, org, member)
if organization.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
return
}
case "leave":
- err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+ err = models.RemoveOrgUser(ctx, org, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index fd7486cacd..144d9b1b43 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -5,6 +5,7 @@
package org
import (
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
- err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
case "leave":
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
return
}
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if user == nil {
ctx.Redirect(ctx.Org.OrgLink + "/teams")
return
}
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
if ctx.Org.Team.IsMember(ctx, u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
- err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, u)
}
page = "team"
@@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
} else {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]any{
@@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
return
}
- if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
+ if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
ctx.ServerError("AddTeamMember", err)
return
}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index b8c7f70aa6..45fd01f4da 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -57,6 +57,7 @@ import (
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -1258,9 +1259,11 @@ func NewIssuePost(ctx *context.Context) {
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
+ } else {
+ ctx.ServerError("NewIssue", err)
}
- ctx.ServerError("NewIssue", err)
return
}
@@ -2047,6 +2050,10 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
ctx.HTML(http.StatusOK, tplIssueView)
}
@@ -2250,7 +2257,11 @@ func UpdateIssueContent(ctx *context.Context) {
}
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
- ctx.ServerError("ChangeContent", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+ } else {
+ ctx.ServerError("ChangeContent", err)
+ }
return
}
@@ -3108,7 +3119,11 @@ func NewComment(ctx *context.Context) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
- ctx.ServerError("CreateIssueComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("CreateIssueComment", err)
+ }
return
}
@@ -3152,7 +3167,11 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
- ctx.ServerError("UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
return
}
@@ -3260,9 +3279,9 @@ func ChangeIssueReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
+ reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
@@ -3367,9 +3386,9 @@ func ChangeCommentReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
+ reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index bf52d76e95..ed063715e5 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -47,6 +47,7 @@ import (
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/gobwas/glob"
)
@@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
case db.IsErrNamePatternNotAllowed(err):
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ case errors.Is(err, user_model.ErrBlockedUser):
+ ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
default:
ctx.ServerError("ForkPost", err)
}
@@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
upload.AddUploadContext(ctx, "comment")
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
ctx.HTML(http.StatusOK, tplPullFiles)
}
@@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
} else if git.IsErrPushRejected(err) {
pushrejErr := err.(*git.ErrPushRejected)
message := pushrejErr.Message
@@ -1501,9 +1507,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
ctx.JSONError(flashError)
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": ctx.Tr("repo.pulls.push_rejected"),
+ "Summary": ctx.Tr("repo.pulls.new.blocked_user"),
+ })
+ if err != nil {
+ ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+ return
+ }
+ ctx.JSONError(flashError)
}
- ctx.ServerError("NewPullRequest", err)
return
}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 49779efa37..f0caf199a2 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -313,13 +313,13 @@ func Action(ctx *context.Context) {
var err error
switch ctx.Params(":action") {
case "watch":
- err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unwatch":
- err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "star":
- err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unstar":
- err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true)
case "reject_transfer":
@@ -336,8 +336,12 @@ func Action(ctx *context.Context) {
}
if err != nil {
- ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
- return
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
+ } else {
+ ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+ return
+ }
}
switch ctx.Params(":action") {
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index 6bfd485566..31f9f76d0f 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -4,10 +4,10 @@
package setting
import (
+ "errors"
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
@@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
ctx.Data["PageIsSettingsCollaboration"] = true
- users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
+ users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("GetCollaborators", err)
return
@@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
}
if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
- ctx.ServerError("AddCollaborator", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ } else {
+ ctx.ServerError("AddCollaborator", err)
+ }
return
}
@@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
// DeleteCollaboration delete a collaboration for a repository
func DeleteCollaboration(ctx *context.Context) {
- if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
- ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+ if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+ } else {
+ ctx.ServerError("GetUserByName", err)
+ return
+ }
} else {
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+ if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
+ ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+ }
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 3af0ddb578..992a980d9e 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -5,6 +5,7 @@
package setting
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -782,6 +783,8 @@ func SettingsPost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
} else if models.IsErrRepoTransferInProgress(err) {
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
} else {
ctx.ServerError("TransferOwnership", err)
}
diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go
new file mode 100644
index 0000000000..8a2357623f
--- /dev/null
+++ b/routers/web/shared/user/block.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "errors"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
+ blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+ BlockerID: blocker.ID,
+ })
+ if err != nil {
+ ctx.ServerError("FindBlockings", err)
+ return
+ }
+ if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return
+ }
+ ctx.Data["UserBlocks"] = blocks
+}
+
+func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
+ form := web.GetForm(ctx).(*forms.BlockUserForm)
+ if ctx.HasError() {
+ ctx.ServerError("FormValidation", nil)
+ return
+ }
+
+ blockee, err := user_model.GetUserByName(ctx, form.Blockee)
+ if err != nil {
+ ctx.ServerError("GetUserByName", nil)
+ return
+ }
+
+ switch form.Action {
+ case "block":
+ if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
+ if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
+ } else {
+ ctx.ServerError("BlockUser", err)
+ return
+ }
+ }
+ case "unblock":
+ if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
+ if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
+ } else {
+ ctx.ServerError("UnblockUser", err)
+ return
+ }
+ }
+ case "note":
+ block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ ctx.ServerError("GetBlocking", err)
+ return
+ }
+ if block != nil {
+ if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
+ ctx.ServerError("UpdateBlockingNote", err)
+ return
+ }
+ }
+ }
+}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 3bc1adae99..2d6d9ad98d 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -72,6 +72,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
if _, ok := ctx.Data["NumFollowing"]; !ok {
_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
}
+
+ if ctx.Doer != nil {
+ if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
+ ctx.ServerError("GetBlocking", err)
+ } else {
+ ctx.Data["UserBlocking"] = block
+ }
+ }
}
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 833312c501..9851ea90a6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -339,7 +339,7 @@ func Action(ctx *context.Context) {
var err error
switch ctx.FormString("action") {
case "follow":
- err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+ err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
case "unfollow":
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
}
diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go
new file mode 100644
index 0000000000..94fc380cee
--- /dev/null
+++ b/routers/web/user/setting/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/setting"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("user.block.list")
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ shared_user.BlockedUsers(ctx, ctx.Doer)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+ shared_user.BlockedUsersPost(ctx, ctx.Doer)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 14d31b3a90..8710f6e3e5 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) {
})
addWebhookEditRoutes()
}, webhooksEnabled)
+
+ m.Group("/blocked_users", func() {
+ m.Get("", user_setting.BlockedUsers)
+ m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
+ })
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
m.Group("/user", func() {
@@ -945,6 +950,11 @@ func registerRoutes(m *web.Route) {
m.Post("/rebuild", org.RebuildCargoIndex)
})
}, packagesEnabled)
+
+ m.Group("/blocked_users", func() {
+ m.Get("", org.BlockedUsers)
+ m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
+ })
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(true, true))
}, reqSignIn)
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 3a2411ec55..05293f202f 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
}
if action == syncAdd && !isMember {
- if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
+ if err := models.AddTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not add user to team: %v", err)
return err
}
} else if action == syncRemove && isMember {
- if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
+ if err := models.RemoveTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not remove user from team: %v", err)
return err
}
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 186aa4a878..416592bfda 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -449,3 +449,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
+
+type BlockUserForm struct {
+ Action string `binding:"Required;In(block,unblock,note)"`
+ Blockee string `binding:"Required"`
+ Note string
+}
+
+func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetValidateContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..d68623aff6 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
return fmt.Errorf("cannot create reference with empty commit SHA")
}
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
// Check if same reference from same commit has already existed.
has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
Type: issues_model.CommentTypeCommitRef,
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return nil, user_model.ErrBlockedUser
+ }
+ }
+
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeComment,
Doer: doer,
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
// UpdateComment updates information of comment.
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
+ if err := c.LoadIssue(ctx); err != nil {
+ return err
+ }
+ if err := c.Issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
if needsContentHistory {
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a03211..0a59088d12 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -5,6 +5,7 @@ package issue
import (
"context"
+ "errors"
"fmt"
"html"
"net/url"
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ continue
+ }
return err
}
diff --git a/services/issue/content.go b/services/issue/content.go
index 6e56714ddf..2f9bee806a 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -7,12 +7,23 @@ import (
"context"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
notify_service "code.gitea.io/gitea/services/notify"
)
// ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
oldContent := issue.Content
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..27a106009c 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/storage"
notify_service "code.gitea.io/gitea/services/notify"
@@ -22,6 +23,14 @@ import (
// NewIssue creates new issue with labels for repository.
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+ if err := issue.LoadPoster(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+ return user_model.ErrBlockedUser
+ }
+
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
return err
}
@@ -57,6 +66,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return nil
}
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
return err
}
@@ -93,31 +112,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
- var allNewAssignees []*user_model.User
+ uniqueAssignees := container.SetOf(multipleAssignees...)
// Keep the old assignee thingy for compatibility reasons
if oneAssignee != "" {
- // Prevent double adding assignees
- var isDouble bool
- for _, assignee := range multipleAssignees {
- if assignee == oneAssignee {
- isDouble = true
- break
- }
- }
-
- if !isDouble {
- multipleAssignees = append(multipleAssignees, oneAssignee)
- }
+ uniqueAssignees.Add(oneAssignee)
}
// Loop through all assignees to add them
- for _, assigneeName := range multipleAssignees {
+ allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
+ for _, assigneeName := range uniqueAssignees.Values() {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
return err
}
+ if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
+ return user_model.ErrBlockedUser
+ }
+
allNewAssignees = append(allNewAssignees, assignee)
}
diff --git a/services/issue/reaction.go b/services/issue/reaction.go
new file mode 100644
index 0000000000..deb99169e1
--- /dev/null
+++ b/services/issue/reaction.go
@@ -0,0 +1,50 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// CreateIssueReaction creates a reaction on an issue.
+func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: issue.ID,
+ })
+}
+
+// CreateCommentReaction creates a reaction on a comment.
+func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
+ if err := comment.LoadIssue(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := comment.Issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: comment.Issue.ID,
+ CommentID: comment.ID,
+ })
+}
diff --git a/models/issues/reaction_test.go b/services/issue/reaction_test.go
index 5dc8e1a5f3..7734860fc0 100644
--- a/models/issues/reaction_test.go
+++ b/services/issue/reaction_test.go
@@ -1,7 +1,7 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package issues_test
+package issue
import (
"testing"
@@ -16,13 +16,13 @@ import (
"github.com/stretchr/testify/assert"
)
-func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
+func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
var reaction *issues_model.Reaction
var err error
- if commentID == 0 {
- reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
+ if comment == nil {
+ reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
} else {
- reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
+ reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
}
assert.NoError(t, err)
assert.NotNil(t, reaction)
@@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
+ addReaction(t, user1, issue, nil, "heart")
- addReaction(t, user1.ID, issue1ID, 0, "heart")
-
- unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
}
func TestIssueAddDuplicateReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
-
- addReaction(t, user1.ID, issue1ID, 0, "heart")
+ addReaction(t, user1, issue, nil, "heart")
- reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
- DoerID: user1.ID,
- IssueID: issue1ID,
- Type: "heart",
- })
+ reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
assert.Error(t, err)
assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
- existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
assert.Equal(t, existingR.ID, reaction.ID)
}
@@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
+ addReaction(t, user1, issue, nil, "heart")
- addReaction(t, user1.ID, issue1ID, 0, "heart")
-
- err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart")
+ err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
assert.NoError(t, err)
- unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
}
func TestIssueReactionCount(t *testing.T) {
@@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) {
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
ghost := user_model.NewGhostUser()
- var issueID int64 = 2
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
- addReaction(t, user1.ID, issueID, 0, "heart")
- addReaction(t, user2.ID, issueID, 0, "heart")
- addReaction(t, org3.ID, issueID, 0, "heart")
- addReaction(t, org3.ID, issueID, 0, "+1")
- addReaction(t, user4.ID, issueID, 0, "+1")
- addReaction(t, user4.ID, issueID, 0, "heart")
- addReaction(t, ghost.ID, issueID, 0, "-1")
+ addReaction(t, user1, issue, nil, "heart")
+ addReaction(t, user2, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "heart")
+ addReaction(t, ghost, issue, nil, "-1")
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
- IssueID: issueID,
+ IssueID: issue.ID,
})
assert.NoError(t, err)
assert.Len(t, reactionsList, 7)
@@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
-
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
+ addReaction(t, user1, nil, comment, "heart")
- unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
}
func TestIssueCommentDeleteReaction(t *testing.T) {
@@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
- addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
- addReaction(t, org3.ID, issue1ID, comment1ID, "heart")
- addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
+ addReaction(t, user1, nil, comment, "heart")
+ addReaction(t, user2, nil, comment, "heart")
+ addReaction(t, org3, nil, comment, "heart")
+ addReaction(t, user4, nil, comment, "+1")
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
- IssueID: issue1ID,
- CommentID: comment1ID,
+ IssueID: comment.IssueID,
+ CommentID: comment.ID,
})
assert.NoError(t, err)
assert.Len(t, reactionsList, 4)
@@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
-
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
- assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart"))
+ addReaction(t, user1, nil, comment, "heart")
+ assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
- unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 42363f886d..be3d25d20a 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool()
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
+ if err := issue.LoadPoster(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+ return user_model.ErrBlockedUser
+ }
+
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
if !git_model.IsErrBranchNotExist(err) {
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index dccc124748..4a43ae2a28 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -11,13 +11,14 @@ import (
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
)
// DeleteCollaboration removes collaboration relation between the user and repository.
-func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
+func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
collaboration := &repo_model.Collaboration{
RepoID: repo.ID,
- UserID: uid,
+ UserID: collaborator.ID,
}
ctx, committer, err := db.TxContext(ctx)
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
} else if has == 0 {
return committer.Commit()
}
+
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
return err
}
- if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
return err
}
- if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
+ if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
return err
}
// Unassign a user from any issue (s)he has been assigned to in the repository
- if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
+ if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
return err
}
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index c3d006bfd8..a2eb06b81a 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
@@ -16,13 +17,15 @@ import (
func TestRepository_DeleteCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
+
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
- assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
- unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
+ assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+ unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
- assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
- unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
+ assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+ unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 08d6800ee7..1eeec27660 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -365,24 +365,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
}
}
- teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
+ teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
+ TeamID: t.ID,
+ })
if err != nil {
- return fmt.Errorf("getTeamUsersByTeamID: %w", err)
+ return fmt.Errorf("GetTeamMembers: %w", err)
}
- for _, teamUser := range teamUsers {
- has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
+ for _, member := range teamMembers {
+ has, err := access_model.HasAccess(ctx, member.ID, repo)
if err != nil {
return err
} else if has {
continue
}
- if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repositories
- if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
+ if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
return err
}
}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index f9c13a109e..f074fd1082 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -53,6 +53,14 @@ type ForkRepoOptions struct {
// ForkRepository forks a repository
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
+ if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
// Fork is prohibited, if user has reached maximum limit of repositories
if !owner.CanForkRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 59a4eb260e..83d3032188 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -139,9 +139,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
}
// Remove redundant collaborators.
- collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
if err != nil {
- return fmt.Errorf("getCollaborators: %w", err)
+ return fmt.Errorf("GetCollaborators: %w", err)
}
// Dummy object.
@@ -201,13 +201,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
return fmt.Errorf("decrease old owner repository count: %w", err)
}
- if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+ if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
return fmt.Errorf("watchRepo: %w", err)
}
// Remove watch for organization.
if oldOwner.IsOrganization() {
- if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
+ if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
return fmt.Errorf("watchRepo [false]: %w", err)
}
}
@@ -371,6 +371,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
return TransferOwnership(ctx, doer, newOwner, repo, teams)
}
+ if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
+ return user_model.ErrBlockedUser
+ }
+
// If new owner is an org and user can create repos he can transfer directly too
if newOwner.IsOrganization() {
allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
diff --git a/services/user/block.go b/services/user/block.go
new file mode 100644
index 0000000000..0b3b618aae
--- /dev/null
+++ b/services/user/block.go
@@ -0,0 +1,308 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ org_model "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+ if blocker.ID == blockee.ID {
+ return false
+ }
+ if doer.ID == blockee.ID {
+ return false
+ }
+
+ if blockee.IsOrganization() {
+ return false
+ }
+
+ if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+ return false
+ }
+
+ if blocker.IsOrganization() {
+ org := org_model.OrgFromUser(blocker)
+ if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
+ return false
+ }
+ if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+ return false
+ }
+ } else if !doer.IsAdmin && doer.ID != blocker.ID {
+ return false
+ }
+
+ return true
+}
+
+func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+ if doer.ID == blockee.ID {
+ return false
+ }
+
+ if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+ return false
+ }
+
+ if blocker.IsOrganization() {
+ org := org_model.OrgFromUser(blocker)
+ if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+ return false
+ }
+ } else if !doer.IsAdmin && doer.ID != blocker.ID {
+ return false
+ }
+
+ return true
+}
+
+func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
+ if blockee.IsOrganization() {
+ return user_model.ErrBlockOrganization
+ }
+
+ if !CanBlockUser(ctx, doer, blocker, blockee) {
+ return user_model.ErrCanNotBlock
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ // unfollow each other
+ if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
+ return err
+ }
+ if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
+ return err
+ }
+
+ // unstar each other
+ if err := unstarRepos(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unstarRepos(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // unwatch each others repositories
+ if err := unwatchRepos(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unwatchRepos(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // unassign each other from issues
+ if err := unassignIssues(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unassignIssues(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // remove each other from repository collaborations
+ if err := removeCollaborations(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := removeCollaborations(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // cancel each other repository transfers
+ if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ return db.Insert(ctx, &user_model.Blocking{
+ BlockerID: blocker.ID,
+ BlockeeID: blockee.ID,
+ Note: note,
+ })
+ })
+}
+
+func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
+ opts := &repo_model.StarredReposOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ StarrerID: starrer.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ repos, err := repo_model.GetStarredRepos(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
+ opts := &repo_model.WatchedReposOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ WatcherID: watcher.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
+ transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
+ SenderID: sender.ID,
+ RecipientID: recipient.ID,
+ })
+ if err != nil {
+ return err
+ }
+
+ for _, transfer := range transfers {
+ repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
+ if err != nil {
+ return err
+ }
+
+ if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
+ opts := &issues_model.AssignedIssuesOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ AssigneeID: assignee.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(issues) == 0 {
+ return nil
+ }
+
+ for _, issue := range issues {
+ if err := issue.LoadAssignees(ctx); err != nil {
+ return err
+ }
+
+ if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
+ opts := &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ CollaboratorID: collaborator.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(collaborations) == 0 {
+ return nil
+ }
+
+ for _, collaboration := range collaborations {
+ repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
+ if err != nil {
+ return err
+ }
+
+ if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
+ if blockee.IsOrganization() {
+ return user_model.ErrBlockOrganization
+ }
+
+ if !CanUnblockUser(ctx, doer, blocker, blockee) {
+ return user_model.ErrCanNotUnblock
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ return err
+ }
+ if block != nil {
+ _, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
+ return err
+ }
+ return nil
+ })
+}
diff --git a/services/user/block_test.go b/services/user/block_test.go
new file mode 100644
index 0000000000..aec3e03cf3
--- /dev/null
+++ b/services/user/block_test.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCanBlockUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+
+ // Doer can't self block
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
+ // Blocker can't be blockee
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
+ // Can't block already blocked user
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
+ // Blockee can't be an organization
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
+ // Doer must be blocker or admin
+ assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
+ // Organization can't block a member
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
+ // Doer must be organization owner or admin if blocker is an organization
+ assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
+
+ assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
+ assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
+ assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
+}
+
+func TestCanUnblockUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+ user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+ org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+ // Doer can't self unblock
+ assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
+ // Can't unblock not blocked user
+ assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
+ // Doer must be blocker or admin
+ assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
+ // Doer must be organization owner or admin if blocker is an organization
+ assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
+
+ assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
+ assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
+ assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
+}
diff --git a/services/user/delete.go b/services/user/delete.go
index 000910319a..212cb83e03 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -92,6 +92,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
&pull_model.ReviewState{UserID: u.ID},
&user_model.Redirect{RedirectUserID: u.ID},
&actions_model.ActionRunner{OwnerID: u.ID},
+ &user_model.Blocking{BlockerID: u.ID},
+ &user_model.Blocking{BlockeeID: u.ID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}
diff --git a/services/user/user.go b/services/user/user.go
index f2648db409..6604dba4d6 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
break
}
for _, org := range orgs {
- if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
+ if err := models.RemoveOrgUser(ctx, org, u); err != nil {
if organization.IsErrLastOrgOwner(err) {
err = org_service.DeleteOrganization(ctx, org, true)
if err != nil {
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 2ebcded925..f110bd26d0 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -41,7 +41,8 @@ func TestDeleteUser(t *testing.T) {
orgUsers := make([]*organization.OrgUser, 0, 10)
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
for _, orgUser := range orgUsers {
- if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
+ org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
+ if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
assert.True(t, organization.IsErrLastOrgOwner(err))
return
}
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..eab5ec0007
--- /dev/null
+++ b/templates/org/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}}
+<div class="org-setting-content">
+ {{template "shared/user/blocked_users" .}}
+</div>
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 64ae20f0a3..ce792f667c 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -17,6 +17,9 @@
{{ctx.Locale.Tr "settings.applications"}}
</a>
{{end}}
+ <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
+ {{ctx.Locale.Tr "user.block.list"}}
+ </a>
{{if .EnablePackages}}
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
{{ctx.Locale.Tr "packages.title"}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 39cf8755f2..1cb3aaaa21 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -251,5 +251,6 @@
{{end}}
{{if (not .DiffNotAvailable)}}
{{template "repo/issue/view_content/reference_issue_dialog" .}}
+ {{template "shared/user/block_user_dialog" .}}
{{end}}
</div>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 747132931e..edfa9c0bc5 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -170,6 +170,7 @@
</template>
{{template "repo/issue/view_content/reference_issue_dialog" .}}
+{{template "shared/user/block_user_dialog" .}}
<div class="gt-hidden" id="no-content">
<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl
index 4afd73c371..17556d4e48 100644
--- a/templates/repo/issue/view_content/context_menu.tmpl
+++ b/templates/repo/issue/view_content/context_menu.tmpl
@@ -10,16 +10,33 @@
{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
{{end}}
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
- {{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}}
- <div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
- {{if not .ctxData.UnitIssuesGlobalDisabled}}
- <div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+ {{if .ctxData.IsSigned}}
+ {{$needDivider := false}}
+ {{if not .ctxData.Repository.IsArchived}}
+ {{$needDivider = true}}
+ <div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
+ {{if not .ctxData.UnitIssuesGlobalDisabled}}
+ <div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+ {{end}}
+ {{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
+ <div class="divider"></div>
+ <div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
+ {{if .delete}}
+ <div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+ {{end}}
+ {{end}}
{{end}}
- {{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
- <div class="divider"></div>
- <div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
- {{if .delete}}
- <div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+ {{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
+ {{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
+ {{if or $canOrgBlock $canUserBlock}}
+ {{if $needDivider}}
+ <div class="divider"></div>
+ {{end}}
+ {{if $canUserBlock}}
+ <div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
+ {{end}}
+ {{if $canOrgBlock}}
+ <div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
{{end}}
{{end}}
{{end}}
diff --git a/templates/shared/user/block_user_dialog.tmpl b/templates/shared/user/block_user_dialog.tmpl
new file mode 100644
index 0000000000..c6db4ca1e4
--- /dev/null
+++ b/templates/shared/user/block_user_dialog.tmpl
@@ -0,0 +1,23 @@
+<div class="ui small modal" id="block-user-modal">
+ <div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
+ <div class="content">
+ <div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div>
+ <form class="ui form modal-form" method="post">
+ {{.CsrfTokenHtml}}
+ <input type="hidden" name="action" value="block" />
+ <input type="hidden" name="blockee" class="modal-blockee" />
+ <div class="field">
+ <label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label>
+ </div>
+ <div class="field">
+ <label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
+ <input id="block-note" name="note">
+ <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+ </div>
+ <div class="text right actions">
+ <button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+ <button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
new file mode 100644
index 0000000000..b2f0957691
--- /dev/null
+++ b/templates/shared/user/blocked_users.tmpl
@@ -0,0 +1,83 @@
+<h4 class="ui top attached header">
+ {{ctx.Locale.Tr "user.block.title"}}
+</h4>
+<div class="ui attached segment">
+ <p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
+ <ul>
+ <li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
+ <li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
+ <li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
+ <li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
+ <li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
+ <li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
+ </ul>
+</div>
+<div class="ui segment">
+ <form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
+ {{.CsrfTokenHtml}}
+ <input type="hidden" name="action" value="block" />
+ <div id="search-user-box" class="field ui fluid search input">
+ <input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+ <button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+ </div>
+ <div class="field">
+ <label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+ <input name="note">
+ <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+ </div>
+ </form>
+</div>
+<h4 class="ui top attached header">
+ {{ctx.Locale.Tr "user.block.list"}}
+</h4>
+<div class="ui attached segment">
+ <div class="flex-list">
+ {{range .UserBlocks}}
+ <div class="flex-item">
+ <div class="flex-item-leading">
+ {{ctx.AvatarUtils.Avatar .Blockee}}
+ </div>
+ <div class="flex-item-main">
+ <div class="flex-item-title">
+ <a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
+ </div>
+ {{if .Note}}
+ <div class="flex-item-body">
+ <i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
+ </div>
+ {{end}}
+ </div>
+ <div class="flex-item-trailing">
+ <button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
+ <form action="{{$.Link}}" method="post">
+ {{$.CsrfTokenHtml}}
+ <input type="hidden" name="action" value="unblock" />
+ <input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
+ <button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
+ </form>
+ </div>
+ </div>
+ {{else}}
+ <div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
+ {{end}}
+ </div>
+</div>
+<div class="ui small modal" id="block-user-note-modal">
+ <div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
+ <div class="content">
+ <form class="ui form" action="{{$.Link}}" method="post">
+ {{.CsrfTokenHtml}}
+ <input type="hidden" name="action" value="note" />
+ <input type="hidden" name="blockee" class="modal-blockee" />
+ <div class="field">
+ <label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+ <input name="note" class="modal-note" />
+ <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+ </div>
+ <div class="text right actions">
+ <button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+ <button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 88d3b9a6e5..a168e6903e 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -27,6 +27,12 @@
</div>
<div class="extra content gt-word-break">
<ul>
+ {{if .UserBlocking}}
+ <li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li>
+ {{if .UserBlocking.Note}}
+ <li class="text small red">{{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}</li>
+ {{end}}
+ {{end}}
{{if .ContextUser.Location}}
<li>
{{svg "octicon-location"}}
@@ -109,18 +115,29 @@
</li>
{{end}}
{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
- <li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" >
- {{if $.IsFollowing}}
- <button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
- {{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
- </button>
- {{else}}
- <button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
- {{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
- </button>
+ {{if not .UserBlocking}}
+ <li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
+ {{if $.IsFollowing}}
+ <button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
+ {{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
+ </button>
+ {{else}}
+ <button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
+ {{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
+ </button>
+ {{end}}
+ </li>
{{end}}
- </li>
+ <li>
+ {{if not .UserBlocking}}
+ <a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a>
+ {{else}}
+ <a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a>
+ {{end}}
+ </li>
{{end}}
</ul>
</div>
</div>
+
+{{template "shared/user/block_user_dialog" .}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9aba84a023..98198696bc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1955,6 +1955,151 @@
}
}
},
+ "/orgs/{org}/blocks": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "List users blocked by the organization",
+ "operationId": "organizationListBlocks",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/UserList"
+ }
+ }
+ }
+ },
+ "/orgs/{org}/blocks/{username}": {
+ "get": {
+ "tags": [
+ "organization"
+ ],
+ "summary": "Check if a user is blocked by the organization",
+ "operationId": "organizationCheckUserBlock",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "user to check",
+ "name": "username",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "organization"
+ ],
+ "summary": "Block a user",
+ "operationId": "organizationBlockUser",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "user to block",
+ "name": "username",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "optional note for the block",
+ "name": "note",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "organization"
+ ],
+ "summary": "Unblock a user",
+ "operationId": "organizationUnblockUser",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "user to unblock",
+ "name": "username",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/orgs/{org}/hooks": {
"get": {
"produces": [
@@ -4340,6 +4485,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
},
@@ -6692,6 +6840,9 @@
"400": {
"$ref": "#/responses/error"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/error"
},
@@ -10461,6 +10612,9 @@
"201": {
"$ref": "#/responses/PullRequest"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
},
@@ -12959,6 +13113,9 @@
"200": {
"$ref": "#/responses/WatchInfo"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -14513,6 +14670,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -15081,6 +15241,123 @@
}
}
},
+ "/user/blocks": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "List users blocked by the authenticated user",
+ "operationId": "userListBlocks",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/UserList"
+ }
+ }
+ }
+ },
+ "/user/blocks/{username}": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Check if a user is blocked by the authenticated user",
+ "operationId": "userCheckUserBlock",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "user to check",
+ "name": "username",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Block a user",
+ "operationId": "userBlockUser",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "user to block",
+ "name": "username",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "optional note for the block",
+ "name": "note",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Unblock a user",
+ "operationId": "userUnblockUser",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "user to unblock",
+ "name": "username",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/user/emails": {
"get": {
"produces": [
@@ -15258,6 +15535,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -15965,6 +16245,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..e495b85f58
--- /dev/null
+++ b/templates/user/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
+ <div class="user-setting-content">
+ {{template "shared/user/blocked_users" .}}
+ </div>
+{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
index a690d00352..c360944814 100644
--- a/templates/user/settings/navbar.tmpl
+++ b/templates/user/settings/navbar.tmpl
@@ -13,6 +13,9 @@
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
{{ctx.Locale.Tr "settings.security"}}
</a>
+ <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
+ {{ctx.Locale.Tr "user.block.list"}}
+ </a>
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
{{ctx.Locale.Tr "settings.applications"}}
</a>
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
index a9c5228a16..255b8332b2 100644
--- a/tests/integration/api_comment_test.go
+++ b/tests/integration/api_comment_test.go
@@ -108,6 +108,32 @@ func TestAPICreateComment(t *testing.T) {
DecodeJSON(t, resp, &updatedComment)
assert.EqualValues(t, commentBody, updatedComment.Body)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+
+ t.Run("BlockedByRepoOwner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+ "body": commentBody,
+ }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+
+ t.Run("BlockedByIssuePoster", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+ "body": commentBody,
+ }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+ MakeRequest(t, req, http.StatusForbidden)
+ })
}
func TestAPIGetComment(t *testing.T) {
diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go
index 4ca909f281..17e9f7aed5 100644
--- a/tests/integration/api_issue_reaction_test.go
+++ b/tests/integration/api_issue_reaction_test.go
@@ -58,6 +58,13 @@ func TestAPIIssuesReactions(t *testing.T) {
// Add existing reaction
MakeRequest(t, req, http.StatusForbidden)
+ // Blocked user can't react to comment
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "rocket",
+ }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+ MakeRequest(t, req, http.StatusForbidden)
+
// Get end result of reaction list of issue #1
req = NewRequest(t, "GET", urlStr).
AddTokenAuth(token)
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 650bac2e32..17b4e5bd71 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -84,7 +84,7 @@ func TestAPICreateIssue(t *testing.T) {
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
- urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
Body: body,
Title: title,
@@ -106,6 +106,12 @@ func TestAPICreateIssue(t *testing.T) {
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
+
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+ Title: title,
+ }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+ MakeRequest(t, req, http.StatusForbidden)
}
func TestAPICreateIssueParallel(t *testing.T) {
@@ -117,7 +123,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
- urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go
index 59cf85fef3..463db1dfb1 100644
--- a/tests/integration/api_repo_collaborator_test.go
+++ b/tests/integration/api_repo_collaborator_test.go
@@ -27,6 +27,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
@@ -86,6 +87,12 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
MakeRequest(t, req, http.StatusNotFound)
})
+ t.Run("CollaboratorBlocked", func(t *testing.T) {
+ ctx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
+ ctx.ExpectedCode = http.StatusForbidden
+ doAPIAddCollaborator(ctx, user34.Name, perm.AccessModeAdmin)(t)
+ })
+
t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
diff --git a/tests/integration/api_user_block_test.go b/tests/integration/api_user_block_test.go
new file mode 100644
index 0000000000..2cc3895a71
--- /dev/null
+++ b/tests/integration/api_user_block_test.go
@@ -0,0 +1,243 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBlockUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ countStars := func(t *testing.T, repoOwnerID, starrerID int64) int64 {
+ count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.StarredReposOptions{
+ StarrerID: starrerID,
+ RepoOwnerID: repoOwnerID,
+ IncludePrivate: true,
+ })
+ assert.NoError(t, err)
+ return count
+ }
+
+ countWatches := func(t *testing.T, repoOwnerID, watcherID int64) int64 {
+ count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.WatchedReposOptions{
+ WatcherID: watcherID,
+ RepoOwnerID: repoOwnerID,
+ })
+ assert.NoError(t, err)
+ return count
+ }
+
+ countRepositoryTransfers := func(t *testing.T, senderID, recipientID int64) int64 {
+ transfers, err := models.GetPendingRepositoryTransfers(db.DefaultContext, &models.PendingRepositoryTransferOptions{
+ SenderID: senderID,
+ RecipientID: recipientID,
+ })
+ assert.NoError(t, err)
+ return int64(len(transfers))
+ }
+
+ countAssignedIssues := func(t *testing.T, repoOwnerID, assigneeID int64) int64 {
+ _, count, err := issues_model.GetAssignedIssues(db.DefaultContext, &issues_model.AssignedIssuesOptions{
+ AssigneeID: assigneeID,
+ RepoOwnerID: repoOwnerID,
+ })
+ assert.NoError(t, err)
+ return count
+ }
+
+ countCollaborations := func(t *testing.T, repoOwnerID, collaboratorID int64) int64 {
+ count, err := db.Count[repo_model.Collaboration](db.DefaultContext, &repo_model.FindCollaborationOptions{
+ CollaboratorID: collaboratorID,
+ RepoOwnerID: repoOwnerID,
+ })
+ assert.NoError(t, err)
+ return count
+ }
+
+ t.Run("User", func(t *testing.T) {
+ var blockerID int64 = 16
+ blockerName := "user16"
+ blockerToken := getUserToken(t, blockerName, auth_model.AccessTokenScopeWriteUser)
+
+ var blockeeID int64 = 10
+ blockeeName := "user10"
+
+ t.Run("Block", func(t *testing.T) {
+ req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s?reason=test", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+ req = NewRequest(t, "GET", "/api/v1/user/blocks")
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "GET", "/api/v1/user/blocks").
+ AddTokenAuth(blockerToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var users []api.User
+ DecodeJSON(t, resp, &users)
+
+ assert.Len(t, users, 1)
+ assert.Equal(t, blockeeName, users[0].UserName)
+ })
+
+ t.Run("Unblock", func(t *testing.T) {
+ req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+ AddTokenAuth(blockerToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "GET", "/api/v1/user/blocks").
+ AddTokenAuth(blockerToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var users []api.User
+ DecodeJSON(t, resp, &users)
+
+ assert.Empty(t, users)
+ })
+ })
+
+ t.Run("Organization", func(t *testing.T) {
+ var blockerID int64 = 3
+ blockerName := "org3"
+
+ doerToken := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization)
+
+ var blockeeID int64 = 10
+ blockeeName := "user10"
+
+ t.Run("Block", func(t *testing.T) {
+ req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "user4")).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusBadRequest) // can't block member
+
+ assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countAssignedIssues(t, blockerID, blockeeID))
+ assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s?reason=test", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countAssignedIssues(t, blockerID, blockeeID))
+ assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+ AddTokenAuth(doerToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var users []api.User
+ DecodeJSON(t, resp, &users)
+
+ assert.Len(t, users, 1)
+ assert.Equal(t, blockeeName, users[0].UserName)
+ })
+
+ t.Run("Unblock", func(t *testing.T) {
+ req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+ AddTokenAuth(doerToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+ AddTokenAuth(doerToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var users []api.User
+ DecodeJSON(t, resp, &users)
+
+ assert.Empty(t, users)
+ })
+ })
+}
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go
index 1762732c10..fe20af6769 100644
--- a/tests/integration/api_user_follow_test.go
+++ b/tests/integration/api_user_follow_test.go
@@ -9,6 +9,8 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
@@ -33,6 +35,12 @@ func TestAPIFollow(t *testing.T) {
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/following/%s", user1)).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusNoContent)
+
+ // blocked user can't follow blocker
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequest(t, "PUT", "/api/v1/user/following/user2").
+ AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteUser))
+ MakeRequest(t, req, http.StatusForbidden)
})
t.Run("ListFollowing", func(t *testing.T) {
diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go
index 50423c80e7..0062889a92 100644
--- a/tests/integration/api_user_star_test.go
+++ b/tests/integration/api_user_star_test.go
@@ -9,6 +9,8 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
@@ -31,6 +33,12 @@ func TestAPIStar(t *testing.T) {
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
AddTokenAuth(tokenWithUserScope)
MakeRequest(t, req, http.StatusNoContent)
+
+ // blocked user can't star a repo
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+ AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+ MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetStarredRepos", func(t *testing.T) {
diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go
index 953e00551d..71dc57453e 100644
--- a/tests/integration/api_user_watch_test.go
+++ b/tests/integration/api_user_watch_test.go
@@ -9,6 +9,8 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
@@ -31,6 +33,12 @@ func TestAPIWatch(t *testing.T) {
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
AddTokenAuth(tokenWithRepoScope)
MakeRequest(t, req, http.StatusOK)
+
+ // blocked user can't watch a repo
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
+ AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+ MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetWatchedRepos", func(t *testing.T) {
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 3a5fdb97a6..0d733f663a 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -428,9 +428,9 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
assert.NoError(t, err)
assert.True(t, isMember, "Membership should be added to the right team")
- err = models.RemoveTeamMember(db.DefaultContext, team, user.ID)
+ err = models.RemoveTeamMember(db.DefaultContext, team, user)
assert.NoError(t, err)
- err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0].ID, user.ID)
+ err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0], user)
assert.NoError(t, err)
} else {
// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
@@ -460,7 +460,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
})
err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID)
assert.NoError(t, err)
- err = models.AddTeamMember(db.DefaultContext, team, user.ID)
+ err = models.AddTeamMember(db.DefaultContext, team, user)
assert.NoError(t, err)
isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
assert.NoError(t, err)