mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-11-04 00:11:04 +00:00 
			
		
		
		
	Merge branch 'rebase-v1.21/forgejo-moderation' into wip-v1.21-forgejo
This commit is contained in:
		
				commit
				
					
						2f84786d0c
					
				
			
		
					 79 changed files with 2355 additions and 59 deletions
				
			
		| 
						 | 
					@ -588,7 +588,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if repoChanged {
 | 
							if repoChanged {
 | 
				
			||||||
			// Add feeds for user self and all watchers.
 | 
								// Add feeds for user self and all watchers.
 | 
				
			||||||
			watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
 | 
								watchers, err = repo_model.GetWatchersExcludeBlocked(ctx, act.RepoID, act.ActUserID)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return fmt.Errorf("get watchers: %w", err)
 | 
									return fmt.Errorf("get watchers: %w", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -235,6 +235,15 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 | 
				
			||||||
		for _, id := range issueUnWatches {
 | 
							for _, id := range issueUnWatches {
 | 
				
			||||||
			toNotify.Remove(id)
 | 
								toNotify.Remove(id)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove users who have the notification author blocked.
 | 
				
			||||||
 | 
							blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, id := range blockedAuthorIDs {
 | 
				
			||||||
 | 
								toNotify.Remove(id)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = issue.LoadRepo(ctx)
 | 
						err = issue.LoadRepo(ctx)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,3 +66,12 @@
 | 
				
			||||||
  tree_path: "README.md"
 | 
					  tree_path: "README.md"
 | 
				
			||||||
  created_unix: 946684812
 | 
					  created_unix: 946684812
 | 
				
			||||||
  invalidated: true
 | 
					  invalidated: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 8
 | 
				
			||||||
 | 
					  type: 0 # comment
 | 
				
			||||||
 | 
					  poster_id: 2
 | 
				
			||||||
 | 
					  issue_id: 4 # in repo_id 2
 | 
				
			||||||
 | 
					  content: "I just wanted to add.."
 | 
				
			||||||
 | 
					  created_unix: 946684812
 | 
				
			||||||
 | 
					  updated_unix: 946684812
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								models/fixtures/forgejo_blocked_user.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								models/fixtures/forgejo_blocked_user.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  user_id: 4
 | 
				
			||||||
 | 
					  block_id: 1
 | 
				
			||||||
 | 
					  created_unix: 1671607299
 | 
				
			||||||
| 
						 | 
					@ -61,7 +61,7 @@
 | 
				
			||||||
  priority: 0
 | 
					  priority: 0
 | 
				
			||||||
  is_closed: true
 | 
					  is_closed: true
 | 
				
			||||||
  is_pull: false
 | 
					  is_pull: false
 | 
				
			||||||
  num_comments: 0
 | 
					  num_comments: 1
 | 
				
			||||||
  created_unix: 946684830
 | 
					  created_unix: 946684830
 | 
				
			||||||
  updated_unix: 978307200
 | 
					  updated_unix: 978307200
 | 
				
			||||||
  is_locked: false
 | 
					  is_locked: false
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@
 | 
				
			||||||
  lower_name: repo2
 | 
					  lower_name: repo2
 | 
				
			||||||
  name: repo2
 | 
					  name: repo2
 | 
				
			||||||
  default_branch: master
 | 
					  default_branch: master
 | 
				
			||||||
  num_watches: 0
 | 
					  num_watches: 1
 | 
				
			||||||
  num_stars: 1
 | 
					  num_stars: 1
 | 
				
			||||||
  num_forks: 0
 | 
					  num_forks: 0
 | 
				
			||||||
  num_issues: 2
 | 
					  num_issues: 2
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@
 | 
				
			||||||
  is_empty: false
 | 
					  is_empty: false
 | 
				
			||||||
  is_archived: false
 | 
					  is_archived: false
 | 
				
			||||||
  is_mirror: false
 | 
					  is_mirror: false
 | 
				
			||||||
  status: 0
 | 
					  status: 2
 | 
				
			||||||
  is_fork: false
 | 
					  is_fork: false
 | 
				
			||||||
  fork_id: 0
 | 
					  fork_id: 0
 | 
				
			||||||
  is_template: false
 | 
					  is_template: false
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,3 +27,9 @@
 | 
				
			||||||
  user_id: 11
 | 
					  user_id: 11
 | 
				
			||||||
  repo_id: 1
 | 
					  repo_id: 1
 | 
				
			||||||
  mode: 3 # auto
 | 
					  mode: 3 # auto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 6
 | 
				
			||||||
 | 
					  user_id: 4
 | 
				
			||||||
 | 
					  repo_id: 2
 | 
				
			||||||
 | 
					  mode: 1 # normal
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -305,6 +305,8 @@ func TestIssue_ResolveMentions(t *testing.T) {
 | 
				
			||||||
	testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
 | 
						testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
 | 
				
			||||||
	// Public repo, doer
 | 
						// Public repo, doer
 | 
				
			||||||
	testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
 | 
						testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
 | 
				
			||||||
 | 
						// Public repo, blocked user
 | 
				
			||||||
 | 
						testSuccess("user2", "repo1", "user1", []string{"user4"}, []int64{})
 | 
				
			||||||
	// Private repo, team member
 | 
						// Private repo, team member
 | 
				
			||||||
	testSuccess("org17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
 | 
						testSuccess("org17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
 | 
				
			||||||
	// Private repo, not a team member
 | 
						// Private repo, not a team member
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -630,9 +630,11 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
 | 
				
			||||||
				teamusers := make([]*user_model.User, 0, 20)
 | 
									teamusers := make([]*user_model.User, 0, 20)
 | 
				
			||||||
				if err := db.GetEngine(ctx).
 | 
									if err := db.GetEngine(ctx).
 | 
				
			||||||
					Join("INNER", "team_user", "team_user.uid = `user`.id").
 | 
										Join("INNER", "team_user", "team_user.uid = `user`.id").
 | 
				
			||||||
 | 
										Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
 | 
				
			||||||
					In("`team_user`.team_id", checked).
 | 
										In("`team_user`.team_id", checked).
 | 
				
			||||||
					And("`user`.is_active = ?", true).
 | 
										And("`user`.is_active = ?", true).
 | 
				
			||||||
					And("`user`.prohibit_login = ?", false).
 | 
										And("`user`.prohibit_login = ?", false).
 | 
				
			||||||
 | 
										And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
 | 
				
			||||||
					Find(&teamusers); err != nil {
 | 
										Find(&teamusers); err != nil {
 | 
				
			||||||
					return nil, fmt.Errorf("get teams users: %w", err)
 | 
										return nil, fmt.Errorf("get teams users: %w", err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
| 
						 | 
					@ -666,8 +668,10 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unchecked := make([]*user_model.User, 0, len(mentionUsers))
 | 
						unchecked := make([]*user_model.User, 0, len(mentionUsers))
 | 
				
			||||||
	if err := db.GetEngine(ctx).
 | 
						if err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
 | 
				
			||||||
		Where("`user`.is_active = ?", true).
 | 
							Where("`user`.is_active = ?", true).
 | 
				
			||||||
		And("`user`.prohibit_login = ?", false).
 | 
							And("`user`.prohibit_login = ?", false).
 | 
				
			||||||
 | 
							And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
 | 
				
			||||||
		In("`user`.lower_name", mentionUsers).
 | 
							In("`user`.lower_name", mentionUsers).
 | 
				
			||||||
		Find(&unchecked); err != nil {
 | 
							Find(&unchecked); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("find mentioned users: %w", err)
 | 
							return nil, fmt.Errorf("find mentioned users: %w", err)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
 | 
				
			||||||
	return reaction, nil
 | 
						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.
 | 
					// DeleteReaction deletes reaction for issue or comment.
 | 
				
			||||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 | 
					func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 | 
				
			||||||
	reaction := &Reaction{
 | 
						reaction := &Reaction{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,11 +19,14 @@ import (
 | 
				
			||||||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
 | 
					func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
 | 
				
			||||||
	var reaction *issues_model.Reaction
 | 
						var reaction *issues_model.Reaction
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	if commentID == 0 {
 | 
						// NOTE: This doesn't do user blocking checking.
 | 
				
			||||||
		reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
 | 
						reaction, err = issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
 | 
				
			||||||
	} else {
 | 
							DoerID:    doerID,
 | 
				
			||||||
		reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
 | 
							IssueID:   issueID,
 | 
				
			||||||
	}
 | 
							CommentID: commentID,
 | 
				
			||||||
 | 
							Type:      content,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.NotNil(t, reaction)
 | 
						assert.NotNil(t, reaction)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,6 +137,19 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCollaboratorWithUser returns all collaborator IDs of collabUserID on
 | 
				
			||||||
 | 
					// repositories of ownerID.
 | 
				
			||||||
 | 
					func GetCollaboratorWithUser(ctx context.Context, ownerID, collabUserID int64) ([]int64, error) {
 | 
				
			||||||
 | 
						collabsID := make([]int64, 0, 8)
 | 
				
			||||||
 | 
						err := db.GetEngine(ctx).Table("collaboration").Select("collaboration.`id`").
 | 
				
			||||||
 | 
							Join("INNER", "repository", "repository.id = collaboration.repo_id").
 | 
				
			||||||
 | 
							Where("repository.`owner_id` = ?", ownerID).
 | 
				
			||||||
 | 
							And("collaboration.`user_id` = ?", collabUserID).
 | 
				
			||||||
 | 
							Find(&collabsID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return collabsID, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
 | 
					// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
 | 
				
			||||||
func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int64) (bool, error) {
 | 
					func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int64) (bool, error) {
 | 
				
			||||||
	if repo.OwnerID == userID {
 | 
						if repo.OwnerID == userID {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ import (
 | 
				
			||||||
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
						access_model "code.gitea.io/gitea/models/perm/access"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -156,3 +157,23 @@ func TestRepo_GetCollaboration(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Nil(t, collab)
 | 
						assert.Nil(t, collab)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetCollaboratorWithUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user16 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
 | 
				
			||||||
 | 
						user15 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
 | 
				
			||||||
 | 
						user18 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 18})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						collabs, err := repo_model.GetCollaboratorWithUser(db.DefaultContext, user16.ID, user15.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, collabs, 2)
 | 
				
			||||||
 | 
						assert.EqualValues(t, 5, collabs[0])
 | 
				
			||||||
 | 
						assert.EqualValues(t, 7, collabs[1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						collabs, err = repo_model.GetCollaboratorWithUser(db.DefaultContext, user16.ID, user18.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, collabs, 2)
 | 
				
			||||||
 | 
						assert.EqualValues(t, 6, collabs[0])
 | 
				
			||||||
 | 
						assert.EqualValues(t, 8, collabs[1])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -177,3 +177,16 @@ func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull boo
 | 
				
			||||||
		Limit(30).
 | 
							Limit(30).
 | 
				
			||||||
		Find(&users)
 | 
							Find(&users)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetWatchedRepoIDsOwnedBy returns the repos owned by a particular user watched by a particular user
 | 
				
			||||||
 | 
					func GetWatchedRepoIDsOwnedBy(ctx context.Context, userID, ownedByUserID int64) ([]int64, error) {
 | 
				
			||||||
 | 
						repoIDs := make([]int64, 0, 10)
 | 
				
			||||||
 | 
						err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Table("repository").
 | 
				
			||||||
 | 
							Select("`repository`.id").
 | 
				
			||||||
 | 
							Join("LEFT", "watch", "`repository`.id=`watch`.repo_id").
 | 
				
			||||||
 | 
							Where("`watch`.user_id=?", userID).
 | 
				
			||||||
 | 
							And("`watch`.mode<>?", WatchModeDont).
 | 
				
			||||||
 | 
							And("`repository`.owner_id=?", ownedByUserID).Find(&repoIDs)
 | 
				
			||||||
 | 
						return repoIDs, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -71,3 +72,15 @@ func TestRepoGetReviewers(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, reviewers, 1)
 | 
						assert.Len(t, reviewers, 1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetWatchedRepoIDsOwnedBy(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(db.DefaultContext, user1.ID, user2.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, repoIDs, 1)
 | 
				
			||||||
 | 
						assert.EqualValues(t, 1, repoIDs[0])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@ import (
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WatchMode specifies what kind of watch the user has on a repository
 | 
					// WatchMode specifies what kind of watch the user has on a repository
 | 
				
			||||||
| 
						 | 
					@ -142,6 +144,21 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
 | 
				
			||||||
		Find(&watches)
 | 
							Find(&watches)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetWatchersExcludeBlocked returns all watchers of given repository, whereby
 | 
				
			||||||
 | 
					// the doer isn't blocked by one of the watchers.
 | 
				
			||||||
 | 
					func GetWatchersExcludeBlocked(ctx context.Context, repoID, doerID int64) ([]*Watch, error) {
 | 
				
			||||||
 | 
						watches := make([]*Watch, 0, 10)
 | 
				
			||||||
 | 
						return watches, db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Join("INNER", "`user`", "`user`.id = `watch`.user_id").
 | 
				
			||||||
 | 
							Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `watch`.user_id").
 | 
				
			||||||
 | 
							Where("`watch`.repo_id=?", repoID).
 | 
				
			||||||
 | 
							And("`watch`.mode<>?", WatchModeDont).
 | 
				
			||||||
 | 
							And("`user`.is_active=?", true).
 | 
				
			||||||
 | 
							And("`user`.prohibit_login=?", false).
 | 
				
			||||||
 | 
							And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doerID})).
 | 
				
			||||||
 | 
							Find(&watches)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
 | 
					// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
 | 
				
			||||||
// but avoids joining with `user` for performance reasons
 | 
					// but avoids joining with `user` for performance reasons
 | 
				
			||||||
// User permissions must be verified elsewhere if required
 | 
					// User permissions must be verified elsewhere if required
 | 
				
			||||||
| 
						 | 
					@ -184,3 +201,9 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return watchRepoMode(ctx, watch, WatchModeAuto)
 | 
						return watchRepoMode(ctx, watch, WatchModeAuto)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnwatchRepos will unwatch the user from all given repositories.
 | 
				
			||||||
 | 
					func UnwatchRepos(ctx context.Context, userID int64, repoIDs []int64) error {
 | 
				
			||||||
 | 
						_, err := db.GetEngine(ctx).Where("user_id=?", userID).In("repo_id", repoIDs).Delete(&Watch{})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,24 @@ func TestGetWatchers(t *testing.T) {
 | 
				
			||||||
	assert.Len(t, watches, 0)
 | 
						assert.Len(t, watches, 0)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetWatchersExcludeBlocked(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
						watches, err := repo_model.GetWatchersExcludeBlocked(db.DefaultContext, repo.ID, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// One watchers are inactive and one watcher is blocked, thus minus 2
 | 
				
			||||||
 | 
						assert.Len(t, watches, repo.NumWatches-2)
 | 
				
			||||||
 | 
						for _, watch := range watches {
 | 
				
			||||||
 | 
							assert.EqualValues(t, repo.ID, watch.RepoID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watches, err = repo_model.GetWatchersExcludeBlocked(db.DefaultContext, unittest.NonexistentID, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, watches, 0)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRepository_GetWatchers(t *testing.T) {
 | 
					func TestRepository_GetWatchers(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,3 +155,16 @@ func TestWatchRepoMode(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone))
 | 
						assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone))
 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
 | 
						unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUnwatchRepos(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
 | 
				
			||||||
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := repo_model.UnwatchRepos(db.DefaultContext, 4, []int64{1, 2})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -417,3 +417,13 @@ func TransferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return committer.Commit()
 | 
						return committer.Commit()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPendingTransfers returns the pending transfers of recipient which were sent by by doer.
 | 
				
			||||||
 | 
					func GetPendingTransferIDs(ctx context.Context, reciepientID, doerID int64) ([]int64, error) {
 | 
				
			||||||
 | 
						pendingTransferIDs := make([]int64, 0, 8)
 | 
				
			||||||
 | 
						return pendingTransferIDs, db.GetEngine(ctx).Table("repo_transfer").
 | 
				
			||||||
 | 
							Where("doer_id = ?", doerID).
 | 
				
			||||||
 | 
							And("recipient_id = ?", reciepientID).
 | 
				
			||||||
 | 
							Cols("id").
 | 
				
			||||||
 | 
							Find(&pendingTransferIDs)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,3 +55,16 @@ func TestRepositoryTransfer(t *testing.T) {
 | 
				
			||||||
	// Cancel transfer
 | 
						// Cancel transfer
 | 
				
			||||||
	assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo))
 | 
						assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetPendingTransferIDs(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
				
			||||||
 | 
						reciepient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						pendingTransfer := unittest.AssertExistsAndLoadBean(t, &RepoTransfer{RecipientID: reciepient.ID, DoerID: doer.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pendingTransferIDs, err := GetPendingTransferIDs(db.DefaultContext, reciepient.ID, doer.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						if assert.Len(t, pendingTransferIDs, 1) {
 | 
				
			||||||
 | 
							assert.EqualValues(t, pendingTransfer.ID, pendingTransferIDs[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										91
									
								
								models/user/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								models/user/block.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,91 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrBlockedByUser defines an error stating that the user is not allowed to perform the action because they are blocked.
 | 
				
			||||||
 | 
					var ErrBlockedByUser = errors.New("user is blocked by the poster or repository owner")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUser represents a blocked user entry.
 | 
				
			||||||
 | 
					type BlockedUser struct {
 | 
				
			||||||
 | 
						ID int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						// UID of the one who got blocked.
 | 
				
			||||||
 | 
						BlockID int64 `xorm:"index"`
 | 
				
			||||||
 | 
						// UID of the one who did the block action.
 | 
				
			||||||
 | 
						UserID int64 `xorm:"index"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"created"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName provides the real table name
 | 
				
			||||||
 | 
					func (*BlockedUser) TableName() string {
 | 
				
			||||||
 | 
						return "forgejo_blocked_user"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						db.RegisterModel(new(BlockedUser))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsBlocked returns if userID has blocked blockID.
 | 
				
			||||||
 | 
					func IsBlocked(ctx context.Context, userID, blockID int64) bool {
 | 
				
			||||||
 | 
						has, _ := db.GetEngine(ctx).Exist(&BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsBlockedMultiple returns if one of the userIDs has blocked blockID.
 | 
				
			||||||
 | 
					func IsBlockedMultiple(ctx context.Context, userIDs []int64, blockID int64) bool {
 | 
				
			||||||
 | 
						has, _ := db.GetEngine(ctx).In("user_id", userIDs).Exist(&BlockedUser{BlockID: blockID})
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser removes the blocked user entry.
 | 
				
			||||||
 | 
					func UnblockUser(ctx context.Context, userID, blockID int64) error {
 | 
				
			||||||
 | 
						_, err := db.GetEngine(ctx).Delete(&BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CountBlockedUsers returns the number of users the user has blocked.
 | 
				
			||||||
 | 
					func CountBlockedUsers(ctx context.Context, userID int64) (int64, error) {
 | 
				
			||||||
 | 
						return db.GetEngine(ctx).Where("user_id=?", userID).Count(&BlockedUser{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedUsers returns the users that the user has blocked.
 | 
				
			||||||
 | 
					// The created_unix field of the user struct is overridden by the creation_unix
 | 
				
			||||||
 | 
					// field of blockeduser.
 | 
				
			||||||
 | 
					func ListBlockedUsers(ctx context.Context, userID int64, opts db.ListOptions) ([]*User, error) {
 | 
				
			||||||
 | 
						sess := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Select("`forgejo_blocked_user`.created_unix, `user`.*").
 | 
				
			||||||
 | 
							Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id").
 | 
				
			||||||
 | 
							Where("`forgejo_blocked_user`.user_id=?", userID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opts.Page > 0 {
 | 
				
			||||||
 | 
							sess = db.SetSessionPagination(sess, &opts)
 | 
				
			||||||
 | 
							users := make([]*User, 0, opts.PageSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return users, sess.Find(&users)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						users := make([]*User, 0, 8)
 | 
				
			||||||
 | 
						return users, sess.Find(&users)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedByUsersID returns the ids of the users that blocked the user.
 | 
				
			||||||
 | 
					func ListBlockedByUsersID(ctx context.Context, userID int64) ([]int64, error) {
 | 
				
			||||||
 | 
						users := make([]int64, 0, 8)
 | 
				
			||||||
 | 
						err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Table("user").
 | 
				
			||||||
 | 
							Select("`user`.id").
 | 
				
			||||||
 | 
							Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.user_id").
 | 
				
			||||||
 | 
							Where("`forgejo_blocked_user`.block_id=?", userID).
 | 
				
			||||||
 | 
							Find(&users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return users, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								models/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								models/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,77 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 TestIsBlocked(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 1, 1))
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 3, 2))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsBlockedMultiple(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4}, 1))
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4, 3, 4, 5}, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{1}, 1))
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{3, 4, 1}, 2))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUnblockUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, user_model.UnblockUser(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple test cases to ensure the function can also respond with false.
 | 
				
			||||||
 | 
						assert.False(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListBlockedUsers(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(db.DefaultContext, 4, db.ListOptions{})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						if assert.Len(t, blockedUsers, 1) {
 | 
				
			||||||
 | 
							assert.EqualValues(t, 1, blockedUsers[0].ID)
 | 
				
			||||||
 | 
							// The function returns the created Unix of the block, not that of the user.
 | 
				
			||||||
 | 
							assert.EqualValues(t, 1671607299, blockedUsers[0].CreatedUnix)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListBlockedByUsersID(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedByUserIDs, err := user_model.ListBlockedByUsersID(db.DefaultContext, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						if assert.Len(t, blockedByUserIDs, 1) {
 | 
				
			||||||
 | 
							assert.EqualValues(t, 4, blockedByUserIDs[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCountBlockedUsers(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						count, err := user_model.CountBlockedUsers(db.DefaultContext, 4)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.EqualValues(t, 1, count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						count, err = user_model.CountBlockedUsers(db.DefaultContext, 1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.EqualValues(t, 0, count)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,10 @@ func FollowUser(ctx context.Context, userID, followID int64) (err error) {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if IsBlocked(ctx, userID, followID) || IsBlocked(ctx, followID, userID) {
 | 
				
			||||||
 | 
							return ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -457,6 +457,12 @@ func TestFollowUser(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
 | 
						assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Blocked user.
 | 
				
			||||||
 | 
						assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 1, 4))
 | 
				
			||||||
 | 
						assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 4, 1))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 1, FollowID: 4})
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 4, FollowID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,10 @@ import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
 | 
					func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, repo.OwnerID, u.ID) || user_model.IsBlocked(ctx, u.ID, repo.OwnerID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return db.WithTx(ctx, func(ctx context.Context) error {
 | 
						return db.WithTx(ctx, func(ctx context.Context) error {
 | 
				
			||||||
		collaboration := &repo_model.Collaboration{
 | 
							collaboration := &repo_model.Collaboration{
 | 
				
			||||||
			RepoID: repo.ID,
 | 
								RepoID: repo.ID,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,33 @@ func TestRepository_AddCollaborator(t *testing.T) {
 | 
				
			||||||
	testSuccess(3, 4)
 | 
						testSuccess(3, 4)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_AddCollaborator_IsBlocked(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testSuccess := func(repoID, userID int64) {
 | 
				
			||||||
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 | 
				
			||||||
 | 
							assert.NoError(t, repo.LoadOwner(db.DefaultContext))
 | 
				
			||||||
 | 
							user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Owner blocked user.
 | 
				
			||||||
 | 
							unittest.AssertSuccessfulInsert(t, &user_model.BlockedUser{UserID: repo.OwnerID, BlockID: userID})
 | 
				
			||||||
 | 
							assert.ErrorIs(t, AddCollaborator(db.DefaultContext, repo, user), user_model.ErrBlockedByUser)
 | 
				
			||||||
 | 
							unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID})
 | 
				
			||||||
 | 
							_, err := db.DeleteByBean(db.DefaultContext, &user_model.BlockedUser{UserID: repo.OwnerID, BlockID: userID})
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// User has owner blocked.
 | 
				
			||||||
 | 
							unittest.AssertSuccessfulInsert(t, &user_model.BlockedUser{UserID: userID, BlockID: repo.OwnerID})
 | 
				
			||||||
 | 
							assert.ErrorIs(t, AddCollaborator(db.DefaultContext, repo, user), user_model.ErrBlockedByUser)
 | 
				
			||||||
 | 
							unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Ensure idempotency (public repository).
 | 
				
			||||||
 | 
						testSuccess(1, 4)
 | 
				
			||||||
 | 
						testSuccess(1, 4)
 | 
				
			||||||
 | 
						// Add collaborator to private repository.
 | 
				
			||||||
 | 
						testSuccess(3, 4)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRepoPermissionPublicNonOrgRepo(t *testing.T) {
 | 
					func TestRepoPermissionPublicNonOrgRepo(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								modules/structs/moderation.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								modules/structs/moderation.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package structs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUser represents a blocked user.
 | 
				
			||||||
 | 
					type BlockedUser struct {
 | 
				
			||||||
 | 
						BlockID int64 `json:"block_id"`
 | 
				
			||||||
 | 
						// swagger:strfmt date-time
 | 
				
			||||||
 | 
						Created time.Time `json:"created_at"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -594,6 +594,12 @@ joined_on = Joined on %s
 | 
				
			||||||
repositories = Repositories
 | 
					repositories = Repositories
 | 
				
			||||||
activity = Public Activity
 | 
					activity = Public Activity
 | 
				
			||||||
followers = Followers
 | 
					followers = Followers
 | 
				
			||||||
 | 
					block_user = Block User
 | 
				
			||||||
 | 
					block_user.detail = Please understand that if you block this user, other actions will be taken. Such as:
 | 
				
			||||||
 | 
					block_user.detail_1 = You are being unfollowed from this user.
 | 
				
			||||||
 | 
					block_user.detail_2 = This user cannot interact with your repositories, created issues and comments.
 | 
				
			||||||
 | 
					block_user.detail_3 = This user cannot add you as a collaborator, nor can you add them as a collaborator.
 | 
				
			||||||
 | 
					follow_blocked_user = You cannot follow this user because you have blocked this user or this user has blocked you.
 | 
				
			||||||
starred = Starred Repositories
 | 
					starred = Starred Repositories
 | 
				
			||||||
watched = Watched Repositories
 | 
					watched = Watched Repositories
 | 
				
			||||||
code = Code
 | 
					code = Code
 | 
				
			||||||
| 
						 | 
					@ -602,6 +608,8 @@ overview = Overview
 | 
				
			||||||
following = Following
 | 
					following = Following
 | 
				
			||||||
follow = Follow
 | 
					follow = Follow
 | 
				
			||||||
unfollow = Unfollow
 | 
					unfollow = Unfollow
 | 
				
			||||||
 | 
					block = Block
 | 
				
			||||||
 | 
					unblock = Unblock
 | 
				
			||||||
user_bio = Biography
 | 
					user_bio = Biography
 | 
				
			||||||
disabled_public_activity = This user has disabled the public visibility of the activity.
 | 
					disabled_public_activity = This user has disabled the public visibility of the activity.
 | 
				
			||||||
email_visibility.limited = Your email address is visible to all authenticated users
 | 
					email_visibility.limited = Your email address is visible to all authenticated users
 | 
				
			||||||
| 
						 | 
					@ -631,6 +639,7 @@ account_link = Linked Accounts
 | 
				
			||||||
organization = Organizations
 | 
					organization = Organizations
 | 
				
			||||||
uid = UID
 | 
					uid = UID
 | 
				
			||||||
webauthn = Security Keys
 | 
					webauthn = Security Keys
 | 
				
			||||||
 | 
					blocked_users = Blocked Users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public_profile = Public Profile
 | 
					public_profile = Public Profile
 | 
				
			||||||
biography_placeholder = Tell us a little bit about yourself! (You can use Markdown)
 | 
					biography_placeholder = Tell us a little bit about yourself! (You can use Markdown)
 | 
				
			||||||
| 
						 | 
					@ -900,6 +909,7 @@ hooks.desc = Add webhooks which will be triggered for <strong>all repositories</
 | 
				
			||||||
 | 
					
 | 
				
			||||||
orgs_none = You are not a member of any organizations.
 | 
					orgs_none = You are not a member of any organizations.
 | 
				
			||||||
repos_none = You do not own any repositories.
 | 
					repos_none = You do not own any repositories.
 | 
				
			||||||
 | 
					blocked_users_none = You haven't blocked any users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
delete_account = Delete Your Account
 | 
					delete_account = Delete Your Account
 | 
				
			||||||
delete_prompt = This operation will permanently delete your user account. It <strong>CANNOT</strong> be undone.
 | 
					delete_prompt = This operation will permanently delete your user account. It <strong>CANNOT</strong> be undone.
 | 
				
			||||||
| 
						 | 
					@ -922,6 +932,10 @@ visibility.limited_tooltip = Visible only to authenticated users
 | 
				
			||||||
visibility.private = Private
 | 
					visibility.private = Private
 | 
				
			||||||
visibility.private_tooltip = Visible only to members of organizations you have joined
 | 
					visibility.private_tooltip = Visible only to members of organizations you have joined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					blocked_since = Blocked since %s
 | 
				
			||||||
 | 
					user_unblock_success = The user has been unblocked successfully.
 | 
				
			||||||
 | 
					user_block_success = The user has been blocked successfully.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[repo]
 | 
					[repo]
 | 
				
			||||||
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
 | 
					new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
 | 
				
			||||||
owner = Owner
 | 
					owner = Owner
 | 
				
			||||||
| 
						 | 
					@ -1671,6 +1685,8 @@ issues.content_history.delete_from_history = Delete from history
 | 
				
			||||||
issues.content_history.delete_from_history_confirm = Delete from history?
 | 
					issues.content_history.delete_from_history_confirm = Delete from history?
 | 
				
			||||||
issues.content_history.options = Options
 | 
					issues.content_history.options = Options
 | 
				
			||||||
issues.reference_link = Reference: %s
 | 
					issues.reference_link = Reference: %s
 | 
				
			||||||
 | 
					issues.blocked_by_user = You cannot create a issue on this repository because you are blocked by the repository owner.
 | 
				
			||||||
 | 
					issues.comment.blocked_by_user = You cannot create a comment on this issue because you are blocked by the repository owner or the poster of the issue.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
compare.compare_base = base
 | 
					compare.compare_base = base
 | 
				
			||||||
compare.compare_head = compare
 | 
					compare.compare_head = compare
 | 
				
			||||||
| 
						 | 
					@ -1750,6 +1766,7 @@ pulls.reject_count_n = "%d change requests"
 | 
				
			||||||
pulls.waiting_count_1 = "%d waiting review"
 | 
					pulls.waiting_count_1 = "%d waiting review"
 | 
				
			||||||
pulls.waiting_count_n = "%d waiting reviews"
 | 
					pulls.waiting_count_n = "%d waiting reviews"
 | 
				
			||||||
pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
 | 
					pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
 | 
				
			||||||
 | 
					pulls.blocked_by_user = You cannot create a pull request on this repository because you are blocked by the repository owner.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
					pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
				
			||||||
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
					pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
				
			||||||
| 
						 | 
					@ -2056,6 +2073,7 @@ settings.reindex_requested=Reindex Requested
 | 
				
			||||||
settings.admin_enable_close_issues_via_commit_in_any_branch = Close an issue via a commit made in a non default branch
 | 
					settings.admin_enable_close_issues_via_commit_in_any_branch = Close an issue via a commit made in a non default branch
 | 
				
			||||||
settings.danger_zone = Danger Zone
 | 
					settings.danger_zone = Danger Zone
 | 
				
			||||||
settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name.
 | 
					settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name.
 | 
				
			||||||
 | 
					settings.new_owner_blocked_doer = The new owner has blocked you.
 | 
				
			||||||
settings.convert = Convert to Regular Repository
 | 
					settings.convert = Convert to Regular Repository
 | 
				
			||||||
settings.convert_desc = You can convert this mirror into a regular repository. This cannot be undone.
 | 
					settings.convert_desc = You can convert this mirror into a regular repository. This cannot be undone.
 | 
				
			||||||
settings.convert_notices_1 = This operation will convert the mirror into a regular repository and cannot be undone.
 | 
					settings.convert_notices_1 = This operation will convert the mirror into a regular repository and cannot be undone.
 | 
				
			||||||
| 
						 | 
					@ -2114,6 +2132,8 @@ 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_inactive_user = Cannot add an inactive user as a collaborator.
 | 
				
			||||||
settings.add_collaborator_owner = Cannot add an owner 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_duplicate = The collaborator is already added to this repository.
 | 
				
			||||||
 | 
					settings.add_collaborator_blocked_our = Cannot add the collaborator, because the repository owner has blocked them.
 | 
				
			||||||
 | 
					settings.add_collaborator_blocked_them = Cannot add the collaborator, because they have blocked the repository owner.
 | 
				
			||||||
settings.delete_collaborator = Remove
 | 
					settings.delete_collaborator = Remove
 | 
				
			||||||
settings.collaborator_deletion = Remove Collaborator
 | 
					settings.collaborator_deletion = Remove Collaborator
 | 
				
			||||||
settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
 | 
					settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
 | 
				
			||||||
| 
						 | 
					@ -2577,6 +2597,7 @@ team_access_desc = Repository access
 | 
				
			||||||
team_permission_desc = Permission
 | 
					team_permission_desc = Permission
 | 
				
			||||||
team_unit_desc = Allow Access to Repository Sections
 | 
					team_unit_desc = Allow Access to Repository Sections
 | 
				
			||||||
team_unit_disabled = (Disabled)
 | 
					team_unit_disabled = (Disabled)
 | 
				
			||||||
 | 
					follow_blocked_user = You cannot follow this organisation because this organisation has blocked you.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
form.name_reserved = The organization name "%s" is reserved.
 | 
					form.name_reserved = The organization name "%s" is reserved.
 | 
				
			||||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name.
 | 
					form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name.
 | 
				
			||||||
| 
						 | 
					@ -2824,7 +2845,7 @@ users.cannot_delete_self = "You cannot delete yourself"
 | 
				
			||||||
users.still_own_repo = This user still owns one or more repositories. Delete or transfer these repositories first.
 | 
					users.still_own_repo = This user still owns one or more repositories. Delete or transfer these repositories first.
 | 
				
			||||||
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
 | 
					users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
 | 
				
			||||||
users.purge = Purge User
 | 
					users.purge = Purge User
 | 
				
			||||||
users.purge_help = Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too.
 | 
					users.purge_help = Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments and issues posted by this user will also be deleted.
 | 
				
			||||||
users.still_own_packages = This user still owns one or more packages, delete these packages first.
 | 
					users.still_own_packages = This user still owns one or more packages, delete these packages first.
 | 
				
			||||||
users.deletion_success = The user account has been deleted.
 | 
					users.deletion_success = The user account has been deleted.
 | 
				
			||||||
users.reset_2fa = Reset 2FA
 | 
					users.reset_2fa = Reset 2FA
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -991,6 +991,14 @@ func Routes() *web.Route {
 | 
				
			||||||
					Delete(user.DeleteHook)
 | 
										Delete(user.DeleteHook)
 | 
				
			||||||
			}, reqWebhooksEnabled())
 | 
								}, reqWebhooksEnabled())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m.Group("", func() {
 | 
				
			||||||
 | 
									m.Get("/list_blocked", user.ListBlockedUsers)
 | 
				
			||||||
 | 
									m.Group("", func() {
 | 
				
			||||||
 | 
										m.Put("/block/{username}", user.BlockUser)
 | 
				
			||||||
 | 
										m.Put("/unblock/{username}", user.UnblockUser)
 | 
				
			||||||
 | 
									}, context_service.UserAssignmentAPI())
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			m.Group("/avatar", func() {
 | 
								m.Group("/avatar", func() {
 | 
				
			||||||
				m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
 | 
									m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
 | 
				
			||||||
				m.Delete("", user.DeleteAvatar)
 | 
									m.Delete("", user.DeleteAvatar)
 | 
				
			||||||
| 
						 | 
					@ -1430,6 +1438,14 @@ func Routes() *web.Route {
 | 
				
			||||||
				m.Delete("", org.DeleteAvatar)
 | 
									m.Delete("", org.DeleteAvatar)
 | 
				
			||||||
			}, reqToken(), reqOrgOwnership())
 | 
								}, reqToken(), reqOrgOwnership())
 | 
				
			||||||
			m.Get("/activities/feeds", org.ListOrgActivityFeeds)
 | 
								m.Get("/activities/feeds", org.ListOrgActivityFeeds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m.Group("", func() {
 | 
				
			||||||
 | 
									m.Get("/list_blocked", org.ListBlockedUsers)
 | 
				
			||||||
 | 
									m.Group("", func() {
 | 
				
			||||||
 | 
										m.Put("/block/{username}", org.BlockUser)
 | 
				
			||||||
 | 
										m.Put("/unblock/{username}", org.UnblockUser)
 | 
				
			||||||
 | 
									}, context_service.UserAssignmentAPI())
 | 
				
			||||||
 | 
								}, reqToken(), reqOrgOwnership())
 | 
				
			||||||
		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 | 
							}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 | 
				
			||||||
		m.Group("/teams/{teamid}", func() {
 | 
							m.Group("/teams/{teamid}", func() {
 | 
				
			||||||
			m.Combo("").Get(reqToken(), org.GetTeam).
 | 
								m.Combo("").Get(reqToken(), org.GetTeam).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package org
 | 
					package org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	activities_model "code.gitea.io/gitea/models/activities"
 | 
						activities_model "code.gitea.io/gitea/models/activities"
 | 
				
			||||||
| 
						 | 
					@ -457,3 +458,99 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
 | 
						ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedUsers list the organization's blocked users.
 | 
				
			||||||
 | 
					func ListBlockedUsers(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /orgs/{org}/list_blocked organization orgListBlockedUsers
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: List the organization's blocked users
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the org
 | 
				
			||||||
 | 
						//   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
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/BlockedUserList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.ListUserBlockedUsers(ctx, ctx.ContextUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockUser blocks a user from the organization.
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /orgs/{org}/block/{username} organization orgBlockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Blocks a user from the organization
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the org
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: username of the user
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.ContextUser.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.BlockUser(ctx, ctx.Org.Organization.AsUser(), ctx.ContextUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser unblocks a user from the organization.
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /orgs/{org}/unblock/{username} organization orgUnblockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Unblock a user from the organization
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the org
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: username of the user
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.ContextUser.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.UnblockUser(ctx, ctx.Org.Organization.AsUser(), ctx.ContextUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +160,8 @@ func AddCollaborator(ctx *context.APIContext) {
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
	//   "422":
 | 
						//   "422":
 | 
				
			||||||
	//     "$ref": "#/responses/validationError"
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form := web.GetForm(ctx).(*api.AddCollaboratorOption)
 | 
						form := web.GetForm(ctx).(*api.AddCollaboratorOption)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -179,7 +181,11 @@ func AddCollaborator(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
 | 
						if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "AddCollaborator", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
| 
						 | 
					@ -686,7 +687,10 @@ func CreateIssue(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
						if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedByUser", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -378,7 +378,11 @@ func CreateIssueComment(ctx *context.APIContext) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
						comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,11 +8,13 @@ import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/v1/utils"
 | 
						"code.gitea.io/gitea/routers/api/v1/utils"
 | 
				
			||||||
	"code.gitea.io/gitea/services/convert"
 | 
						"code.gitea.io/gitea/services/convert"
 | 
				
			||||||
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetIssueCommentReactions list reactions of a comment from an issue
 | 
					// GetIssueCommentReactions list reactions of a comment from an issue
 | 
				
			||||||
| 
						 | 
					@ -202,9 +204,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if isCreateType {
 | 
						if isCreateType {
 | 
				
			||||||
		// PostIssueCommentReaction part
 | 
							// 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.Issue, comment, form.Reaction)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
				ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
									ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
				
			||||||
			} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
								} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
				
			||||||
				ctx.JSON(http.StatusOK, api.Reaction{
 | 
									ctx.JSON(http.StatusOK, api.Reaction{
 | 
				
			||||||
| 
						 | 
					@ -418,9 +420,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if isCreateType {
 | 
						if isCreateType {
 | 
				
			||||||
		// PostIssueReaction part
 | 
							// 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 err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
				ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
									ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
				
			||||||
			} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
								} else if issues_model.IsErrReactionAlreadyExist(err) {
 | 
				
			||||||
				ctx.JSON(http.StatusOK, api.Reaction{
 | 
									ctx.JSON(http.StatusOK, api.Reaction{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -420,7 +420,10 @@ func CreatePullRequest(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedByUser", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@
 | 
				
			||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,6 +108,11 @@ func Transfer(ctx *context.APIContext) {
 | 
				
			||||||
	oldFullname := ctx.Repo.Repository.FullName()
 | 
						oldFullname := ctx.Repo.Repository.FullName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
 | 
						if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "StartRepositoryTransfer", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if models.IsErrRepoTransferInProgress(err) {
 | 
							if models.IsErrRepoTransferInProgress(err) {
 | 
				
			||||||
			ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err)
 | 
								ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -414,3 +414,10 @@ type swaggerRepoNewIssuePinsAllowed struct {
 | 
				
			||||||
	// in:body
 | 
						// in:body
 | 
				
			||||||
	Body api.NewIssuePinsAllowed `json:"body"`
 | 
						Body api.NewIssuePinsAllowed `json:"body"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUserList
 | 
				
			||||||
 | 
					// swagger:response BlockedUserList
 | 
				
			||||||
 | 
					type swaggerBlockedUserList struct {
 | 
				
			||||||
 | 
						// in:body
 | 
				
			||||||
 | 
						Body []api.BlockedUser `json:"body"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
| 
						 | 
					@ -223,8 +224,14 @@ func Follow(ctx *context.APIContext) {
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
						if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedByUser", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
							ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	activities_model "code.gitea.io/gitea/models/activities"
 | 
						activities_model "code.gitea.io/gitea/models/activities"
 | 
				
			||||||
| 
						 | 
					@ -202,3 +203,84 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
 | 
						ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListBlockedUsers list the authenticated user's blocked users.
 | 
				
			||||||
 | 
					func ListBlockedUsers(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /user/list_blocked user userListBlockedUsers
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: List the authenticated user's blocked users
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// 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
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/BlockedUserList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.ListUserBlockedUsers(ctx, ctx.Doer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockUser blocks a user from the doer.
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /user/block/{username} user userBlockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Blocks a user from the doer.
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: username of the user
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.ContextUser.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.BlockUser(ctx, ctx.Doer, ctx.ContextUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser unblocks a user from the doer.
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /user/unblock/{username} user userUnblockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Unblocks a user from the doer.
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: username of the user
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.ContextUser.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						utils.UnblockUser(ctx, ctx.Doer, ctx.ContextUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										65
									
								
								routers/api/v1/utils/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								routers/api/v1/utils/block.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListUserBlockedUsers lists the blocked users of the provided doer.
 | 
				
			||||||
 | 
					func ListUserBlockedUsers(ctx *context.APIContext, doer *user_model.User) {
 | 
				
			||||||
 | 
						count, err := user_model.CountBlockedUsers(ctx, doer.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.InternalServerError(err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(ctx, doer.ID, GetListOptions(ctx))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.InternalServerError(err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiBlockedUsers := make([]*api.BlockedUser, len(blockedUsers))
 | 
				
			||||||
 | 
						for i, blockedUser := range blockedUsers {
 | 
				
			||||||
 | 
							apiBlockedUsers[i] = &api.BlockedUser{
 | 
				
			||||||
 | 
								BlockID: blockedUser.ID,
 | 
				
			||||||
 | 
								Created: blockedUser.CreatedUnix.AsTime(),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ctx.InternalServerError(err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.SetTotalCountHeader(count)
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, apiBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockUser blocks the blockUser from the doer.
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext, doer, blockUser *user_model.User) {
 | 
				
			||||||
 | 
						err := user_service.BlockUser(ctx, doer.ID, blockUser.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.InternalServerError(err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser unblocks the blockUser from the doer.
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext, doer, blockUser *user_model.User) {
 | 
				
			||||||
 | 
						err := user_model.UnblockUser(ctx, doer.ID, blockUser.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.InternalServerError(err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								routers/web/org/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								routers/web/org/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,79 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/utils"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tplBlockedUsers = "org/settings/blocked_users"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUsers renders the blocked users page.
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
 | 
				
			||||||
 | 
						ctx.Data["PageIsSettingsBlockedUsers"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID, db.ListOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("ListBlockedUsers", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["BlockedUsers"] = blockedUsers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUsersBlock blocks a particular user from the organization.
 | 
				
			||||||
 | 
					func BlockedUsersBlock(ctx *context.Context) {
 | 
				
			||||||
 | 
						uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
 | 
				
			||||||
 | 
						u, err := user_model.GetUserByName(ctx, uname)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetUserByName", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if u.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.ServerError("IsOrganization", fmt.Errorf("%s is an organization not a user", u.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := user_service.BlockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("BlockUser", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Flash.Success(ctx.Tr("settings.user_block_success"))
 | 
				
			||||||
 | 
						ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUsersUnblock unblocks a particular user from the organization.
 | 
				
			||||||
 | 
					func BlockedUsersUnblock(ctx *context.Context) {
 | 
				
			||||||
 | 
						u, err := user_model.GetUserByID(ctx, ctx.FormInt64("user_id"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetUserByID", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if u.IsOrganization() {
 | 
				
			||||||
 | 
							ctx.ServerError("IsOrganization", fmt.Errorf("%s is an organization not a user", u.Name))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := user_model.UnblockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("UnblockUser", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
 | 
				
			||||||
 | 
						ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1217,7 +1217,10 @@ func NewIssuePost(ctx *context.Context) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
 | 
						if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.RenderWithErr(ctx.Tr("repo.issues.blocked_by_user"), tplIssueNew, form)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -3071,7 +3074,11 @@ func NewComment(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 | 
						comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("CreateIssueComment", err)
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.issues.comment.blocked_by_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.ServerError("CreateIssueComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3211,7 +3218,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "react":
 | 
						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 err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
| 
						 | 
					@ -3313,7 +3320,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "react":
 | 
						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.Issue, comment, form.Content)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1414,7 +1414,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 | 
				
			||||||
	// instead of 500.
 | 
						// instead of 500.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.pulls.blocked_by_user"))
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Link)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		} else if git.IsErrPushRejected(err) {
 | 
							} else if git.IsErrPushRejected(err) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@
 | 
				
			||||||
package setting
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -102,7 +103,18 @@ func CollaborationPost(ctx *context.Context) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
 | 
						if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
 | 
				
			||||||
		ctx.ServerError("AddCollaborator", err)
 | 
							if !errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
								ctx.ServerError("AddCollaborator", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// To give an good error message, be precise on who has blocked who.
 | 
				
			||||||
 | 
							if blockedOurs := user_model.IsBlocked(ctx, ctx.Repo.Repository.OwnerID, u.ID); blockedOurs {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_blocked_our"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_blocked_them"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package setting
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
| 
						 | 
					@ -775,7 +776,9 @@ func SettingsPost(ctx *context.Context) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
 | 
							if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
 | 
				
			||||||
			if repo_model.IsErrRepoAlreadyExist(err) {
 | 
								if errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
 | 
									ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_blocked_doer"), tplSettingsOptions, nil)
 | 
				
			||||||
 | 
								} else if repo_model.IsErrRepoAlreadyExist(err) {
 | 
				
			||||||
				ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
 | 
									ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
 | 
				
			||||||
			} else if models.IsErrRepoTransferInProgress(err) {
 | 
								} else if models.IsErrRepoTransferInProgress(err) {
 | 
				
			||||||
				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
 | 
									ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,13 +103,15 @@ func TestCollaborationPost(t *testing.T) {
 | 
				
			||||||
	ctx.Req.Form.Set("collaborator", "user4")
 | 
						ctx.Req.Form.Set("collaborator", "user4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u := &user_model.User{
 | 
						u := &user_model.User{
 | 
				
			||||||
 | 
							ID:        2,
 | 
				
			||||||
		LowerName: "user2",
 | 
							LowerName: "user2",
 | 
				
			||||||
		Type:      user_model.UserTypeIndividual,
 | 
							Type:      user_model.UserTypeIndividual,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	re := &repo_model.Repository{
 | 
						re := &repo_model.Repository{
 | 
				
			||||||
		ID:    2,
 | 
							ID:      2,
 | 
				
			||||||
		Owner: u,
 | 
							Owner:   u,
 | 
				
			||||||
 | 
							OwnerID: u.ID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repo := &context.Repository{
 | 
						repo := &context.Repository{
 | 
				
			||||||
| 
						 | 
					@ -161,13 +163,15 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
 | 
				
			||||||
	ctx.Req.Form.Set("collaborator", "user4")
 | 
						ctx.Req.Form.Set("collaborator", "user4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	u := &user_model.User{
 | 
						u := &user_model.User{
 | 
				
			||||||
 | 
							ID:        2,
 | 
				
			||||||
		LowerName: "user2",
 | 
							LowerName: "user2",
 | 
				
			||||||
		Type:      user_model.UserTypeIndividual,
 | 
							Type:      user_model.UserTypeIndividual,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	re := &repo_model.Repository{
 | 
						re := &repo_model.Repository{
 | 
				
			||||||
		ID:    2,
 | 
							ID:      2,
 | 
				
			||||||
		Owner: u,
 | 
							Owner:   u,
 | 
				
			||||||
 | 
							OwnerID: u.ID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repo := &context.Repository{
 | 
						repo := &context.Repository{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 | 
				
			||||||
	prepareContextForCommonProfile(ctx)
 | 
						prepareContextForCommonProfile(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
						ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
						ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 | 
						ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 | 
				
			||||||
	ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
 | 
						ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,7 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/routers/web/feed"
 | 
						"code.gitea.io/gitea/routers/web/feed"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/web/org"
 | 
						"code.gitea.io/gitea/routers/web/org"
 | 
				
			||||||
	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
						shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
 | 
					// OwnerProfile render profile page for a user or a organization (aka, repo owner)
 | 
				
			||||||
| 
						 | 
					@ -290,16 +292,45 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGi
 | 
				
			||||||
// Action response for follow/unfollow user request
 | 
					// Action response for follow/unfollow user request
 | 
				
			||||||
func Action(ctx *context.Context) {
 | 
					func Action(ctx *context.Context) {
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	switch ctx.FormString("action") {
 | 
						var redirectViaJSON bool
 | 
				
			||||||
 | 
						action := ctx.FormString("action")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.ContextUser.IsOrganization() && (action == "block" || action == "unblock") {
 | 
				
			||||||
 | 
							log.Error("Cannot perform this action on an organization %q", ctx.FormString("action"))
 | 
				
			||||||
 | 
							ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch action {
 | 
				
			||||||
	case "follow":
 | 
						case "follow":
 | 
				
			||||||
		err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
							err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	case "unfollow":
 | 
						case "unfollow":
 | 
				
			||||||
		err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
							err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
						case "block":
 | 
				
			||||||
 | 
							err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
 | 
							redirectViaJSON = true
 | 
				
			||||||
 | 
						case "unblock":
 | 
				
			||||||
 | 
							err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
 | 
							if !errors.Is(err, user_model.ErrBlockedByUser) {
 | 
				
			||||||
		ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 | 
								log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
 | 
				
			||||||
 | 
								ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ctx.ContextUser.IsOrganization() {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if redirectViaJSON {
 | 
				
			||||||
 | 
							ctx.JSON(http.StatusOK, map[string]interface{}{
 | 
				
			||||||
 | 
								"redirect": ctx.ContextUser.HomeLink(),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.JSONOK()
 | 
						ctx.JSONOK()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										46
									
								
								routers/web/user/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								routers/web/user/setting/blocked_users.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockedUsers render the blocked users list page.
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
 | 
				
			||||||
 | 
						ctx.Data["PageIsBlockedUsers"] = true
 | 
				
			||||||
 | 
						ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/blocked_users"
 | 
				
			||||||
 | 
						ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/blocked_users"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Doer.ID, db.ListOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("ListBlockedUsers", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["BlockedUsers"] = blockedUsers
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnblockUser unblocks a particular user for the doer.
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.Context) {
 | 
				
			||||||
 | 
						if err := user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.FormInt64("user_id")); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("UnblockUser", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
 | 
				
			||||||
 | 
						ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -631,6 +631,11 @@ func registerRoutes(m *web.Route) {
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			addWebhookEditRoutes()
 | 
								addWebhookEditRoutes()
 | 
				
			||||||
		}, webhooksEnabled)
 | 
							}, webhooksEnabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.Group("/blocked_users", func() {
 | 
				
			||||||
 | 
								m.Get("", user_setting.BlockedUsers)
 | 
				
			||||||
 | 
								m.Post("/unblock", user_setting.UnblockUser)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
						}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m.Group("/user", func() {
 | 
						m.Group("/user", func() {
 | 
				
			||||||
| 
						 | 
					@ -888,6 +893,12 @@ func registerRoutes(m *web.Route) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				m.Methods("GET,POST", "/delete", org.SettingsDelete)
 | 
									m.Methods("GET,POST", "/delete", org.SettingsDelete)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									m.Group("/blocked_users", func() {
 | 
				
			||||||
 | 
										m.Get("", org_setting.BlockedUsers)
 | 
				
			||||||
 | 
										m.Post("/block", org_setting.BlockedUsersBlock)
 | 
				
			||||||
 | 
										m.Post("/unblock", org_setting.BlockedUsersUnblock)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				m.Group("/packages", func() {
 | 
									m.Group("/packages", func() {
 | 
				
			||||||
					m.Get("", org.Packages)
 | 
										m.Get("", org.Packages)
 | 
				
			||||||
					m.Group("/rules", func() {
 | 
										m.Group("/rules", func() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,11 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateIssueComment creates a plain issue comment.
 | 
					// 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) {
 | 
					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) {
 | 
				
			||||||
 | 
						// Check if doer is blocked by the poster of the issue.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, issue.PosterID, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
						comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
				
			||||||
		Type:        issues_model.CommentTypeComment,
 | 
							Type:        issues_model.CommentTypeComment,
 | 
				
			||||||
		Doer:        doer,
 | 
							Doer:        doer,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,11 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewIssue creates new issue with labels for repository.
 | 
					// 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 {
 | 
					func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
 | 
				
			||||||
 | 
						// Check if the user is not blocked by the repo's owner.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, repo.OwnerID, issue.PosterID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 | 
						if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								services/issue/reaction.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/issue/reaction.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo 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 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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the doer is blocked by the issue's poster or repository owner.
 | 
				
			||||||
 | 
						if user_model.IsBlockedMultiple(ctx, []int64{issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:    content,
 | 
				
			||||||
 | 
							DoerID:  doer.ID,
 | 
				
			||||||
 | 
							IssueID: issue.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateCommentReaction creates a reaction on comment.
 | 
				
			||||||
 | 
					func CreateCommentReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the doer is blocked by the issue's poster, the comment's poster or repository owner.
 | 
				
			||||||
 | 
						if user_model.IsBlockedMultiple(ctx, []int64{comment.PosterID, issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:      content,
 | 
				
			||||||
 | 
							DoerID:    doer.ID,
 | 
				
			||||||
 | 
							IssueID:   issue.ID,
 | 
				
			||||||
 | 
							CommentID: comment.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,11 @@ var pullWorkingPool = sync.NewExclusivePool()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewPullRequest creates new pull request with labels for repository.
 | 
					// 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 {
 | 
					func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
 | 
				
			||||||
 | 
						// Check if the doer is not blocked by the repository's owner.
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, repo.OwnerID, issue.PosterID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 | 
						prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if !git_model.IsErrBranchNotExist(err) {
 | 
							if !git_model.IsErrBranchNotExist(err) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,6 +85,10 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo
 | 
				
			||||||
// StartRepositoryTransfer transfer a repo from one owner to a new one.
 | 
					// StartRepositoryTransfer transfer a repo from one owner to a new one.
 | 
				
			||||||
// it make repository into pending transfer state, if doer can not create repo for new owner.
 | 
					// it make repository into pending transfer state, if doer can not create repo for new owner.
 | 
				
			||||||
func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
 | 
					func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
 | 
				
			||||||
 | 
						if user_model.IsBlocked(ctx, newOwner.ID, doer.ID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedByUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil {
 | 
						if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ func TestStartRepositoryTransferSetPermission(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
				
			||||||
	recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
						recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
 | 
				
			||||||
	repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 | 
						repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	hasAccess, err := access_model.HasAccess(db.DefaultContext, recipient.ID, repo)
 | 
						hasAccess, err := access_model.HasAccess(db.DefaultContext, recipient.ID, repo)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										95
									
								
								services/user/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								services/user/block.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,95 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model "code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BlockUser adds a blocked user entry for userID to block blockID.
 | 
				
			||||||
 | 
					// TODO: Figure out if instance admins should be immune to blocking.
 | 
				
			||||||
 | 
					// TODO: Add more mechanism like removing blocked user as collaborator on
 | 
				
			||||||
 | 
					// repositories where the user is an owner.
 | 
				
			||||||
 | 
					func BlockUser(ctx context.Context, userID, blockID int64) error {
 | 
				
			||||||
 | 
						if userID == blockID || user_model.IsBlocked(ctx, userID, blockID) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer committer.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the blocked user entry.
 | 
				
			||||||
 | 
						_, err = db.GetEngine(ctx).Insert(&user_model.BlockedUser{UserID: userID, BlockID: blockID})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unfollow the user from the block's perspective.
 | 
				
			||||||
 | 
						err = user_model.UnfollowUser(ctx, blockID, userID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unfollow the user from the doer's perspective.
 | 
				
			||||||
 | 
						err = user_model.UnfollowUser(ctx, userID, blockID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Blocked user unwatch all repository owned by the doer.
 | 
				
			||||||
 | 
						repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(ctx, blockID, userID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = repo_model.UnwatchRepos(ctx, blockID, repoIDs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove blocked user as collaborator from repositories the user owns as an
 | 
				
			||||||
 | 
						// individual.
 | 
				
			||||||
 | 
						collabsID, err := repo_model.GetCollaboratorWithUser(ctx, userID, blockID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = db.GetEngine(ctx).In("id", collabsID).Delete(&repo_model.Collaboration{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove pending repository transfers, and set the status on those repository
 | 
				
			||||||
 | 
						// back to ready.
 | 
				
			||||||
 | 
						pendingTransfersIDs, err := model.GetPendingTransferIDs(ctx, userID, blockID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use a subquery instead of a JOIN, because not every database supports JOIN
 | 
				
			||||||
 | 
						// on a UPDATE query.
 | 
				
			||||||
 | 
						_, err = db.GetEngine(ctx).Table("repository").
 | 
				
			||||||
 | 
							In("id", builder.Select("repo_id").From("repo_transfer").Where(builder.In("id", pendingTransfersIDs))).
 | 
				
			||||||
 | 
							Cols("status").
 | 
				
			||||||
 | 
							Update(&repo_model.Repository{Status: repo_model.RepositoryReady})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = db.GetEngine(ctx).In("id", pendingTransfersIDs).Delete(&model.RepoTransfer{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return committer.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										91
									
								
								services/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								services/user/block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,91 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model "code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"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"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestBlockUser will ensure that when you block a user, certain actions have
 | 
				
			||||||
 | 
					// been taken, like unfollowing each other etc.
 | 
				
			||||||
 | 
					func TestBlockUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Follow", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer user_model.UnblockUser(db.DefaultContext, doer.ID, blockedUser.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Follow each other.
 | 
				
			||||||
 | 
							assert.NoError(t, user_model.FollowUser(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
							assert.NoError(t, user_model.FollowUser(db.DefaultContext, blockedUser.ID, doer.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Ensure they aren't following each other anymore.
 | 
				
			||||||
 | 
							assert.False(t, user_model.IsFollowing(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
							assert.False(t, user_model.IsFollowing(db.DefaultContext, blockedUser.ID, doer.ID))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Watch", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer user_model.UnblockUser(db.DefaultContext, doer.ID, blockedUser.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Blocked user watch repository of doer.
 | 
				
			||||||
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: doer.ID})
 | 
				
			||||||
 | 
							assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, blockedUser.ID, repo.ID, true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Ensure blocked user isn't following doer's repository.
 | 
				
			||||||
 | 
							assert.False(t, repo_model.IsWatching(db.DefaultContext, blockedUser.ID, repo.ID))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Collaboration", func(t *testing.T) {
 | 
				
			||||||
 | 
							doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
 | 
				
			||||||
 | 
							blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 18})
 | 
				
			||||||
 | 
							repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22, OwnerID: doer.ID})
 | 
				
			||||||
 | 
							repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21, OwnerID: doer.ID})
 | 
				
			||||||
 | 
							defer user_model.UnblockUser(db.DefaultContext, doer.ID, blockedUser.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isBlockedUserCollab := func(repo *repo_model.Repository) bool {
 | 
				
			||||||
 | 
								isCollaborator, err := repo_model.IsCollaborator(db.DefaultContext, repo.ID, blockedUser.ID)
 | 
				
			||||||
 | 
								assert.NoError(t, err)
 | 
				
			||||||
 | 
								return isCollaborator
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.True(t, isBlockedUserCollab(repo1))
 | 
				
			||||||
 | 
							assert.True(t, isBlockedUserCollab(repo2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, isBlockedUserCollab(repo1))
 | 
				
			||||||
 | 
							assert.False(t, isBlockedUserCollab(repo2))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Pending transfers", func(t *testing.T) {
 | 
				
			||||||
 | 
							doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
							blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
				
			||||||
 | 
							defer user_model.UnblockUser(db.DefaultContext, doer.ID, blockedUser.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertExistsIf(t, true, &repo_model.Repository{ID: 3, OwnerID: blockedUser.ID, Status: repo_model.RepositoryPendingTransfer})
 | 
				
			||||||
 | 
							unittest.AssertExistsIf(t, true, &model.RepoTransfer{ID: 1, RecipientID: doer.ID, DoerID: blockedUser.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertExistsIf(t, false, &model.RepoTransfer{ID: 1, RecipientID: doer.ID, DoerID: blockedUser.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Don't use AssertExistsIf, as it doesn't include the zero values in the condition such as `repo_model.RepositoryReady`.
 | 
				
			||||||
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3, OwnerID: blockedUser.ID})
 | 
				
			||||||
 | 
							assert.Equal(t, repo_model.RepositoryReady, repo.Status)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@ import (
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"xorm.io/builder"
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -92,6 +93,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 | 
				
			||||||
		&pull_model.ReviewState{UserID: u.ID},
 | 
							&pull_model.ReviewState{UserID: u.ID},
 | 
				
			||||||
		&user_model.Redirect{RedirectUserID: u.ID},
 | 
							&user_model.Redirect{RedirectUserID: u.ID},
 | 
				
			||||||
		&actions_model.ActionRunner{OwnerID: u.ID},
 | 
							&actions_model.ActionRunner{OwnerID: u.ID},
 | 
				
			||||||
 | 
							&user_model.BlockedUser{BlockID: u.ID},
 | 
				
			||||||
 | 
							&user_model.BlockedUser{UserID: u.ID},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return fmt.Errorf("deleteBeans: %w", err)
 | 
							return fmt.Errorf("deleteBeans: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -127,6 +130,31 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ***** START: Issues *****
 | 
				
			||||||
 | 
						if purge {
 | 
				
			||||||
 | 
							const batchSize = 50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								issues := make([]*issues_model.Issue, 0, batchSize)
 | 
				
			||||||
 | 
								if err = e.Where("poster_id=?", u.ID).Limit(batchSize, 0).Find(&issues); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(issues) == 0 {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for _, issue := range issues {
 | 
				
			||||||
 | 
									// NOTE: Don't open git repositories just to remove the reference data,
 | 
				
			||||||
 | 
									// `git gc` is able to remove that reference which is run as a cron job
 | 
				
			||||||
 | 
									// by default. Also use the deleted user as doer to delete the issue.
 | 
				
			||||||
 | 
									if err = issue_service.DeleteIssue(ctx, u, nil, issue); err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// ***** END: Issues *****
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ***** START: Branch Protections *****
 | 
						// ***** START: Branch Protections *****
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		const batchSize = 50
 | 
							const batchSize = 50
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,10 @@
 | 
				
			||||||
{{template "base/head" .}}
 | 
					{{template "base/head" .}}
 | 
				
			||||||
<div role="main" aria-label="{{.Title}}" class="page-content organization profile">
 | 
					<div role="main" aria-label="{{.Title}}" class="page-content organization profile">
 | 
				
			||||||
 | 
						{{if .Flash}}
 | 
				
			||||||
 | 
							<div class="ui container gt-mb-5">
 | 
				
			||||||
 | 
								{{template "base/alert" .}}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{{end}}
 | 
				
			||||||
	<div class="ui container gt-df">
 | 
						<div class="ui container gt-df">
 | 
				
			||||||
		{{ctx.AvatarUtils.Avatar .Org 140 "org-avatar"}}
 | 
							{{ctx.AvatarUtils.Avatar .Org 140 "org-avatar"}}
 | 
				
			||||||
		<div id="org-info">
 | 
							<div id="org-info">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								templates/org/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								templates/org/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}}
 | 
				
			||||||
 | 
					<div class="org-setting-content">
 | 
				
			||||||
 | 
						<div class="ui attached segment">
 | 
				
			||||||
 | 
							<form class="ui form ignore-dirty gt-df gt-fw gt-gap-3" action="{{$.Link}}/block" method="post">
 | 
				
			||||||
 | 
								{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
								<input type="hidden" name="uid" value="">
 | 
				
			||||||
 | 
								<div class="ui left">
 | 
				
			||||||
 | 
									<div id="search-user-box" class="ui search">
 | 
				
			||||||
 | 
										<div class="ui input">
 | 
				
			||||||
 | 
											<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<button type="submit" class="ui red button">{{ctx.Locale.Tr "user.block"}}</button>
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="ui attached segment">
 | 
				
			||||||
 | 
							{{template "shared/blocked_users_list" .}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "org/settings/layout_footer" .}}
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,9 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</details>
 | 
							</details>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
 | 
								<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
 | 
				
			||||||
 | 
								{{ctx.Locale.Tr "settings.blocked_users"}}
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
		<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
 | 
							<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
 | 
				
			||||||
			{{ctx.Locale.Tr "org.settings.delete"}}
 | 
								{{ctx.Locale.Tr "org.settings.delete"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								templates/shared/blocked_users_list.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								templates/shared/blocked_users_list.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					<div class="flex-list">
 | 
				
			||||||
 | 
						{{range .BlockedUsers}}
 | 
				
			||||||
 | 
							<div class="flex-item flex-item-center">
 | 
				
			||||||
 | 
								<div class="flex-item-leading">
 | 
				
			||||||
 | 
									{{ctx.AvatarUtils.Avatar . 48}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="flex-item-main">
 | 
				
			||||||
 | 
									<div class="flex-item-title">
 | 
				
			||||||
 | 
										{{template "shared/user/name" .}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="flex-item-body">
 | 
				
			||||||
 | 
										<span>{{ctx.Locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</span>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="flex-item-trailing">
 | 
				
			||||||
 | 
									<form action="{{$.Link}}/unblock" method="post">
 | 
				
			||||||
 | 
										{{$.CsrfTokenHtml}}
 | 
				
			||||||
 | 
										<input type="hidden" name="user_id" value="{{.ID}}">
 | 
				
			||||||
 | 
										<button class="ui red button">{{ctx.Locale.Tr "user.unblock"}}</button>
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{{else}}
 | 
				
			||||||
 | 
							<div class="flex-item">
 | 
				
			||||||
 | 
								<span class="text grey italic">{{ctx.Locale.Tr "settings.blocked_users_none"}}</span>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{{end}}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -117,6 +117,18 @@
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
			</li>
 | 
								</li>
 | 
				
			||||||
 | 
								<li class="block">
 | 
				
			||||||
 | 
									{{if $.IsBlocked}}
 | 
				
			||||||
 | 
										<button class="ui basic red button link-action" data-url="{{.ContextUser.HomeLink}}?action=unblock&redirect_to={{$.Link}}">
 | 
				
			||||||
 | 
											{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unblock"}}
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									{{else}}
 | 
				
			||||||
 | 
										<button type="submit" class="ui basic orange button delete-button"
 | 
				
			||||||
 | 
										data-modal-id="block-user" data-url="{{.ContextUser.HomeLink}}?action=block">
 | 
				
			||||||
 | 
											{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block"}}
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								</li>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
		</ul>
 | 
							</ul>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										243
									
								
								templates/swagger/v1_json.tmpl
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										243
									
								
								templates/swagger/v1_json.tmpl
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -1806,6 +1806,45 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/orgs/{org}/block/{username}": {
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Blocks a user from the organization",
 | 
				
			||||||
 | 
					        "operationId": "orgBlockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the org",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "username of the user",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/orgs/{org}/hooks": {
 | 
					    "/orgs/{org}/hooks": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -2200,6 +2239,44 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/orgs/{org}/list_blocked": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "List the organization's blocked users",
 | 
				
			||||||
 | 
					        "operationId": "orgListBlockedUsers",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the org",
 | 
				
			||||||
 | 
					            "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/BlockedUserList"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/orgs/{org}/members": {
 | 
					    "/orgs/{org}/members": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -2691,6 +2768,45 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/orgs/{org}/unblock/{username}": {
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Unblock a user from the organization",
 | 
				
			||||||
 | 
					        "operationId": "orgUnblockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the org",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "username of the user",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/packages/{owner}": {
 | 
					    "/packages/{owner}": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -4179,6 +4295,9 @@
 | 
				
			||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
| 
						 | 
					@ -14739,6 +14858,38 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/user/block/{username}": {
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Blocks a user from the doer.",
 | 
				
			||||||
 | 
					        "operationId": "userBlockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "username of the user",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/user/emails": {
 | 
					    "/user/emails": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -14916,6 +15067,9 @@
 | 
				
			||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
| 
						 | 
					@ -15391,6 +15545,37 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/user/list_blocked": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "List the authenticated user's blocked users",
 | 
				
			||||||
 | 
					        "operationId": "userListBlockedUsers",
 | 
				
			||||||
 | 
					        "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/BlockedUserList"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/user/orgs": {
 | 
					    "/user/orgs": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -15801,6 +15986,38 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/user/unblock/{username}": {
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Unblocks a user from the doer.",
 | 
				
			||||||
 | 
					        "operationId": "userUnblockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "username of the user",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/users/search": {
 | 
					    "/users/search": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
| 
						 | 
					@ -16764,6 +16981,23 @@
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "BlockedUser": {
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "title": "BlockedUser represents a blocked user.",
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "block_id": {
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "format": "int64",
 | 
				
			||||||
 | 
					          "x-go-name": "BlockID"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "created_at": {
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "format": "date-time",
 | 
				
			||||||
 | 
					          "x-go-name": "Created"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Branch": {
 | 
					    "Branch": {
 | 
				
			||||||
      "description": "Branch represents a repository branch",
 | 
					      "description": "Branch represents a repository branch",
 | 
				
			||||||
      "type": "object",
 | 
					      "type": "object",
 | 
				
			||||||
| 
						 | 
					@ -23084,6 +23318,15 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "BlockedUserList": {
 | 
				
			||||||
 | 
					      "description": "BlockedUserList",
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "array",
 | 
				
			||||||
 | 
					        "items": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/BlockedUser"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Branch": {
 | 
					    "Branch": {
 | 
				
			||||||
      "description": "Branch",
 | 
					      "description": "Branch",
 | 
				
			||||||
      "schema": {
 | 
					      "schema": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
{{template "base/head" .}}
 | 
					{{template "base/head" .}}
 | 
				
			||||||
<div role="main" aria-label="{{.Title}}" class="page-content user profile">
 | 
					<div role="main" aria-label="{{.Title}}" class="page-content user profile">
 | 
				
			||||||
	<div class="ui container">
 | 
						<div class="ui container">
 | 
				
			||||||
 | 
							{{template "base/alert" .}}
 | 
				
			||||||
		<div class="ui stackable grid">
 | 
							<div class="ui stackable grid">
 | 
				
			||||||
			<div class="ui four wide column">
 | 
								<div class="ui four wide column">
 | 
				
			||||||
				{{template "shared/user/profile_big_avatar" .}}
 | 
									{{template "shared/user/profile_big_avatar" .}}
 | 
				
			||||||
| 
						 | 
					@ -39,4 +40,20 @@
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="ui g-modal-confirm delete modal" id="block-user">
 | 
				
			||||||
 | 
						<div class="header">
 | 
				
			||||||
 | 
							{{ctx.Locale.Tr "user.block_user"}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<p>{{ctx.Locale.Tr "user.block_user.detail"}}</p>
 | 
				
			||||||
 | 
							<ul>
 | 
				
			||||||
 | 
								<li>{{ctx.Locale.Tr "user.block_user.detail_1"}}</li>
 | 
				
			||||||
 | 
								<li>{{ctx.Locale.Tr "user.block_user.detail_2"}}</li>
 | 
				
			||||||
 | 
								<li>{{ctx.Locale.Tr "user.block_user.detail_3"}}</li>
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{{template "base/modal_actions_confirm" .}}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{template "base/footer" .}}
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								templates/user/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								templates/user/settings/blocked_users.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked-users")}}
 | 
				
			||||||
 | 
					<div class="user-setting-content">
 | 
				
			||||||
 | 
						<h4 class="ui top attached header">
 | 
				
			||||||
 | 
							{{ctx.Locale.Tr "settings.blocked_users"}}
 | 
				
			||||||
 | 
						</h4>
 | 
				
			||||||
 | 
						<div class="ui attached segment">
 | 
				
			||||||
 | 
							{{template "shared/blocked_users_list" .}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "user/settings/layout_footer" .}}
 | 
				
			||||||
| 
						 | 
					@ -51,5 +51,8 @@
 | 
				
			||||||
		<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
 | 
							<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
 | 
				
			||||||
			{{ctx.Locale.Tr "settings.repos"}}
 | 
								{{ctx.Locale.Tr "settings.repos"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
 | 
							<a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
 | 
				
			||||||
 | 
								{{ctx.Locale.Tr "settings.blocked_users"}}
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,10 +4,12 @@
 | 
				
			||||||
package integration
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/tests"
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
| 
						 | 
					@ -68,16 +70,22 @@ func makeRequest(t *testing.T, formData user_model.User, headerCode int) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestAdminDeleteUser(t *testing.T) {
 | 
					func TestAdminDeleteUser(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.AddFixtures("tests/integration/fixtures/TestAdminDeleteUser/")()
 | 
				
			||||||
	defer tests.PrepareTestEnv(t)()
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	session := loginUser(t, "user1")
 | 
						session := loginUser(t, "user1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	csrf := GetCSRF(t, session, "/admin/users/8/edit")
 | 
						userID := int64(1000)
 | 
				
			||||||
	req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{PosterID: userID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						csrf := GetCSRF(t, session, fmt.Sprintf("/admin/users/%d/edit", userID))
 | 
				
			||||||
 | 
						req := NewRequestWithValues(t, "POST", fmt.Sprintf("/admin/users/%d/delete", userID), map[string]string{
 | 
				
			||||||
		"_csrf": csrf,
 | 
							"_csrf": csrf,
 | 
				
			||||||
 | 
							"purge": "true",
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	session.MakeRequest(t, req, http.StatusSeeOther)
 | 
						session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assertUserDeleted(t, 8)
 | 
						assertUserDeleted(t, userID, true)
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										228
									
								
								tests/integration/api_block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								tests/integration/api_block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,228 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth_model "code.gitea.io/gitea/models/auth"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAPIUserBlock(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := "user4"
 | 
				
			||||||
 | 
						session := loginUser(t, user)
 | 
				
			||||||
 | 
						token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("BlockUser", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/block/user2?token=%s", token))
 | 
				
			||||||
 | 
							MakeRequest(t, req, http.StatusNoContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("ListBlocked", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/list_blocked?token=%s", token))
 | 
				
			||||||
 | 
							resp := MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// One user just got blocked and the other one is defined in the fixtures.
 | 
				
			||||||
 | 
							assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var blockedUsers []api.BlockedUser
 | 
				
			||||||
 | 
							DecodeJSON(t, resp, &blockedUsers)
 | 
				
			||||||
 | 
							assert.Len(t, blockedUsers, 2)
 | 
				
			||||||
 | 
							assert.EqualValues(t, 1, blockedUsers[0].BlockID)
 | 
				
			||||||
 | 
							assert.EqualValues(t, 2, blockedUsers[1].BlockID)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("UnblockUser", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/unblock/user2?token=%s", token))
 | 
				
			||||||
 | 
							MakeRequest(t, req, http.StatusNoContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Organization as target", func(t *testing.T) {
 | 
				
			||||||
 | 
							org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26, Type: user_model.UserTypeOrganization})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Block", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/block/%s?token=%s", org.Name, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusUnprocessableEntity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 4, BlockID: org.ID})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Unblock", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/unblock/%s?token=%s", org.Name, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusUnprocessableEntity)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAPIOrgBlock(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := "user5"
 | 
				
			||||||
 | 
						org := "org6"
 | 
				
			||||||
 | 
						session := loginUser(t, user)
 | 
				
			||||||
 | 
						token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("BlockUser", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/user2?token=%s", org, token))
 | 
				
			||||||
 | 
							MakeRequest(t, req, http.StatusNoContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("ListBlocked", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/list_blocked?token=%s", org, token))
 | 
				
			||||||
 | 
							resp := MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var blockedUsers []api.BlockedUser
 | 
				
			||||||
 | 
							DecodeJSON(t, resp, &blockedUsers)
 | 
				
			||||||
 | 
							assert.Len(t, blockedUsers, 1)
 | 
				
			||||||
 | 
							assert.EqualValues(t, 2, blockedUsers[0].BlockID)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("UnblockUser", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/unblock/user2?token=%s", org, token))
 | 
				
			||||||
 | 
							MakeRequest(t, req, http.StatusNoContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Organization as target", func(t *testing.T) {
 | 
				
			||||||
 | 
							targetOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26, Type: user_model.UserTypeOrganization})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Block", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/%s?token=%s", org, targetOrg.Name, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusUnprocessableEntity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 4, BlockID: targetOrg.ID})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Unblock", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/unblock/%s?token=%s", org, targetOrg.Name, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusUnprocessableEntity)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Read scope token", func(t *testing.T) {
 | 
				
			||||||
 | 
							token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Write action", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/user2?token=%s", org, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Read action", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/list_blocked?token=%s", org, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Not as owner", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							org := "org3"
 | 
				
			||||||
 | 
							user := "user4" // Part of org team with write perms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							session := loginUser(t, user)
 | 
				
			||||||
 | 
							token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Block user", func(t *testing.T) {
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/user2?token=%s", org, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 3, BlockID: 2})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Unblock user", func(t *testing.T) {
 | 
				
			||||||
 | 
								req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/unblock/user2?token=%s", org, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("List blocked users", func(t *testing.T) {
 | 
				
			||||||
 | 
								req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/list_blocked?token=%s", org, token))
 | 
				
			||||||
 | 
								MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestAPIBlock_AddCollaborator ensures that the doer and blocked user cannot
 | 
				
			||||||
 | 
					// add each others as collaborators via the API.
 | 
				
			||||||
 | 
					func TestAPIBlock_AddCollaborator(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user1 := "user10"
 | 
				
			||||||
 | 
						user2 := "user2"
 | 
				
			||||||
 | 
						perm := "write"
 | 
				
			||||||
 | 
						collabOption := &api.AddCollaboratorOption{Permission: &perm}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// User1 blocks User2.
 | 
				
			||||||
 | 
						session := loginUser(t, user1)
 | 
				
			||||||
 | 
						token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/block/%s?token=%s", user2, token))
 | 
				
			||||||
 | 
						MakeRequest(t, req, http.StatusNoContent)
 | 
				
			||||||
 | 
						unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 10, BlockID: 2})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("BlockedUser Add Doer", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2})
 | 
				
			||||||
 | 
							session := loginUser(t, user2)
 | 
				
			||||||
 | 
							token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s?token=%s", user2, repo.Name, user1, token), collabOption)
 | 
				
			||||||
 | 
							session.MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Doer Add BlockedUser", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 7, OwnerID: 10})
 | 
				
			||||||
 | 
							session := loginUser(t, user1)
 | 
				
			||||||
 | 
							token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s?token=%s", user1, repo.Name, user2, token), collabOption)
 | 
				
			||||||
 | 
							session.MakeRequest(t, req, http.StatusForbidden)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,6 @@ func TestNodeinfo(t *testing.T) {
 | 
				
			||||||
		assert.Equal(t, "gitea", nodeinfo.Software.Name)
 | 
							assert.Equal(t, "gitea", nodeinfo.Software.Name)
 | 
				
			||||||
		assert.Equal(t, 25, nodeinfo.Usage.Users.Total)
 | 
							assert.Equal(t, 25, nodeinfo.Usage.Users.Total)
 | 
				
			||||||
		assert.Equal(t, 20, nodeinfo.Usage.LocalPosts)
 | 
							assert.Equal(t, 20, nodeinfo.Usage.LocalPosts)
 | 
				
			||||||
		assert.Equal(t, 2, nodeinfo.Usage.LocalComments)
 | 
							assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ func TestAPIFollow(t *testing.T) {
 | 
				
			||||||
	defer tests.PrepareTestEnv(t)()
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := "user4"
 | 
						user1 := "user4"
 | 
				
			||||||
	user2 := "user1"
 | 
						user2 := "user10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	session1 := loginUser(t, user1)
 | 
						session1 := loginUser(t, user1)
 | 
				
			||||||
	token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser)
 | 
						token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										388
									
								
								tests/integration/block_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								tests/integration/block_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,388 @@
 | 
				
			||||||
 | 
					// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issue_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						forgejo_context "code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/translation"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(t *testing.T, doer, blockedUser *user_model.User) {
 | 
				
			||||||
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
						req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
 | 
				
			||||||
 | 
							"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
 | 
				
			||||||
 | 
							"action": "block",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type redirect struct {
 | 
				
			||||||
 | 
							Redirect string `json:"redirect"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var respBody redirect
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
						assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect)
 | 
				
			||||||
 | 
						assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestBlockUser ensures that users can execute blocking related actions can
 | 
				
			||||||
 | 
					// happen under the correct conditions.
 | 
				
			||||||
 | 
					func TestBlockUser(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Block", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
							BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unblock user.
 | 
				
			||||||
 | 
						t.Run("Unblock", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
							req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
 | 
				
			||||||
 | 
								"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
 | 
				
			||||||
 | 
								"action": "unblock",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Organization as target", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
							targetOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Block", func(t *testing.T) {
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", "/"+targetOrg.Name, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":  GetCSRF(t, session, "/"+targetOrg.Name),
 | 
				
			||||||
 | 
									"action": "block",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								resp := session.MakeRequest(t, req, http.StatusBadRequest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.Contains(t, resp.Body.String(), "Action \\\"block\\\" failed")
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Unblock", func(t *testing.T) {
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", "/"+targetOrg.Name, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":  GetCSRF(t, session, "/"+targetOrg.Name),
 | 
				
			||||||
 | 
									"action": "unblock",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								resp := session.MakeRequest(t, req, http.StatusBadRequest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.Contains(t, resp.Body.String(), "Action \\\"unblock\\\" failed")
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestBlockUserFromOrganization ensures that an organisation can block and unblock an user.
 | 
				
			||||||
 | 
					func TestBlockUserFromOrganization(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17, Type: user_model.UserTypeOrganization})
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
 | 
				
			||||||
 | 
						session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Block user", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/block", map[string]string{
 | 
				
			||||||
 | 
								"_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
 | 
				
			||||||
 | 
								"uname": blockedUser.Name,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
							assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID}))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Unblock user", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/unblock", map[string]string{
 | 
				
			||||||
 | 
								"_csrf":   GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
 | 
				
			||||||
 | 
								"user_id": strconv.FormatInt(blockedUser.ID, 10),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Organization as target", func(t *testing.T) {
 | 
				
			||||||
 | 
							targetOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Block", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/block", map[string]string{
 | 
				
			||||||
 | 
									"_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
 | 
				
			||||||
 | 
									"uname": targetOrg.Name,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusInternalServerError)
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: targetOrg.ID})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Unblock", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/unblock", map[string]string{
 | 
				
			||||||
 | 
									"_csrf":   GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
 | 
				
			||||||
 | 
									"user_id": strconv.FormatInt(targetOrg.ID, 10),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusInternalServerError)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestBlockActions ensures that certain actions cannot be performed as a doer
 | 
				
			||||||
 | 
					// and as a blocked user and are handled cleanly after the blocking has taken
 | 
				
			||||||
 | 
					// place.
 | 
				
			||||||
 | 
					func TestBlockActions(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						blockedUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
 | 
				
			||||||
 | 
						repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: doer.ID})
 | 
				
			||||||
 | 
						repo7 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 7, OwnerID: blockedUser2.ID})
 | 
				
			||||||
 | 
						issue4 := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 4, RepoID: repo2.ID})
 | 
				
			||||||
 | 
						issue4URL := fmt.Sprintf("/%s/issues/%d", repo2.FullName(), issue4.Index)
 | 
				
			||||||
 | 
						// NOTE: Sessions shouldn't be shared, because in some situations flash
 | 
				
			||||||
 | 
						// messages are persistent and that would interfere with accurate test
 | 
				
			||||||
 | 
						// results.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser)
 | 
				
			||||||
 | 
						BlockUser(t, doer, blockedUser2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that issue creation on doer's ownen repositories are blocked.
 | 
				
			||||||
 | 
						t.Run("Issue creation", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
							link := fmt.Sprintf("%s/issues/new", repo2.FullName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithValues(t, "POST", link, map[string]string{
 | 
				
			||||||
 | 
								"_csrf":   GetCSRF(t, session, link),
 | 
				
			||||||
 | 
								"title":   "Title",
 | 
				
			||||||
 | 
								"content": "Hello!",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							htmlDoc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
							assert.Contains(t,
 | 
				
			||||||
 | 
								htmlDoc.doc.Find(".ui.negative.message").Text(),
 | 
				
			||||||
 | 
								translation.NewLocale("en-US").Tr("repo.issues.blocked_by_user"),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that comment creation on doer's owned repositories and doer's
 | 
				
			||||||
 | 
						// posted issues are blocked.
 | 
				
			||||||
 | 
						t.Run("Comment creation", func(t *testing.T) {
 | 
				
			||||||
 | 
							expectedFlash := "error%3DYou%2Bcannot%2Bcreate%2Ba%2Bcomment%2Bon%2Bthis%2Bissue%2Bbecause%2Byou%2Bare%2Bblocked%2Bby%2Bthe%2Brepository%2Bowner%2Bor%2Bthe%2Bposter%2Bof%2Bthe%2Bissue."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Blocked by repository owner", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", path.Join(issue4URL, "/comments"), map[string]string{
 | 
				
			||||||
 | 
									"_csrf":   GetCSRF(t, session, issue4URL),
 | 
				
			||||||
 | 
									"content": "Not a kind comment",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, expectedFlash, flashCookie.Value)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Blocked by issue poster", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								repo5 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
 | 
				
			||||||
 | 
								issue15 := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 15, RepoID: repo5.ID, PosterID: doer.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
								issueURL := fmt.Sprintf("/%s/%s/issues/%d", url.PathEscape(repo5.OwnerName), url.PathEscape(repo5.Name), issue15.Index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", path.Join(issueURL, "/comments"), map[string]string{
 | 
				
			||||||
 | 
									"_csrf":   GetCSRF(t, session, issueURL),
 | 
				
			||||||
 | 
									"content": "Not a kind comment",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, expectedFlash, flashCookie.Value)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that reactions on doer's owned issues and doer's owned comments are
 | 
				
			||||||
 | 
						// blocked.
 | 
				
			||||||
 | 
						t.Run("Add a reaction", func(t *testing.T) {
 | 
				
			||||||
 | 
							type reactionResponse struct {
 | 
				
			||||||
 | 
								Empty bool `json:"empty"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("On a issue", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", path.Join(issue4URL, "/reactions/react"), map[string]string{
 | 
				
			||||||
 | 
									"_csrf":   GetCSRF(t, session, issue4URL),
 | 
				
			||||||
 | 
									"content": "eyes",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var respBody reactionResponse
 | 
				
			||||||
 | 
								DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.EqualValues(t, true, respBody.Empty)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("On a comment", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{ID: 8, PosterID: doer.ID, IssueID: issue4.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/comments/%d/reactions/react", repo2.FullName(), comment.ID), map[string]string{
 | 
				
			||||||
 | 
									"_csrf":   GetCSRF(t, session, issue4URL),
 | 
				
			||||||
 | 
									"content": "eyes",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var respBody reactionResponse
 | 
				
			||||||
 | 
								DecodeJSON(t, resp, &respBody)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								assert.EqualValues(t, true, respBody.Empty)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that the doer and blocked user cannot follow each other.
 | 
				
			||||||
 | 
						t.Run("Follow", func(t *testing.T) {
 | 
				
			||||||
 | 
							// Sanity checks to make sure doing these tests are valid.
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Doer cannot follow blocked user.
 | 
				
			||||||
 | 
							t.Run("Doer follow blocked user", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
 | 
				
			||||||
 | 
									"action": "follow",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, "error%3DYou%2Bcannot%2Bfollow%2Bthis%2Buser%2Bbecause%2Byou%2Bhave%2Bblocked%2Bthis%2Buser%2Bor%2Bthis%2Buser%2Bhas%2Bblocked%2Byou.", flashCookie.Value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Assert it still doesn't exist.
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Blocked user cannot follow doer.
 | 
				
			||||||
 | 
							t.Run("Blocked user follow doer", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", "/"+doer.Name, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":  GetCSRF(t, session, "/"+doer.Name),
 | 
				
			||||||
 | 
									"action": "follow",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, "error%3DYou%2Bcannot%2Bfollow%2Bthis%2Buser%2Bbecause%2Byou%2Bhave%2Bblocked%2Bthis%2Buser%2Bor%2Bthis%2Buser%2Bhas%2Bblocked%2Byou.", flashCookie.Value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that the doer and blocked user cannot add each each other as collaborators.
 | 
				
			||||||
 | 
						t.Run("Add collaborator", func(t *testing.T) {
 | 
				
			||||||
 | 
							t.Run("Doer Add BlockedUser", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, doer.Name)
 | 
				
			||||||
 | 
								link := fmt.Sprintf("/%s/settings/collaboration", repo2.FullName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", link, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":        GetCSRF(t, session, link),
 | 
				
			||||||
 | 
									"collaborator": blockedUser2.Name,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, "error%3DCannot%2Badd%2Bthe%2Bcollaborator%252C%2Bbecause%2Bthe%2Brepository%2Bowner%2Bhas%2Bblocked%2Bthem.", flashCookie.Value)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("BlockedUser Add doer", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								session := loginUser(t, blockedUser2.Name)
 | 
				
			||||||
 | 
								link := fmt.Sprintf("/%s/settings/collaboration", repo7.FullName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := NewRequestWithValues(t, "POST", link, map[string]string{
 | 
				
			||||||
 | 
									"_csrf":        GetCSRF(t, session, link),
 | 
				
			||||||
 | 
									"collaborator": doer.Name,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
 | 
				
			||||||
 | 
								assert.NotNil(t, flashCookie)
 | 
				
			||||||
 | 
								assert.EqualValues(t, "error%3DCannot%2Badd%2Bthe%2Bcollaborator%252C%2Bbecause%2Bthey%2Bhave%2Bblocked%2Bthe%2Brepository%2Bowner.", flashCookie.Value)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensures that the blocked user cannot transfer a repository to the doer.
 | 
				
			||||||
 | 
						t.Run("Repository transfer", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							session := loginUser(t, blockedUser2.Name)
 | 
				
			||||||
 | 
							link := fmt.Sprintf("%s/settings", repo7.FullName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithValues(t, "POST", link, map[string]string{
 | 
				
			||||||
 | 
								"_csrf":          GetCSRF(t, session, link),
 | 
				
			||||||
 | 
								"action":         "transfer",
 | 
				
			||||||
 | 
								"repo_name":      repo7.FullName(),
 | 
				
			||||||
 | 
								"new_owner_name": doer.Name,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							htmlDoc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
							assert.Contains(t,
 | 
				
			||||||
 | 
								htmlDoc.doc.Find(".ui.negative.message").Text(),
 | 
				
			||||||
 | 
								translation.NewLocale("en-US").Tr("repo.settings.new_owner_blocked_doer"),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/tests"
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func assertUserDeleted(t *testing.T, userID int64) {
 | 
					func assertUserDeleted(t *testing.T, userID int64, purged bool) {
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
 | 
						unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: userID})
 | 
						unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: userID})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &user_model.Follow{FollowID: userID})
 | 
						unittest.AssertNotExistsBean(t, &user_model.Follow{FollowID: userID})
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,9 @@ func assertUserDeleted(t *testing.T, userID int64) {
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &issues_model.IssueUser{UID: userID})
 | 
						unittest.AssertNotExistsBean(t, &issues_model.IssueUser{UID: userID})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID})
 | 
						unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID})
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID})
 | 
				
			||||||
 | 
						if purged {
 | 
				
			||||||
 | 
							unittest.AssertNotExistsBean(t, &issues_model.Issue{PosterID: userID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestUserDeleteAccount(t *testing.T) {
 | 
					func TestUserDeleteAccount(t *testing.T) {
 | 
				
			||||||
| 
						 | 
					@ -40,7 +43,7 @@ func TestUserDeleteAccount(t *testing.T) {
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	session.MakeRequest(t, req, http.StatusSeeOther)
 | 
						session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assertUserDeleted(t, 8)
 | 
						assertUserDeleted(t, 8, false)
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								tests/integration/fixtures/TestAdminDeleteUser/issue.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tests/integration/fixtures/TestAdminDeleteUser/issue.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1000
 | 
				
			||||||
 | 
					  repo_id: 1000
 | 
				
			||||||
 | 
					  index: 2
 | 
				
			||||||
 | 
					  poster_id: 1000
 | 
				
			||||||
 | 
					  original_author_id: 0
 | 
				
			||||||
 | 
					  name: NAME
 | 
				
			||||||
 | 
					  content: content
 | 
				
			||||||
 | 
					  milestone_id: 0
 | 
				
			||||||
 | 
					  priority: 0
 | 
				
			||||||
 | 
					  is_closed: false
 | 
				
			||||||
 | 
					  is_pull: false
 | 
				
			||||||
 | 
					  num_comments: 0
 | 
				
			||||||
 | 
					  created_unix: 946684830
 | 
				
			||||||
 | 
					  updated_unix: 978307200
 | 
				
			||||||
 | 
					  is_locked: false
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  group_id: 1000
 | 
				
			||||||
 | 
					  max_index: 2
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1000
 | 
				
			||||||
 | 
					  owner_id: 1001
 | 
				
			||||||
 | 
					  owner_name: user1001
 | 
				
			||||||
 | 
					  lower_name: repo1000
 | 
				
			||||||
 | 
					  name: repo1000
 | 
				
			||||||
 | 
					  default_branch: master
 | 
				
			||||||
 | 
					  num_watches: 0
 | 
				
			||||||
 | 
					  num_stars: 0
 | 
				
			||||||
 | 
					  num_forks: 0
 | 
				
			||||||
 | 
					  num_issues: 1
 | 
				
			||||||
 | 
					  num_closed_issues: 0
 | 
				
			||||||
 | 
					  num_pulls: 0
 | 
				
			||||||
 | 
					  num_closed_pulls: 0
 | 
				
			||||||
 | 
					  num_milestones: 0
 | 
				
			||||||
 | 
					  num_closed_milestones: 0
 | 
				
			||||||
 | 
					  num_projects: 0
 | 
				
			||||||
 | 
					  num_closed_projects: 0
 | 
				
			||||||
 | 
					  is_private: false
 | 
				
			||||||
 | 
					  is_empty: false
 | 
				
			||||||
 | 
					  is_archived: false
 | 
				
			||||||
 | 
					  is_mirror: false
 | 
				
			||||||
 | 
					  status: 0
 | 
				
			||||||
 | 
					  is_fork: false
 | 
				
			||||||
 | 
					  fork_id: 0
 | 
				
			||||||
 | 
					  is_template: false
 | 
				
			||||||
 | 
					  template_id: 0
 | 
				
			||||||
 | 
					  size: 0
 | 
				
			||||||
 | 
					  is_fsck_enabled: true
 | 
				
			||||||
 | 
					  close_issues_via_commit_in_any_branch: false
 | 
				
			||||||
							
								
								
									
										73
									
								
								tests/integration/fixtures/TestAdminDeleteUser/user.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								tests/integration/fixtures/TestAdminDeleteUser/user.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1000
 | 
				
			||||||
 | 
					  lower_name: user1000
 | 
				
			||||||
 | 
					  name: user1000
 | 
				
			||||||
 | 
					  full_name: User Thousand
 | 
				
			||||||
 | 
					  email: user1000@example.com
 | 
				
			||||||
 | 
					  keep_email_private: false
 | 
				
			||||||
 | 
					  email_notifications_preference: enabled
 | 
				
			||||||
 | 
					  passwd: ZogKvWdyEx:password
 | 
				
			||||||
 | 
					  passwd_hash_algo: dummy
 | 
				
			||||||
 | 
					  must_change_password: false
 | 
				
			||||||
 | 
					  login_source: 0
 | 
				
			||||||
 | 
					  login_name: user1000
 | 
				
			||||||
 | 
					  type: 0
 | 
				
			||||||
 | 
					  salt: ZogKvWdyEx
 | 
				
			||||||
 | 
					  max_repo_creation: -1
 | 
				
			||||||
 | 
					  is_active: true
 | 
				
			||||||
 | 
					  is_admin: false
 | 
				
			||||||
 | 
					  is_restricted: false
 | 
				
			||||||
 | 
					  allow_git_hook: false
 | 
				
			||||||
 | 
					  allow_import_local: false
 | 
				
			||||||
 | 
					  allow_create_organization: true
 | 
				
			||||||
 | 
					  prohibit_login: false
 | 
				
			||||||
 | 
					  avatar: avatar1000
 | 
				
			||||||
 | 
					  avatar_email: user1000@example.com
 | 
				
			||||||
 | 
					  use_custom_avatar: false
 | 
				
			||||||
 | 
					  num_followers: 1
 | 
				
			||||||
 | 
					  num_following: 1
 | 
				
			||||||
 | 
					  num_stars: 0
 | 
				
			||||||
 | 
					  num_repos: 0
 | 
				
			||||||
 | 
					  num_teams: 0
 | 
				
			||||||
 | 
					  num_members: 0
 | 
				
			||||||
 | 
					  visibility: 0
 | 
				
			||||||
 | 
					  repo_admin_change_team_access: false
 | 
				
			||||||
 | 
					  theme: ""
 | 
				
			||||||
 | 
					  keep_activity_private: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1001
 | 
				
			||||||
 | 
					  lower_name: user1001
 | 
				
			||||||
 | 
					  name: user1001
 | 
				
			||||||
 | 
					  full_name: User 1001
 | 
				
			||||||
 | 
					  email: user1001@example.com
 | 
				
			||||||
 | 
					  keep_email_private: false
 | 
				
			||||||
 | 
					  email_notifications_preference: enabled
 | 
				
			||||||
 | 
					  passwd: ZogKvWdyEx:password
 | 
				
			||||||
 | 
					  passwd_hash_algo: dummy
 | 
				
			||||||
 | 
					  must_change_password: false
 | 
				
			||||||
 | 
					  login_source: 0
 | 
				
			||||||
 | 
					  login_name: user1001
 | 
				
			||||||
 | 
					  type: 0
 | 
				
			||||||
 | 
					  salt: ZogKvWdyEx
 | 
				
			||||||
 | 
					  max_repo_creation: -1
 | 
				
			||||||
 | 
					  is_active: true
 | 
				
			||||||
 | 
					  is_admin: false
 | 
				
			||||||
 | 
					  is_restricted: false
 | 
				
			||||||
 | 
					  allow_git_hook: false
 | 
				
			||||||
 | 
					  allow_import_local: false
 | 
				
			||||||
 | 
					  allow_create_organization: true
 | 
				
			||||||
 | 
					  prohibit_login: false
 | 
				
			||||||
 | 
					  avatar: avatar1001
 | 
				
			||||||
 | 
					  avatar_email: user1001@example.com
 | 
				
			||||||
 | 
					  use_custom_avatar: false
 | 
				
			||||||
 | 
					  num_followers: 0
 | 
				
			||||||
 | 
					  num_following: 0
 | 
				
			||||||
 | 
					  num_stars: 0
 | 
				
			||||||
 | 
					  num_repos: 1
 | 
				
			||||||
 | 
					  num_teams: 0
 | 
				
			||||||
 | 
					  num_members: 0
 | 
				
			||||||
 | 
					  visibility: 0
 | 
				
			||||||
 | 
					  repo_admin_change_team_access: false
 | 
				
			||||||
 | 
					  theme: ""
 | 
				
			||||||
 | 
					  keep_activity_private: false
 | 
				
			||||||
| 
						 | 
					@ -166,6 +166,22 @@
 | 
				
			||||||
  border-bottom: 1px solid var(--color-secondary);
 | 
					  border-bottom: 1px solid var(--color-secondary);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.organization.teams .repositories .item,
 | 
				
			||||||
 | 
					.organization.teams .members .item {
 | 
				
			||||||
 | 
					  padding: 10px 19px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.organization.teams .repositories .item:not(:last-child),
 | 
				
			||||||
 | 
					.organization.teams .members .item:not(:last-child) {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--color-secondary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.organization.teams .repositories .item .button,
 | 
				
			||||||
 | 
					.organization.teams .members .item .button {
 | 
				
			||||||
 | 
					  padding: 9px 10px;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.org-team-navbar .active.item {
 | 
					.org-team-navbar .active.item {
 | 
				
			||||||
  background: var(--color-box-body) !important;
 | 
					  background: var(--color-box-body) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,19 @@
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user.profile .ui.card .extra.content > ul > li .svg {
 | 
				
			||||||
 | 
					  margin-left: 1px;
 | 
				
			||||||
 | 
					  margin-right: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user.profile .ui.card .extra.content > ul > li.follow .ui.button,
 | 
				
			||||||
 | 
					.user.profile .ui.card .extra.content > ul > li.block .ui.button {
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.user.profile .ui.card #profile-avatar {
 | 
					.user.profile .ui.card #profile-avatar {
 | 
				
			||||||
  padding: 1rem 1rem 0.25rem;
 | 
					  padding: 1rem 1rem 0.25rem;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue