mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	Add user blocking (#29028)
Fixes #17453 This PR adds the abbility to block a user from a personal account or organization to restrict how the blocked user can interact with the blocker. The docs explain what's the consequence of blocking a user. Screenshots:    --------- Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
		
							
								
								
									
										56
									
								
								docs/content/usage/blocking-users.en-us.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								docs/content/usage/blocking-users.en-us.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					date: "2024-01-31T00:00:00+00:00"
 | 
				
			||||||
 | 
					title: "Blocking a user"
 | 
				
			||||||
 | 
					slug: "blocking-user"
 | 
				
			||||||
 | 
					sidebar_position: 25
 | 
				
			||||||
 | 
					toc: false
 | 
				
			||||||
 | 
					draft: false
 | 
				
			||||||
 | 
					aliases:
 | 
				
			||||||
 | 
					  - /en-us/webhooks
 | 
				
			||||||
 | 
					menu:
 | 
				
			||||||
 | 
					  sidebar:
 | 
				
			||||||
 | 
					    parent: "usage"
 | 
				
			||||||
 | 
					    name: "Blocking a user"
 | 
				
			||||||
 | 
					    sidebar_position: 30
 | 
				
			||||||
 | 
					    identifier: "blocking-user"
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Blocking a user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Gitea supports blocking of users to restrict how they can interact with you and your content.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can block a user in your account settings, from the user's profile or from comments created by the user.
 | 
				
			||||||
 | 
					The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
 | 
				
			||||||
 | 
					Organization owners can block anyone who is not a member of the organization too.
 | 
				
			||||||
 | 
					If a blocked user has admin permissions, they can still perform all actions even if blocked.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### When you block a user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- the user stops following you
 | 
				
			||||||
 | 
					- you stop following the user
 | 
				
			||||||
 | 
					- the user's stars are removed from your repositories
 | 
				
			||||||
 | 
					- your stars are removed from their repositories
 | 
				
			||||||
 | 
					- the user stops watching your repositories
 | 
				
			||||||
 | 
					- you stop watching their repositories
 | 
				
			||||||
 | 
					- the user's issue assignments are removed from your repositories
 | 
				
			||||||
 | 
					- your issue assignments are removed from their repositories
 | 
				
			||||||
 | 
					- the user is removed as a collaborator on your repositories
 | 
				
			||||||
 | 
					- you are removed as a collaborator on their repositories
 | 
				
			||||||
 | 
					- any pending repository transfers to or from the blocked user are canceled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### When you block a user, the user cannot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- follow you
 | 
				
			||||||
 | 
					- watch your repositories
 | 
				
			||||||
 | 
					- star your repositories
 | 
				
			||||||
 | 
					- fork your repositories
 | 
				
			||||||
 | 
					- transfer repositories to you
 | 
				
			||||||
 | 
					- open issues or pull requests on your repositories
 | 
				
			||||||
 | 
					- comment on issues or pull requests you've created
 | 
				
			||||||
 | 
					- comment on issues or pull requests on your repositories
 | 
				
			||||||
 | 
					- react to your comments on issues or pull requests
 | 
				
			||||||
 | 
					- react to comments on issues or pull requests on your repositories
 | 
				
			||||||
 | 
					- assign you to issues or pull requests
 | 
				
			||||||
 | 
					- add you as a collaborator on their repositories
 | 
				
			||||||
 | 
					- send you notifications by @mentioning your username
 | 
				
			||||||
 | 
					- be added as team member (if blocked by an organization)
 | 
				
			||||||
@@ -42,120 +42,132 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 8
 | 
					  id: 8
 | 
				
			||||||
  user_id: 15
 | 
					  user_id: 10
 | 
				
			||||||
  repo_id: 21
 | 
					  repo_id: 21
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 9
 | 
					  id: 9
 | 
				
			||||||
  user_id: 15
 | 
					  user_id: 10
 | 
				
			||||||
  repo_id: 22
 | 
					  repo_id: 32
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 10
 | 
					  id: 10
 | 
				
			||||||
  user_id: 15
 | 
					  user_id: 15
 | 
				
			||||||
 | 
					  repo_id: 21
 | 
				
			||||||
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 11
 | 
				
			||||||
 | 
					  user_id: 15
 | 
				
			||||||
 | 
					  repo_id: 22
 | 
				
			||||||
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 12
 | 
				
			||||||
 | 
					  user_id: 15
 | 
				
			||||||
  repo_id: 23
 | 
					  repo_id: 23
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 11
 | 
					  id: 13
 | 
				
			||||||
  user_id: 15
 | 
					  user_id: 15
 | 
				
			||||||
  repo_id: 24
 | 
					  repo_id: 24
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 12
 | 
					  id: 14
 | 
				
			||||||
  user_id: 15
 | 
					  user_id: 15
 | 
				
			||||||
  repo_id: 32
 | 
					  repo_id: 32
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 13
 | 
					  id: 15
 | 
				
			||||||
  user_id: 18
 | 
					  user_id: 18
 | 
				
			||||||
  repo_id: 21
 | 
					  repo_id: 21
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 14
 | 
					  id: 16
 | 
				
			||||||
  user_id: 18
 | 
					  user_id: 18
 | 
				
			||||||
  repo_id: 22
 | 
					  repo_id: 22
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 15
 | 
					  id: 17
 | 
				
			||||||
  user_id: 18
 | 
					  user_id: 18
 | 
				
			||||||
  repo_id: 23
 | 
					  repo_id: 23
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 16
 | 
					  id: 18
 | 
				
			||||||
  user_id: 18
 | 
					  user_id: 18
 | 
				
			||||||
  repo_id: 24
 | 
					  repo_id: 24
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 17
 | 
					  id: 19
 | 
				
			||||||
  user_id: 20
 | 
					  user_id: 20
 | 
				
			||||||
  repo_id: 24
 | 
					  repo_id: 24
 | 
				
			||||||
  mode: 1
 | 
					  mode: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 18
 | 
					  id: 20
 | 
				
			||||||
  user_id: 20
 | 
					  user_id: 20
 | 
				
			||||||
  repo_id: 27
 | 
					  repo_id: 27
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 19
 | 
					  id: 21
 | 
				
			||||||
  user_id: 20
 | 
					  user_id: 20
 | 
				
			||||||
  repo_id: 28
 | 
					  repo_id: 28
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 20
 | 
					  id: 22
 | 
				
			||||||
  user_id: 29
 | 
					  user_id: 29
 | 
				
			||||||
  repo_id: 4
 | 
					  repo_id: 4
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 21
 | 
					  id: 23
 | 
				
			||||||
  user_id: 29
 | 
					  user_id: 29
 | 
				
			||||||
  repo_id: 24
 | 
					  repo_id: 24
 | 
				
			||||||
  mode: 1
 | 
					  mode: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 22
 | 
					  id: 24
 | 
				
			||||||
  user_id: 31
 | 
					  user_id: 31
 | 
				
			||||||
  repo_id: 27
 | 
					  repo_id: 27
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 23
 | 
					  id: 25
 | 
				
			||||||
  user_id: 31
 | 
					  user_id: 31
 | 
				
			||||||
  repo_id: 28
 | 
					  repo_id: 28
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 24
 | 
					  id: 26
 | 
				
			||||||
  user_id: 38
 | 
					  user_id: 38
 | 
				
			||||||
  repo_id: 60
 | 
					  repo_id: 60
 | 
				
			||||||
  mode: 2
 | 
					  mode: 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 25
 | 
					  id: 27
 | 
				
			||||||
  user_id: 38
 | 
					  user_id: 38
 | 
				
			||||||
  repo_id: 61
 | 
					  repo_id: 61
 | 
				
			||||||
  mode: 1
 | 
					  mode: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 26
 | 
					  id: 28
 | 
				
			||||||
  user_id: 39
 | 
					  user_id: 39
 | 
				
			||||||
  repo_id: 61
 | 
					  repo_id: 61
 | 
				
			||||||
  mode: 1
 | 
					  mode: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 27
 | 
					  id: 29
 | 
				
			||||||
  user_id: 40
 | 
					  user_id: 40
 | 
				
			||||||
  repo_id: 61
 | 
					  repo_id: 61
 | 
				
			||||||
  mode: 4
 | 
					  mode: 4
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,3 +51,15 @@
 | 
				
			|||||||
  repo_id: 60
 | 
					  repo_id: 60
 | 
				
			||||||
  user_id: 38
 | 
					  user_id: 38
 | 
				
			||||||
  mode: 2 # write
 | 
					  mode: 2 # write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 10
 | 
				
			||||||
 | 
					  repo_id: 21
 | 
				
			||||||
 | 
					  user_id: 10
 | 
				
			||||||
 | 
					  mode: 2 # write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 11
 | 
				
			||||||
 | 
					  repo_id: 32
 | 
				
			||||||
 | 
					  user_id: 10
 | 
				
			||||||
 | 
					  mode: 2 # write
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,3 +14,7 @@
 | 
				
			|||||||
  id: 4
 | 
					  id: 4
 | 
				
			||||||
  assignee_id: 2
 | 
					  assignee_id: 2
 | 
				
			||||||
  issue_id: 17
 | 
					  issue_id: 17
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 5
 | 
				
			||||||
 | 
					  assignee_id: 10
 | 
				
			||||||
 | 
					  issue_id: 6
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,3 +5,19 @@
 | 
				
			|||||||
  repo_id: 3
 | 
					  repo_id: 3
 | 
				
			||||||
  created_unix: 1553610671
 | 
					  created_unix: 1553610671
 | 
				
			||||||
  updated_unix: 1553610671
 | 
					  updated_unix: 1553610671
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 2
 | 
				
			||||||
 | 
					  doer_id: 16
 | 
				
			||||||
 | 
					  recipient_id: 10
 | 
				
			||||||
 | 
					  repo_id: 21
 | 
				
			||||||
 | 
					  created_unix: 1553610671
 | 
				
			||||||
 | 
					  updated_unix: 1553610671
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  doer_id: 3
 | 
				
			||||||
 | 
					  recipient_id: 10
 | 
				
			||||||
 | 
					  repo_id: 32
 | 
				
			||||||
 | 
					  created_unix: 1553610671
 | 
				
			||||||
 | 
					  updated_unix: 1553610671
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -614,8 +614,8 @@
 | 
				
			|||||||
  owner_name: user16
 | 
					  owner_name: user16
 | 
				
			||||||
  lower_name: big_test_public_3
 | 
					  lower_name: big_test_public_3
 | 
				
			||||||
  name: big_test_public_3
 | 
					  name: big_test_public_3
 | 
				
			||||||
  num_watches: 0
 | 
					  num_watches: 1
 | 
				
			||||||
  num_stars: 0
 | 
					  num_stars: 1
 | 
				
			||||||
  num_forks: 0
 | 
					  num_forks: 0
 | 
				
			||||||
  num_issues: 0
 | 
					  num_issues: 0
 | 
				
			||||||
  num_closed_issues: 0
 | 
					  num_closed_issues: 0
 | 
				
			||||||
@@ -945,8 +945,8 @@
 | 
				
			|||||||
  owner_name: org3
 | 
					  owner_name: org3
 | 
				
			||||||
  lower_name: repo21
 | 
					  lower_name: repo21
 | 
				
			||||||
  name: repo21
 | 
					  name: repo21
 | 
				
			||||||
  num_watches: 0
 | 
					  num_watches: 1
 | 
				
			||||||
  num_stars: 0
 | 
					  num_stars: 1
 | 
				
			||||||
  num_forks: 0
 | 
					  num_forks: 0
 | 
				
			||||||
  num_issues: 2
 | 
					  num_issues: 2
 | 
				
			||||||
  num_closed_issues: 0
 | 
					  num_closed_issues: 0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,3 +7,13 @@
 | 
				
			|||||||
  id: 2
 | 
					  id: 2
 | 
				
			||||||
  uid: 2
 | 
					  uid: 2
 | 
				
			||||||
  repo_id: 4
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  uid: 10
 | 
				
			||||||
 | 
					  repo_id: 21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  uid: 10
 | 
				
			||||||
 | 
					  repo_id: 32
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -361,7 +361,7 @@
 | 
				
			|||||||
  use_custom_avatar: false
 | 
					  use_custom_avatar: false
 | 
				
			||||||
  num_followers: 0
 | 
					  num_followers: 0
 | 
				
			||||||
  num_following: 0
 | 
					  num_following: 0
 | 
				
			||||||
  num_stars: 0
 | 
					  num_stars: 2
 | 
				
			||||||
  num_repos: 3
 | 
					  num_repos: 3
 | 
				
			||||||
  num_teams: 0
 | 
					  num_teams: 0
 | 
				
			||||||
  num_members: 0
 | 
					  num_members: 0
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								models/fixtures/user_blocking.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								models/fixtures/user_blocking.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  blocker_id: 2
 | 
				
			||||||
 | 
					  blockee_id: 29
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 2
 | 
				
			||||||
 | 
					  blocker_id: 17
 | 
				
			||||||
 | 
					  blockee_id: 28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  blocker_id: 2
 | 
				
			||||||
 | 
					  blockee_id: 34
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  blocker_id: 50
 | 
				
			||||||
 | 
					  blockee_id: 34
 | 
				
			||||||
@@ -27,3 +27,15 @@
 | 
				
			|||||||
  user_id: 11
 | 
					  user_id: 11
 | 
				
			||||||
  repo_id: 1
 | 
					  repo_id: 1
 | 
				
			||||||
  mode: 3 # auto
 | 
					  mode: 3 # auto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 6
 | 
				
			||||||
 | 
					  user_id: 10
 | 
				
			||||||
 | 
					  repo_id: 21
 | 
				
			||||||
 | 
					  mode: 1 # normal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 7
 | 
				
			||||||
 | 
					  user_id: 10
 | 
				
			||||||
 | 
					  repo_id: 32
 | 
				
			||||||
 | 
					  mode: 1 # normal
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
 | 
				
			|||||||
	return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
 | 
						return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AssignedIssuesOptions struct {
 | 
				
			||||||
 | 
						db.ListOptions
 | 
				
			||||||
 | 
						AssigneeID  int64
 | 
				
			||||||
 | 
						RepoOwnerID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						cond := builder.NewCond()
 | 
				
			||||||
 | 
						if opts.AssigneeID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.RepoOwnerID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
 | 
				
			||||||
 | 
						return db.FindAndCount[Issue](ctx, opts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
 | 
					// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
 | 
				
			||||||
func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
 | 
					func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
							return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						notBlocked := make([]*user_model.User, 0, len(mentions))
 | 
				
			||||||
 | 
						for _, user := range mentions {
 | 
				
			||||||
 | 
							if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
 | 
				
			||||||
 | 
								notBlocked = append(notBlocked, user)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mentions = notBlocked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
 | 
						if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
							return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 | 
				
			|||||||
		if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
 | 
							if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
 | 
				
			||||||
			return nil, references.XRefActionNone, nil
 | 
								return nil, references.XRefActionNone, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
 | 
				
			||||||
 | 
								return nil, references.XRefActionNone, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Accept close/reopening actions only if the poster is able to close the
 | 
							// Accept close/reopening actions only if the poster is able to close the
 | 
				
			||||||
		// referenced issue manually at this moment. The only exception is
 | 
							// referenced issue manually at this moment. The only exception is
 | 
				
			||||||
		// the poster of a new PR referencing an issue on the same repo: then the merger
 | 
							// the poster of a new PR referencing an issue on the same repo: then the merger
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -560,6 +560,8 @@ var migrations = []Migration{
 | 
				
			|||||||
	NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
 | 
						NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
 | 
				
			||||||
	// v287 -> v288
 | 
						// v287 -> v288
 | 
				
			||||||
	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
 | 
						NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
 | 
				
			||||||
 | 
						// v288 -> v289
 | 
				
			||||||
 | 
						NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentDBVersion returns the current db version
 | 
					// GetCurrentDBVersion returns the current db version
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								models/migrations/v1_22/v288.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								models/migrations/v1_22/v288.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package v1_22 //nolint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Blocking struct {
 | 
				
			||||||
 | 
						ID          int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						BlockerID   int64 `xorm:"UNIQUE(block)"`
 | 
				
			||||||
 | 
						BlockeeID   int64 `xorm:"UNIQUE(block)"`
 | 
				
			||||||
 | 
						Note        string
 | 
				
			||||||
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (*Blocking) TableName() string {
 | 
				
			||||||
 | 
						return "user_blocking"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func AddUserBlockingTable(x *xorm.Engine) error {
 | 
				
			||||||
 | 
						return x.Sync(&Blocking{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,15 +12,16 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/models/organization"
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
	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"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RemoveOrgUser removes user from given organization.
 | 
					// RemoveOrgUser removes user from given organization.
 | 
				
			||||||
func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 | 
					func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
 | 
				
			||||||
	ou := new(organization.OrgUser)
 | 
						ou := new(organization.OrgUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	has, err := db.GetEngine(ctx).
 | 
						has, err := db.GetEngine(ctx).
 | 
				
			||||||
		Where("uid=?", userID).
 | 
							Where("uid=?", user.ID).
 | 
				
			||||||
		And("org_id=?", orgID).
 | 
							And("org_id=?", org.ID).
 | 
				
			||||||
		Get(ou)
 | 
							Get(ou)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("get org-user: %w", err)
 | 
							return fmt.Errorf("get org-user: %w", err)
 | 
				
			||||||
@@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	org, err := organization.GetOrgByID(ctx, orgID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if the user to delete is the last member in owner team.
 | 
						// Check if the user to delete is the last member in owner team.
 | 
				
			||||||
	if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
 | 
						if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else if isOwner {
 | 
						} else if isOwner {
 | 
				
			||||||
		t, err := organization.GetOwnerTeam(ctx, org.ID)
 | 
							t, err := organization.GetOwnerTeam(ctx, org.ID)
 | 
				
			||||||
@@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 | 
				
			|||||||
			if err := t.LoadMembers(ctx); err != nil {
 | 
								if err := t.LoadMembers(ctx); err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if t.Members[0].ID == userID {
 | 
								if t.Members[0].ID == user.ID {
 | 
				
			||||||
				return organization.ErrLastOrgOwner{UID: userID}
 | 
									return organization.ErrLastOrgOwner{UID: user.ID}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
 | 
						if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
 | 
						} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete all repository accesses and unwatch them.
 | 
						// Delete all repository accesses and unwatch them.
 | 
				
			||||||
	env, err := organization.AccessibleReposEnv(ctx, org, userID)
 | 
						env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("AccessibleReposEnv: %w", err)
 | 
							return fmt.Errorf("AccessibleReposEnv: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	repoIDs, err := env.RepoIDs(1, org.NumRepos)
 | 
						repoIDs, err := env.RepoIDs(1, org.NumRepos)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
 | 
							return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, repoID := range repoIDs {
 | 
						for _, repoID := range repoIDs {
 | 
				
			||||||
		if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
 | 
							repo, err := repo_model.GetRepositoryByID(ctx, repoID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(repoIDs) > 0 {
 | 
						if len(repoIDs) > 0 {
 | 
				
			||||||
		if _, err = db.GetEngine(ctx).
 | 
							if _, err = db.GetEngine(ctx).
 | 
				
			||||||
			Where("user_id = ?", userID).
 | 
								Where("user_id = ?", user.ID).
 | 
				
			||||||
			In("repo_id", repoIDs).
 | 
								In("repo_id", repoIDs).
 | 
				
			||||||
			Delete(new(access_model.Access)); err != nil {
 | 
								Delete(new(access_model.Access)); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
@@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete member in their teams.
 | 
						// Delete member in their teams.
 | 
				
			||||||
	teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
 | 
						teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, t := range teams {
 | 
						for _, t := range teams {
 | 
				
			||||||
		if err = removeTeamMember(ctx, t, userID); err != nil {
 | 
							if err = removeTeamMember(ctx, t, user); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
 | 
				
			|||||||
			return fmt.Errorf("getMembers: %w", err)
 | 
								return fmt.Errorf("getMembers: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for _, u := range t.Members {
 | 
							for _, u := range t.Members {
 | 
				
			||||||
			if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
 | 
								if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
 | 
				
			||||||
				return fmt.Errorf("watchRepo: %w", err)
 | 
									return fmt.Errorf("watchRepo: %w", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
 | 
				
			|||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
 | 
								if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, tm := range t.Members {
 | 
						for _, tm := range t.Members {
 | 
				
			||||||
		if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
 | 
							if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// AddTeamMember adds new membership of given team to given organization,
 | 
					// AddTeamMember adds new membership of given team to given organization,
 | 
				
			||||||
// the user will have membership to given organization automatically when needed.
 | 
					// the user will have membership to given organization automatically when needed.
 | 
				
			||||||
func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
 | 
					func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 | 
				
			||||||
	isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
 | 
						if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 | 
				
			||||||
	if err != nil || isAlreadyMember {
 | 
						if err != nil || isAlreadyMember {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
 | 
						if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = db.WithTx(ctx, func(ctx context.Context) error {
 | 
						err = db.WithTx(ctx, func(ctx context.Context) error {
 | 
				
			||||||
		// check in transaction
 | 
							// check in transaction
 | 
				
			||||||
		isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
 | 
							isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 | 
				
			||||||
		if err != nil || isAlreadyMember {
 | 
							if err != nil || isAlreadyMember {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 | 
				
			|||||||
		sess := db.GetEngine(ctx)
 | 
							sess := db.GetEngine(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := db.Insert(ctx, &organization.TeamUser{
 | 
							if err := db.Insert(ctx, &organization.TeamUser{
 | 
				
			||||||
			UID:    userID,
 | 
								UID:    user.ID,
 | 
				
			||||||
			OrgID:  team.OrgID,
 | 
								OrgID:  team.OrgID,
 | 
				
			||||||
			TeamID: team.ID,
 | 
								TeamID: team.ID,
 | 
				
			||||||
		}); err != nil {
 | 
							}); err != nil {
 | 
				
			||||||
@@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 | 
				
			|||||||
		subQuery := builder.Select("repo_id").From("team_repo").
 | 
							subQuery := builder.Select("repo_id").From("team_repo").
 | 
				
			||||||
			Where(builder.Eq{"team_id": team.ID})
 | 
								Where(builder.Eq{"team_id": team.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := sess.Where("user_id=?", userID).
 | 
							if _, err := sess.Where("user_id=?", user.ID).
 | 
				
			||||||
			In("repo_id", subQuery).
 | 
								In("repo_id", subQuery).
 | 
				
			||||||
			And("mode < ?", team.AccessMode).
 | 
								And("mode < ?", team.AccessMode).
 | 
				
			||||||
			SetExpr("mode", team.AccessMode).
 | 
								SetExpr("mode", team.AccessMode).
 | 
				
			||||||
@@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// for not exist access
 | 
							// for not exist access
 | 
				
			||||||
		var repoIDs []int64
 | 
							var repoIDs []int64
 | 
				
			||||||
		accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
 | 
							accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
 | 
				
			||||||
		if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
 | 
							if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
 | 
				
			||||||
			return fmt.Errorf("select id accesses: %w", err)
 | 
								return fmt.Errorf("select id accesses: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		accesses := make([]*access_model.Access, 0, 100)
 | 
							accesses := make([]*access_model.Access, 0, 100)
 | 
				
			||||||
		for i, repoID := range repoIDs {
 | 
							for i, repoID := range repoIDs {
 | 
				
			||||||
			accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
 | 
								accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
 | 
				
			||||||
			if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
 | 
								if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
 | 
				
			||||||
				if err = db.Insert(ctx, accesses); err != nil {
 | 
									if err = db.Insert(ctx, accesses); err != nil {
 | 
				
			||||||
					return fmt.Errorf("insert new user accesses: %w", err)
 | 
										return fmt.Errorf("insert new user accesses: %w", err)
 | 
				
			||||||
@@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 | 
				
			|||||||
		if err := team.LoadRepositories(ctx); err != nil {
 | 
							if err := team.LoadRepositories(ctx); err != nil {
 | 
				
			||||||
			log.Error("team.LoadRepositories failed: %v", err)
 | 
								log.Error("team.LoadRepositories failed: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
 | 
							// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
 | 
				
			||||||
		go func(repos []*repo_model.Repository) {
 | 
							go func(repos []*repo_model.Repository) {
 | 
				
			||||||
			for _, repo := range repos {
 | 
								for _, repo := range repos {
 | 
				
			||||||
				if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
 | 
									if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
 | 
				
			||||||
					log.Error("watch repo failed: %v", err)
 | 
										log.Error("watch repo failed: %v", err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
 | 
					func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 | 
				
			||||||
	e := db.GetEngine(ctx)
 | 
						e := db.GetEngine(ctx)
 | 
				
			||||||
	isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
 | 
						isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 | 
				
			||||||
	if err != nil || !isMember {
 | 
						if err != nil || !isMember {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if the user to delete is the last member in owner team.
 | 
						// Check if the user to delete is the last member in owner team.
 | 
				
			||||||
	if team.IsOwnerTeam() && team.NumMembers == 1 {
 | 
						if team.IsOwnerTeam() && team.NumMembers == 1 {
 | 
				
			||||||
		return organization.ErrLastOrgOwner{UID: userID}
 | 
							return organization.ErrLastOrgOwner{UID: user.ID}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	team.NumMembers--
 | 
						team.NumMembers--
 | 
				
			||||||
@@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := e.Delete(&organization.TeamUser{
 | 
						if _, err := e.Delete(&organization.TeamUser{
 | 
				
			||||||
		UID:    userID,
 | 
							UID:    user.ID,
 | 
				
			||||||
		OrgID:  team.OrgID,
 | 
							OrgID:  team.OrgID,
 | 
				
			||||||
		TeamID: team.ID,
 | 
							TeamID: team.ID,
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
@@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Delete access to team repositories.
 | 
						// Delete access to team repositories.
 | 
				
			||||||
	for _, repo := range team.Repos {
 | 
						for _, repo := range team.Repos {
 | 
				
			||||||
		if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
 | 
							if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Remove watches from now unaccessible
 | 
							// Remove watches from now unaccessible
 | 
				
			||||||
		if err := ReconsiderWatches(ctx, repo, userID); err != nil {
 | 
							if err := ReconsiderWatches(ctx, repo, user); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Remove issue assignments from now unaccessible
 | 
							// Remove issue assignments from now unaccessible
 | 
				
			||||||
		if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
 | 
							if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return removeInvalidOrgUser(ctx, userID, team.OrgID)
 | 
						return removeInvalidOrgUser(ctx, team.OrgID, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
 | 
					func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
 | 
				
			||||||
	// Check if the user is a member of any team in the organization.
 | 
						// Check if the user is a member of any team in the organization.
 | 
				
			||||||
	if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
 | 
						if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
 | 
				
			||||||
		UID:   userID,
 | 
							UID:   user.ID,
 | 
				
			||||||
		OrgID: orgID,
 | 
							OrgID: orgID,
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	} else if count == 0 {
 | 
						} else if count == 0 {
 | 
				
			||||||
		return RemoveOrgUser(ctx, orgID, userID)
 | 
							org, err := organization.GetOrgByID(ctx, orgID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return RemoveOrgUser(ctx, org, user)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RemoveTeamMember removes member from given team of given organization.
 | 
					// RemoveTeamMember removes member from given team of given organization.
 | 
				
			||||||
func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
 | 
					func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer committer.Close()
 | 
						defer committer.Close()
 | 
				
			||||||
	if err := removeTeamMember(ctx, team, userID); err != nil {
 | 
						if err := removeTeamMember(ctx, team, user); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return committer.Commit()
 | 
						return committer.Commit()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
 | 
					func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
 | 
				
			||||||
	user, err := user_model.GetUserByID(ctx, uid)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
 | 
						if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
 | 
						if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
 | 
				
			||||||
		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
 | 
							In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
 | 
				
			||||||
		Delete(&issues_model.IssueAssignees{}); err != nil {
 | 
							Delete(&issues_model.IssueAssignees{}); err != nil {
 | 
				
			||||||
		return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
 | 
							return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
 | 
					func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
 | 
				
			||||||
	if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
 | 
						if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
 | 
						if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Remove all IssueWatches a user has subscribed to in the repository
 | 
						// Remove all IssueWatches a user has subscribed to in the repository
 | 
				
			||||||
	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
 | 
						return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,33 +21,42 @@ import (
 | 
				
			|||||||
func TestTeam_AddMember(t *testing.T) {
 | 
					func TestTeam_AddMember(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test := func(teamID, userID int64) {
 | 
						test := func(team *organization.Team, user *user_model.User) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
 | 
				
			||||||
		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
 | 
							unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
 | 
				
			||||||
		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
 | 
							unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 | 
				
			||||||
		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	test(1, 2)
 | 
					
 | 
				
			||||||
	test(1, 4)
 | 
						team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
				
			||||||
	test(3, 2)
 | 
						team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						test(team1, user2)
 | 
				
			||||||
 | 
						test(team1, user4)
 | 
				
			||||||
 | 
						test(team3, user2)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestTeam_RemoveMember(t *testing.T) {
 | 
					func TestTeam_RemoveMember(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(teamID, userID int64) {
 | 
						testSuccess := func(team *organization.Team, user *user_model.User) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
 | 
				
			||||||
		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
 | 
							unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
 | 
				
			||||||
		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
 | 
							unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 | 
				
			||||||
		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(1, 4)
 | 
					 | 
				
			||||||
	testSuccess(2, 2)
 | 
					 | 
				
			||||||
	testSuccess(3, 2)
 | 
					 | 
				
			||||||
	testSuccess(3, unittest.NonexistentID)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
						team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
				
			||||||
	err := RemoveTeamMember(db.DefaultContext, team, 2)
 | 
						team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
 | 
				
			||||||
 | 
						team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testSuccess(team1, user4)
 | 
				
			||||||
 | 
						testSuccess(team2, user2)
 | 
				
			||||||
 | 
						testSuccess(team3, user2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := RemoveTeamMember(db.DefaultContext, team1, user2)
 | 
				
			||||||
	assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
						assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
 | 
				
			|||||||
func TestAddTeamMember(t *testing.T) {
 | 
					func TestAddTeamMember(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test := func(teamID, userID int64) {
 | 
						test := func(team *organization.Team, user *user_model.User) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
 | 
				
			||||||
		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
 | 
							unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
 | 
				
			||||||
		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
 | 
							unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 | 
				
			||||||
		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	test(1, 2)
 | 
					
 | 
				
			||||||
	test(1, 4)
 | 
						team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
				
			||||||
	test(3, 2)
 | 
						team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						test(team1, user2)
 | 
				
			||||||
 | 
						test(team1, user4)
 | 
				
			||||||
 | 
						test(team3, user2)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRemoveTeamMember(t *testing.T) {
 | 
					func TestRemoveTeamMember(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(teamID, userID int64) {
 | 
						testSuccess := func(team *organization.Team, user *user_model.User) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
 | 
				
			||||||
		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
 | 
							unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
 | 
				
			||||||
		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
 | 
							unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 | 
				
			||||||
		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(1, 4)
 | 
					 | 
				
			||||||
	testSuccess(2, 2)
 | 
					 | 
				
			||||||
	testSuccess(3, 2)
 | 
					 | 
				
			||||||
	testSuccess(3, unittest.NonexistentID)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
						team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
 | 
				
			||||||
	err := RemoveTeamMember(db.DefaultContext, team, 2)
 | 
						team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
 | 
				
			||||||
 | 
						team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testSuccess(team1, user4)
 | 
				
			||||||
 | 
						testSuccess(team2, user2)
 | 
				
			||||||
 | 
						testSuccess(team3, user2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := RemoveTeamMember(db.DefaultContext, team1, user2)
 | 
				
			||||||
	assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
						assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
 | 
				
			|||||||
	team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
 | 
						team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
 | 
				
			||||||
	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 | 
						user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
 | 
						has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.False(t, has)
 | 
						assert.False(t, has)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// adding user29 to team5 should add an explicit access row for repo 23
 | 
						// adding user29 to team5 should add an explicit access row for repo 23
 | 
				
			||||||
	// even though repo 23 is public
 | 
						// even though repo 23 is public
 | 
				
			||||||
	assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
 | 
						assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
 | 
						has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.True(t, has)
 | 
						assert.True(t, has)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,22 +16,27 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func TestUser_RemoveMember(t *testing.T) {
 | 
					func TestUser_RemoveMember(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
						org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
						user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// remove a user that is a member
 | 
						// remove a user that is a member
 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
 | 
						unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
 | 
				
			||||||
	prevNumMembers := org.NumMembers
 | 
						prevNumMembers := org.NumMembers
 | 
				
			||||||
	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
 | 
						assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
 | 
						unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
 | 
				
			||||||
	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
					
 | 
				
			||||||
 | 
						org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 | 
				
			||||||
	assert.Equal(t, prevNumMembers-1, org.NumMembers)
 | 
						assert.Equal(t, prevNumMembers-1, org.NumMembers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// remove a user that is not a member
 | 
						// remove a user that is not a member
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
 | 
						unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
 | 
				
			||||||
	prevNumMembers = org.NumMembers
 | 
						prevNumMembers = org.NumMembers
 | 
				
			||||||
	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
 | 
						assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
 | 
						unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
 | 
				
			||||||
	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
					
 | 
				
			||||||
 | 
						org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 | 
				
			||||||
	assert.Equal(t, prevNumMembers, org.NumMembers)
 | 
						assert.Equal(t, prevNumMembers, org.NumMembers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
				
			||||||
@@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func TestRemoveOrgUser(t *testing.T) {
 | 
					func TestRemoveOrgUser(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	testSuccess := func(orgID, userID int64) {
 | 
					
 | 
				
			||||||
		org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
 | 
						testSuccess := func(org *organization.Organization, user *user_model.User) {
 | 
				
			||||||
		expectedNumMembers := org.NumMembers
 | 
							expectedNumMembers := org.NumMembers
 | 
				
			||||||
		if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
 | 
							if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
 | 
				
			||||||
			expectedNumMembers--
 | 
								expectedNumMembers--
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
 | 
							assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
 | 
				
			||||||
		unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
 | 
							unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
 | 
				
			||||||
		org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
 | 
							org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 | 
				
			||||||
		assert.EqualValues(t, expectedNumMembers, org.NumMembers)
 | 
							assert.EqualValues(t, expectedNumMembers, org.NumMembers)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(3, 4)
 | 
					 | 
				
			||||||
	testSuccess(3, 4)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := RemoveOrgUser(db.DefaultContext, 7, 5)
 | 
						org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
				
			||||||
 | 
						org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
						user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testSuccess(org3, user4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
				
			||||||
 | 
						testSuccess(org3, user4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := RemoveOrgUser(db.DefaultContext, org7, user5)
 | 
				
			||||||
	assert.Error(t, err)
 | 
						assert.Error(t, err)
 | 
				
			||||||
	assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
						assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
 | 
						unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
 | 
				
			|||||||
		&TeamUnit{OrgID: org.ID},
 | 
							&TeamUnit{OrgID: org.ID},
 | 
				
			||||||
		&TeamInvite{OrgID: org.ID},
 | 
							&TeamInvite{OrgID: org.ID},
 | 
				
			||||||
		&secret_model.Secret{OwnerID: org.ID},
 | 
							&secret_model.Secret{OwnerID: org.ID},
 | 
				
			||||||
 | 
							&user_model.Blocking{BlockerID: org.ID},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return fmt.Errorf("DeleteBeans: %w", err)
 | 
							return fmt.Errorf("DeleteBeans: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
 | 
				
			|||||||
		Exist()
 | 
							Exist()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetTeamUsersByTeamID returns team users for a team
 | 
					 | 
				
			||||||
func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
 | 
					 | 
				
			||||||
	teamUsers := make([]*TeamUser, 0, 10)
 | 
					 | 
				
			||||||
	return teamUsers, db.GetEngine(ctx).
 | 
					 | 
				
			||||||
		Where("team_id=?", teamID).
 | 
					 | 
				
			||||||
		Find(&teamUsers)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SearchMembersOptions holds the search options
 | 
					// SearchMembersOptions holds the search options
 | 
				
			||||||
type SearchMembersOptions struct {
 | 
					type SearchMembersOptions struct {
 | 
				
			||||||
	db.ListOptions
 | 
						db.ListOptions
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
 | 
					// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
 | 
				
			||||||
func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
 | 
					func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
 | 
				
			||||||
	collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
 | 
						collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("getCollaborations: %w", err)
 | 
							return fmt.Errorf("GetCollaborators: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, c := range collaborators {
 | 
						for _, c := range collaborators {
 | 
				
			||||||
		if c.User.IsGhost() {
 | 
							if c.User.IsGhost() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,14 +36,44 @@ type Collaborator struct {
 | 
				
			|||||||
	Collaboration *Collaboration
 | 
						Collaboration *Collaboration
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FindCollaborationOptions struct {
 | 
				
			||||||
 | 
						db.ListOptions
 | 
				
			||||||
 | 
						RepoID         int64
 | 
				
			||||||
 | 
						RepoOwnerID    int64
 | 
				
			||||||
 | 
						CollaboratorID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *FindCollaborationOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						cond := builder.NewCond()
 | 
				
			||||||
 | 
						if opts.RepoID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.RepoOwnerID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.CollaboratorID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
 | 
				
			||||||
 | 
						if opts.RepoOwnerID != 0 {
 | 
				
			||||||
 | 
							return []db.JoinFunc{
 | 
				
			||||||
 | 
								func(e db.Engine) error {
 | 
				
			||||||
 | 
									e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCollaborators returns the collaborators for a repository
 | 
					// GetCollaborators returns the collaborators for a repository
 | 
				
			||||||
func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
 | 
					func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
 | 
				
			||||||
	collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
 | 
						collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
 | 
				
			||||||
		ListOptions: listOptions,
 | 
					 | 
				
			||||||
		RepoID:      repoID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
 | 
							return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	collaborators := make([]*Collaborator, 0, len(collaborations))
 | 
						collaborators := make([]*Collaborator, 0, len(collaborations))
 | 
				
			||||||
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	usersMap := make(map[int64]*user_model.User)
 | 
						usersMap := make(map[int64]*user_model.User)
 | 
				
			||||||
	if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
 | 
						if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("Find users map by user ids: %w", err)
 | 
							return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, c := range collaborations {
 | 
						for _, c := range collaborations {
 | 
				
			||||||
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 | 
				
			|||||||
			Collaboration: c,
 | 
								Collaboration: c,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return collaborators, nil
 | 
						return collaborators, total, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCollaboration get collaboration for a repository id with a user id
 | 
					// GetCollaboration get collaboration for a repository id with a user id
 | 
				
			||||||
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
 | 
				
			|||||||
	return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
 | 
						return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FindCollaborationOptions struct {
 | 
					 | 
				
			||||||
	db.ListOptions
 | 
					 | 
				
			||||||
	RepoID int64
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (opts FindCollaborationOptions) ToConds() builder.Cond {
 | 
					 | 
				
			||||||
	return builder.And(builder.Eq{"repo_id": opts.RepoID})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
 | 
					// ChangeCollaborationAccessMode sets new access mode for the collaboration.
 | 
				
			||||||
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
 | 
					func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
 | 
				
			||||||
	// Discard invalid input
 | 
						// Discard invalid input
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	test := func(repoID int64) {
 | 
						test := func(repoID int64) {
 | 
				
			||||||
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 | 
							repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 | 
				
			||||||
		collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
 | 
							collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
		expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
 | 
							expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
 | 
				
			|||||||
	// Test db.ListOptions
 | 
						// Test db.ListOptions
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
 | 
						collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{PageSize: 1, Page: 1},
 | 
				
			||||||
 | 
							RepoID:      repo.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, collaborators1, 1)
 | 
						assert.Len(t, collaborators1, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
 | 
						collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{PageSize: 1, Page: 2},
 | 
				
			||||||
 | 
							RepoID:      repo.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, collaborators2, 1)
 | 
						assert.Len(t, collaborators2, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
 | 
				
			|||||||
	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
						unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRepository_CountCollaborators(t *testing.T) {
 | 
					 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
 | 
					 | 
				
			||||||
	count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
 | 
					 | 
				
			||||||
		RepoID: repo1.ID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.EqualValues(t, 2, count)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
 | 
					 | 
				
			||||||
	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
 | 
					 | 
				
			||||||
		RepoID: repo2.ID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.EqualValues(t, 2, count)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Non-existent repository.
 | 
					 | 
				
			||||||
	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
 | 
					 | 
				
			||||||
		RepoID: unittest.NonexistentID,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.EqualValues(t, 0, count)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
 | 
					func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func TestWatchRepo(t *testing.T) {
 | 
					func TestWatchRepo(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	const repoID = 3
 | 
					 | 
				
			||||||
	const userID = 2
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
 | 
						assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
 | 
						unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
 | 
				
			||||||
 | 
						unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMetas(t *testing.T) {
 | 
					func TestMetas(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,26 +24,30 @@ func init() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// StarRepo or unstar repository.
 | 
					// StarRepo or unstar repository.
 | 
				
			||||||
func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
 | 
					func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer committer.Close()
 | 
						defer committer.Close()
 | 
				
			||||||
	staring := IsStaring(ctx, userID, repoID)
 | 
						staring := IsStaring(ctx, doer.ID, repo.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if star {
 | 
						if star {
 | 
				
			||||||
 | 
							if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if staring {
 | 
							if staring {
 | 
				
			||||||
			return nil
 | 
								return nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
 | 
							if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
 | 
							if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
 | 
							if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
 | 
				
			|||||||
			return nil
 | 
								return nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
 | 
							if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
 | 
							if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
 | 
							if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,21 +9,24 @@ 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"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestStarRepo(t *testing.T) {
 | 
					func TestStarRepo(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	const userID = 2
 | 
					
 | 
				
			||||||
	const repoID = 1
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
 | 
					
 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
 | 
						assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
 | 
						assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
						assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestIsStaring(t *testing.T) {
 | 
					func TestIsStaring(t *testing.T) {
 | 
				
			||||||
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func TestClearRepoStars(t *testing.T) {
 | 
					func TestClearRepoStars(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	const userID = 2
 | 
					 | 
				
			||||||
	const repoID = 1
 | 
					 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
 | 
					 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
 | 
					 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
 | 
					 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
						assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
 | 
				
			||||||
 | 
						unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
						assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
						assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 | 
						gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, gazers, 0)
 | 
						assert.Len(t, gazers, 0)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,47 +16,82 @@ import (
 | 
				
			|||||||
	"xorm.io/builder"
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type StarredReposOptions struct {
 | 
				
			||||||
 | 
						db.ListOptions
 | 
				
			||||||
 | 
						StarrerID      int64
 | 
				
			||||||
 | 
						RepoOwnerID    int64
 | 
				
			||||||
 | 
						IncludePrivate bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *StarredReposOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						var cond builder.Cond = builder.Eq{
 | 
				
			||||||
 | 
							"star.uid": opts.StarrerID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.RepoOwnerID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{
 | 
				
			||||||
 | 
								"repository.owner_id": opts.RepoOwnerID,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !opts.IncludePrivate {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{
 | 
				
			||||||
 | 
								"repository.is_private": false,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
 | 
				
			||||||
 | 
						return []db.JoinFunc{
 | 
				
			||||||
 | 
							func(e db.Engine) error {
 | 
				
			||||||
 | 
								e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetStarredRepos returns the repos starred by a particular user
 | 
					// GetStarredRepos returns the repos starred by a particular user
 | 
				
			||||||
func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
 | 
					func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
 | 
				
			||||||
	sess := db.GetEngine(ctx).
 | 
						return db.Find[Repository](ctx, opts)
 | 
				
			||||||
		Where("star.uid=?", userID).
 | 
					}
 | 
				
			||||||
		Join("LEFT", "star", "`repository`.id=`star`.repo_id")
 | 
					
 | 
				
			||||||
	if !private {
 | 
					type WatchedReposOptions struct {
 | 
				
			||||||
		sess = sess.And("is_private=?", false)
 | 
						db.ListOptions
 | 
				
			||||||
 | 
						WatcherID      int64
 | 
				
			||||||
 | 
						RepoOwnerID    int64
 | 
				
			||||||
 | 
						IncludePrivate bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *WatchedReposOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						var cond builder.Cond = builder.Eq{
 | 
				
			||||||
 | 
							"watch.user_id": opts.WatcherID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if opts.RepoOwnerID != 0 {
 | 
				
			||||||
	if listOptions.Page != 0 {
 | 
							cond = cond.And(builder.Eq{
 | 
				
			||||||
		sess = db.SetSessionPagination(sess, &listOptions)
 | 
								"repository.owner_id": opts.RepoOwnerID,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
		repos := make([]*Repository, 0, listOptions.PageSize)
 | 
					 | 
				
			||||||
		return repos, sess.Find(&repos)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if !opts.IncludePrivate {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{
 | 
				
			||||||
 | 
								"repository.is_private": false,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond.And(builder.Neq{
 | 
				
			||||||
 | 
							"watch.mode": WatchModeDont,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repos := make([]*Repository, 0, 10)
 | 
					func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
 | 
				
			||||||
	return repos, sess.Find(&repos)
 | 
						return []db.JoinFunc{
 | 
				
			||||||
 | 
							func(e db.Engine) error {
 | 
				
			||||||
 | 
								e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetWatchedRepos returns the repos watched by a particular user
 | 
					// GetWatchedRepos returns the repos watched by a particular user
 | 
				
			||||||
func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
 | 
					func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
 | 
				
			||||||
	sess := db.GetEngine(ctx).
 | 
						return db.FindAndCount[Repository](ctx, opts)
 | 
				
			||||||
		Where("watch.user_id=?", userID).
 | 
					 | 
				
			||||||
		And("`watch`.mode<>?", WatchModeDont).
 | 
					 | 
				
			||||||
		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
 | 
					 | 
				
			||||||
	if !private {
 | 
					 | 
				
			||||||
		sess = sess.And("is_private=?", false)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if listOptions.Page != 0 {
 | 
					 | 
				
			||||||
		sess = db.SetSessionPagination(sess, &listOptions)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		repos := make([]*Repository, 0, listOptions.PageSize)
 | 
					 | 
				
			||||||
		total, err := sess.FindAndCount(&repos)
 | 
					 | 
				
			||||||
		return repos, total, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	repos := make([]*Repository, 0, 10)
 | 
					 | 
				
			||||||
	total, err := sess.FindAndCount(&repos)
 | 
					 | 
				
			||||||
	return repos, total, err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetRepoAssignees returns all users that have write access and can be assigned to issues
 | 
					// GetRepoAssignees returns all users that have write access and can be assigned to issues
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
 | 
				
			|||||||
	repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
 | 
						repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
 | 
				
			||||||
	users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
 | 
						users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, users, 3)
 | 
						assert.Len(t, users, 4)
 | 
				
			||||||
	assert.Equal(t, users[0].ID, int64(15))
 | 
						assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
 | 
				
			||||||
	assert.Equal(t, users[1].ID, int64(18))
 | 
					 | 
				
			||||||
	assert.Equal(t, users[2].ID, int64(16))
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRepoGetReviewers(t *testing.T) {
 | 
					func TestRepoGetReviewers(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
 | 
				
			|||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WatchRepoMode watch repository in specific mode.
 | 
					 | 
				
			||||||
func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
 | 
					 | 
				
			||||||
	var watch Watch
 | 
					 | 
				
			||||||
	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return watchRepoMode(ctx, watch, mode)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// WatchRepo watch or unwatch repository.
 | 
					// WatchRepo watch or unwatch repository.
 | 
				
			||||||
func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
 | 
					func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
 | 
				
			||||||
	var watch Watch
 | 
						watch, err := GetWatch(ctx, doer.ID, repo.ID)
 | 
				
			||||||
	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !doWatch && watch.Mode == WatchModeAuto {
 | 
						if !doWatch && watch.Mode == WatchModeAuto {
 | 
				
			||||||
		err = watchRepoMode(ctx, watch, WatchModeDont)
 | 
							return watchRepoMode(ctx, watch, WatchModeDont)
 | 
				
			||||||
	} else if !doWatch {
 | 
						} else if !doWatch {
 | 
				
			||||||
		err = watchRepoMode(ctx, watch, WatchModeNone)
 | 
							return watchRepoMode(ctx, watch, WatchModeNone)
 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		err = watchRepoMode(ctx, watch, WatchModeNormal)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return err
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return watchRepoMode(ctx, watch, WatchModeNormal)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetWatchers returns all watchers of given repository.
 | 
					// GetWatchers returns all watchers of given repository.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
						user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 | 
						watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, watchers, repo.NumWatches)
 | 
						assert.Len(t, watchers, repo.NumWatches)
 | 
				
			||||||
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
 | 
				
			|||||||
	assert.Len(t, watchers, prevCount+1)
 | 
						assert.Len(t, watchers, prevCount+1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Should remove watch, inhibit from adding auto
 | 
						// Should remove watch, inhibit from adding auto
 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
 | 
						assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
 | 
				
			||||||
	watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 | 
						watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, watchers, prevCount)
 | 
						assert.Len(t, watchers, prevCount)
 | 
				
			||||||
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, watchers, prevCount)
 | 
						assert.Len(t, watchers, prevCount)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestWatchRepoMode(t *testing.T) {
 | 
					 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
 | 
					 | 
				
			||||||
	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,8 @@ import (
 | 
				
			|||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RepoTransfer is used to manage repository transfers
 | 
					// RepoTransfer is used to manage repository transfers
 | 
				
			||||||
@@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
 | 
				
			|||||||
	return allowed
 | 
						return allowed
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PendingRepositoryTransferOptions struct {
 | 
				
			||||||
 | 
						RepoID      int64
 | 
				
			||||||
 | 
						SenderID    int64
 | 
				
			||||||
 | 
						RecipientID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						cond := builder.NewCond()
 | 
				
			||||||
 | 
						if opts.RepoID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.SenderID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.RecipientID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
 | 
				
			||||||
 | 
						transfers := make([]*RepoTransfer, 0, 10)
 | 
				
			||||||
 | 
						return transfers, db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Where(opts.ToConds()).
 | 
				
			||||||
 | 
							Find(&transfers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
 | 
					// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
 | 
				
			||||||
// process for the repository
 | 
					// process for the repository
 | 
				
			||||||
func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
 | 
					func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
 | 
				
			||||||
	transfer := new(RepoTransfer)
 | 
						transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
 | 
				
			||||||
 | 
					 | 
				
			||||||
	has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !has {
 | 
						if len(transfers) != 1 {
 | 
				
			||||||
		return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
 | 
							return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return transfer, nil
 | 
						return transfers[0], nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
 | 
					func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										123
									
								
								models/user/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								models/user/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/container"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
 | 
				
			||||||
 | 
						ErrCanNotBlock       = util.NewInvalidArgumentErrorf("cannot block the user")
 | 
				
			||||||
 | 
						ErrCanNotUnblock     = util.NewInvalidArgumentErrorf("cannot unblock the user")
 | 
				
			||||||
 | 
						ErrBlockedUser       = util.NewPermissionDeniedErrorf("user is blocked")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Blocking struct {
 | 
				
			||||||
 | 
						ID          int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						BlockerID   int64 `xorm:"UNIQUE(block)"`
 | 
				
			||||||
 | 
						Blocker     *User `xorm:"-"`
 | 
				
			||||||
 | 
						BlockeeID   int64 `xorm:"UNIQUE(block)"`
 | 
				
			||||||
 | 
						Blockee     *User `xorm:"-"`
 | 
				
			||||||
 | 
						Note        string
 | 
				
			||||||
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (*Blocking) TableName() string {
 | 
				
			||||||
 | 
						return "user_blocking"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						db.RegisterModel(new(Blocking))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
 | 
				
			||||||
 | 
						_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
 | 
				
			||||||
 | 
						if len(blockerIDs) == 0 {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if blockee.IsAdmin {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
 | 
				
			||||||
 | 
							And(builder.In("user_blocking.blocker_id", blockerIDs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
 | 
				
			||||||
 | 
						return has
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FindBlockingOptions struct {
 | 
				
			||||||
 | 
						db.ListOptions
 | 
				
			||||||
 | 
						BlockerID int64
 | 
				
			||||||
 | 
						BlockeeID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *FindBlockingOptions) ToConds() builder.Cond {
 | 
				
			||||||
 | 
						cond := builder.NewCond()
 | 
				
			||||||
 | 
						if opts.BlockerID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.BlockeeID != 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
 | 
				
			||||||
 | 
						return db.FindAndCount[Blocking](ctx, opts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
 | 
				
			||||||
 | 
						blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
 | 
				
			||||||
 | 
							BlockerID: blockerID,
 | 
				
			||||||
 | 
							BlockeeID: blockeeID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(blocks) == 0 {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return blocks[0], nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BlockingList []*Blocking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
 | 
				
			||||||
 | 
						ids := make(container.Set[int64], len(blocks)*2)
 | 
				
			||||||
 | 
						for _, b := range blocks {
 | 
				
			||||||
 | 
							ids.Add(b.BlockerID)
 | 
				
			||||||
 | 
							ids.Add(b.BlockeeID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userList, err := GetUsersByIDs(ctx, ids.Values())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userMap := make(map[int64]*User, len(userList))
 | 
				
			||||||
 | 
						for _, u := range userList {
 | 
				
			||||||
 | 
							userMap[u.ID] = u
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, b := range blocks {
 | 
				
			||||||
 | 
							b.Blocker = userMap[b.BlockerID]
 | 
				
			||||||
 | 
							b.Blockee = userMap[b.BlockeeID]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FollowUser marks someone be another's follower.
 | 
					// FollowUser marks someone be another's follower.
 | 
				
			||||||
func FollowUser(ctx context.Context, userID, followID int64) (err error) {
 | 
					func FollowUser(ctx context.Context, user, follow *User) (err error) {
 | 
				
			||||||
	if userID == followID || IsFollowing(ctx, userID, followID) {
 | 
						if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
 | 
				
			||||||
 | 
							return ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer committer.Close()
 | 
						defer committer.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
 | 
						if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
 | 
						if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
 | 
						if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return committer.Commit()
 | 
						return committer.Commit()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1167,7 +1167,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
 | 
				
			|||||||
			return false
 | 
								return false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If they follow - they see each over
 | 
							// If they follow - they see each other
 | 
				
			||||||
		follower := IsFollowing(ctx, u.ID, viewer.ID)
 | 
							follower := IsFollowing(ctx, u.ID, viewer.ID)
 | 
				
			||||||
		if follower {
 | 
							if follower {
 | 
				
			||||||
			return true
 | 
								return true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
 | 
				
			|||||||
func TestFollowUser(t *testing.T) {
 | 
					func TestFollowUser(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(followerID, followedID int64) {
 | 
						testSuccess := func(follower, followed *user_model.User) {
 | 
				
			||||||
		assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
 | 
							assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
 | 
				
			||||||
		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
 | 
							unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	testSuccess(4, 2)
 | 
					 | 
				
			||||||
	testSuccess(5, 2)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
						user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testSuccess(user4, user2)
 | 
				
			||||||
 | 
						testSuccess(user5, user2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,14 @@ 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 err := repo.LoadOwner(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return db.WithTx(ctx, func(ctx context.Context) error {
 | 
						return db.WithTx(ctx, func(ctx context.Context) error {
 | 
				
			||||||
		has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
 | 
							has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
 | 
				
			||||||
			"repo_id": repo.ID,
 | 
								"repo_id": repo.ID,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -153,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if setting.Service.AutoWatchNewRepos {
 | 
						if setting.Service.AutoWatchNewRepos {
 | 
				
			||||||
		if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
 | 
							if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 | 
				
			||||||
			return fmt.Errorf("WatchRepo: %w", err)
 | 
								return fmt.Errorf("WatchRepo: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -632,6 +632,30 @@ form.name_reserved = The username "%s" is reserved.
 | 
				
			|||||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 | 
					form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 | 
				
			||||||
form.name_chars_not_allowed = User name "%s" contains invalid characters.
 | 
					form.name_chars_not_allowed = User name "%s" contains invalid characters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block.block = Block
 | 
				
			||||||
 | 
					block.block.user = Block user
 | 
				
			||||||
 | 
					block.block.org = Block user for organization
 | 
				
			||||||
 | 
					block.block.failure = Failed to block user: %s
 | 
				
			||||||
 | 
					block.unblock = Unblock
 | 
				
			||||||
 | 
					block.unblock.failure = Failed to unblock user: %s
 | 
				
			||||||
 | 
					block.blocked = You have blocked this user.
 | 
				
			||||||
 | 
					block.title = Block a user
 | 
				
			||||||
 | 
					block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
 | 
				
			||||||
 | 
					block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
 | 
				
			||||||
 | 
					block.info_2 = following your account
 | 
				
			||||||
 | 
					block.info_3 = send you notifications by @mentioning your username
 | 
				
			||||||
 | 
					block.info_4 = inviting you as a collaborator to their repositories
 | 
				
			||||||
 | 
					block.info_5 = starring, forking or watching on repositories
 | 
				
			||||||
 | 
					block.info_6 = opening and commenting on issues or pull requests
 | 
				
			||||||
 | 
					block.info_7 = reacting on your comments in issues or pull requests
 | 
				
			||||||
 | 
					block.user_to_block = User to block
 | 
				
			||||||
 | 
					block.note = Note
 | 
				
			||||||
 | 
					block.note.title = Optional note:
 | 
				
			||||||
 | 
					block.note.info = The note is not visible to the blocked user.
 | 
				
			||||||
 | 
					block.note.edit = Edit note
 | 
				
			||||||
 | 
					block.list = Blocked users
 | 
				
			||||||
 | 
					block.list.none = You have not blocked any users. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[settings]
 | 
					[settings]
 | 
				
			||||||
profile = Profile
 | 
					profile = Profile
 | 
				
			||||||
account = Account
 | 
					account = Account
 | 
				
			||||||
@@ -969,6 +993,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
 | 
				
			|||||||
fork_branch = Branch to be cloned to the fork
 | 
					fork_branch = Branch to be cloned to the fork
 | 
				
			||||||
all_branches = All branches
 | 
					all_branches = All branches
 | 
				
			||||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
 | 
					fork_no_valid_owners = This repository can not be forked because there are no valid owners.
 | 
				
			||||||
 | 
					fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
 | 
				
			||||||
use_template = Use this template
 | 
					use_template = Use this template
 | 
				
			||||||
open_with_editor = Open with %s
 | 
					open_with_editor = Open with %s
 | 
				
			||||||
download_zip = Download ZIP
 | 
					download_zip = Download ZIP
 | 
				
			||||||
@@ -1144,6 +1169,7 @@ watch = Watch
 | 
				
			|||||||
unstar = Unstar
 | 
					unstar = Unstar
 | 
				
			||||||
star = Star
 | 
					star = Star
 | 
				
			||||||
fork = Fork
 | 
					fork = Fork
 | 
				
			||||||
 | 
					action.blocked_user = Cannot perform action because you are blocked by the repository owner.
 | 
				
			||||||
download_archive = Download Repository
 | 
					download_archive = Download Repository
 | 
				
			||||||
more_operations = More Operations
 | 
					more_operations = More Operations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1394,6 +1420,8 @@ issues.new.assignees = Assignees
 | 
				
			|||||||
issues.new.clear_assignees = Clear assignees
 | 
					issues.new.clear_assignees = Clear assignees
 | 
				
			||||||
issues.new.no_assignees = No Assignees
 | 
					issues.new.no_assignees = No Assignees
 | 
				
			||||||
issues.new.no_reviewers = No reviewers
 | 
					issues.new.no_reviewers = No reviewers
 | 
				
			||||||
 | 
					issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
 | 
				
			||||||
 | 
					issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
 | 
				
			||||||
issues.choose.get_started = Get Started
 | 
					issues.choose.get_started = Get Started
 | 
				
			||||||
issues.choose.open_external_link = Open
 | 
					issues.choose.open_external_link = Open
 | 
				
			||||||
issues.choose.blank = Default
 | 
					issues.choose.blank = Default
 | 
				
			||||||
@@ -1509,6 +1537,7 @@ issues.close_comment_issue = Comment and Close
 | 
				
			|||||||
issues.reopen_issue = Reopen
 | 
					issues.reopen_issue = Reopen
 | 
				
			||||||
issues.reopen_comment_issue = Comment and Reopen
 | 
					issues.reopen_comment_issue = Comment and Reopen
 | 
				
			||||||
issues.create_comment = Comment
 | 
					issues.create_comment = Comment
 | 
				
			||||||
 | 
					issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
 | 
				
			||||||
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
					issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
				
			||||||
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
					issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
				
			||||||
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
					issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 | 
				
			||||||
@@ -1707,6 +1736,7 @@ compare.compare_head = compare
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pulls.desc = Enable pull requests and code reviews.
 | 
					pulls.desc = Enable pull requests and code reviews.
 | 
				
			||||||
pulls.new = New Pull Request
 | 
					pulls.new = New Pull Request
 | 
				
			||||||
 | 
					pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
 | 
				
			||||||
pulls.view = View Pull Request
 | 
					pulls.view = View Pull Request
 | 
				
			||||||
pulls.compare_changes = New Pull Request
 | 
					pulls.compare_changes = New Pull Request
 | 
				
			||||||
pulls.allow_edits_from_maintainers = Allow edits from maintainers
 | 
					pulls.allow_edits_from_maintainers = Allow edits from maintainers
 | 
				
			||||||
@@ -2120,6 +2150,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
 | 
				
			|||||||
settings.transfer = Transfer Ownership
 | 
					settings.transfer = Transfer Ownership
 | 
				
			||||||
settings.transfer.rejected = Repository transfer was rejected.
 | 
					settings.transfer.rejected = Repository transfer was rejected.
 | 
				
			||||||
settings.transfer.success = Repository transfer was successful.
 | 
					settings.transfer.success = Repository transfer was successful.
 | 
				
			||||||
 | 
					settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
 | 
				
			||||||
settings.transfer_abort = Cancel transfer
 | 
					settings.transfer_abort = Cancel transfer
 | 
				
			||||||
settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
 | 
					settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
 | 
				
			||||||
settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
 | 
					settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
 | 
				
			||||||
@@ -2165,6 +2196,7 @@ settings.add_collaborator_success = The collaborator has been added.
 | 
				
			|||||||
settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
 | 
					settings.add_collaborator_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_user = The collaborator is blocked by the repository owner or vice versa.
 | 
				
			||||||
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?
 | 
				
			||||||
@@ -2731,6 +2763,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
 | 
				
			|||||||
teams.add_duplicate_users = User is already a team member.
 | 
					teams.add_duplicate_users = User is already a team member.
 | 
				
			||||||
teams.repos.none = No repositories could be accessed by this team.
 | 
					teams.repos.none = No repositories could be accessed by this team.
 | 
				
			||||||
teams.members.none = No members on this team.
 | 
					teams.members.none = No members on this team.
 | 
				
			||||||
 | 
					teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
 | 
				
			||||||
teams.specific_repositories = Specific repositories
 | 
					teams.specific_repositories = Specific repositories
 | 
				
			||||||
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
 | 
					teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
 | 
				
			||||||
teams.all_repositories = All repositories
 | 
					teams.all_repositories = All repositories
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1027,7 +1027,16 @@ func Routes() *web.Route {
 | 
				
			|||||||
			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)
 | 
				
			||||||
			}, reqToken())
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								m.Group("/blocks", func() {
 | 
				
			||||||
 | 
									m.Get("", user.ListBlocks)
 | 
				
			||||||
 | 
									m.Group("/{username}", func() {
 | 
				
			||||||
 | 
										m.Get("", user.CheckUserBlock)
 | 
				
			||||||
 | 
										m.Put("", user.BlockUser)
 | 
				
			||||||
 | 
										m.Delete("", user.UnblockUser)
 | 
				
			||||||
 | 
									}, context.UserAssignmentAPI())
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 | 
							}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Repositories (requires repo scope, org scope)
 | 
							// Repositories (requires repo scope, org scope)
 | 
				
			||||||
@@ -1477,6 +1486,15 @@ 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("/blocks", func() {
 | 
				
			||||||
 | 
									m.Get("", org.ListBlocks)
 | 
				
			||||||
 | 
									m.Group("/{username}", func() {
 | 
				
			||||||
 | 
										m.Get("", org.CheckUserBlock)
 | 
				
			||||||
 | 
										m.Put("", org.BlockUser)
 | 
				
			||||||
 | 
										m.Delete("", org.UnblockUser)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}, reqToken(), reqOrgOwnership())
 | 
				
			||||||
		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 | 
							}, 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).
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										116
									
								
								routers/api/v1/org/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								routers/api/v1/org/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/api/v1/shared"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ListBlocks(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: List users blocked by the organization
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the organization
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: page
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page number of results to return (1-based)
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// - name: limit
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page size of results
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/UserList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CheckUserBlock(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Check if a user is blocked by the organization
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the organization
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to check
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Block a user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the organization
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to block
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: note
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: optional note for the block
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Unblock a user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: org
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the organization
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to unblock
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
 | 
				
			|||||||
	if ctx.Written() {
 | 
						if ctx.Written() {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
 | 
						if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
 | 
							ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Status(http.StatusNoContent)
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,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"
 | 
				
			||||||
	unit_model "code.gitea.io/gitea/models/unit"
 | 
						unit_model "code.gitea.io/gitea/models/unit"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	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"
 | 
				
			||||||
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
 | 
				
			|||||||
	if ctx.Written() {
 | 
						if ctx.Written() {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
 | 
						if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "AddMember", err)
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "AddTeamMember", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Status(http.StatusNoContent)
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
 | 
						if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
 | 
							ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/models/perm"
 | 
						"code.gitea.io/gitea/models/perm"
 | 
				
			||||||
	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"
 | 
				
			||||||
@@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
 | 
						collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
 | 
				
			||||||
 | 
							ListOptions: utils.GetListOptions(ctx),
 | 
				
			||||||
		RepoID:      ctx.Repo.Repository.ID,
 | 
							RepoID:      ctx.Repo.Repository.ID,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		ctx.InternalServerError(err)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
 | 
							ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
 | 
				
			|||||||
		users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
 | 
							users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.SetTotalCountHeader(count)
 | 
						ctx.SetTotalCountHeader(total)
 | 
				
			||||||
	ctx.JSON(http.StatusOK, users)
 | 
						ctx.JSON(http.StatusOK, users)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
	//   "422":
 | 
						//   "422":
 | 
				
			||||||
@@ -182,7 +178,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 {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "AddCollaborator", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
 | 
								ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
 | 
						if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
 | 
							ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
 | 
							if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusConflict, "ForkRepository", err)
 | 
								ctx.Error(http.StatusConflict, "ForkRepository", err)
 | 
				
			||||||
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "ForkRepository", err)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
 | 
								ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/validationError"
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
	//   "423":
 | 
						//   "423":
 | 
				
			||||||
	//     "$ref": "#/responses/repoArchivedError"
 | 
						//     "$ref": "#/responses/repoArchivedError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form := web.GetForm(ctx).(*api.CreateIssueOption)
 | 
						form := web.GetForm(ctx).(*api.CreateIssueOption)
 | 
				
			||||||
	var deadlineUnix timeutil.TimeStamp
 | 
						var deadlineUnix timeutil.TimeStamp
 | 
				
			||||||
	if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
 | 
						if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
 | 
				
			||||||
@@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) {
 | 
				
			|||||||
	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
						if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
		}
 | 
								ctx.Error(http.StatusForbidden, "NewIssue", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 | 
								ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
 | 
							err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
									ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 | 
									ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
	//   "423":
 | 
						//   "423":
 | 
				
			||||||
	//     "$ref": "#/responses/repoArchivedError"
 | 
						//     "$ref": "#/responses/repoArchivedError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
 | 
						form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
 | 
				
			||||||
	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 | 
						issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
						comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
								ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
	//   "423":
 | 
						//   "423":
 | 
				
			||||||
	//     "$ref": "#/responses/repoArchivedError"
 | 
						//     "$ref": "#/responses/repoArchivedError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form := web.GetForm(ctx).(*api.EditIssueCommentOption)
 | 
						form := web.GetForm(ctx).(*api.EditIssueCommentOption)
 | 
				
			||||||
	editIssueComment(ctx, *form)
 | 
						editIssueComment(ctx, *form)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 | 
				
			|||||||
	oldContent := comment.Content
 | 
						oldContent := comment.Content
 | 
				
			||||||
	comment.Content = form.Body
 | 
						comment.Content = form.Body
 | 
				
			||||||
	if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
 | 
						if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "UpdateComment", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
 | 
								ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,12 @@
 | 
				
			|||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/Attachment"
 | 
						//     "$ref": "#/responses/Attachment"
 | 
				
			||||||
	//   "400":
 | 
						//   "400":
 | 
				
			||||||
	//     "$ref": "#/responses/error"
 | 
						//     "$ref": "#/responses/error"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/error"
 | 
						//     "$ref": "#/responses/error"
 | 
				
			||||||
	//   "423":
 | 
						//   "423":
 | 
				
			||||||
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
 | 
						if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "UpdateComment", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("UpdateComment", err)
 | 
								ctx.ServerError("UpdateComment", 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"
 | 
				
			||||||
	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/context"
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
	"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
 | 
				
			||||||
@@ -218,9 +220,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, form.Reaction)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
				ctx.Error(http.StatusForbidden, err.Error(), err)
 | 
									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{
 | 
				
			||||||
@@ -434,9 +436,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.ErrBlockedUser) {
 | 
				
			||||||
				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{
 | 
				
			||||||
@@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 | 
				
			|||||||
					Created:  reaction.CreatedUnix.AsTime(),
 | 
										Created:  reaction.CreatedUnix.AsTime(),
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
 | 
									ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "201":
 | 
						//   "201":
 | 
				
			||||||
	//     "$ref": "#/responses/PullRequest"
 | 
						//     "$ref": "#/responses/PullRequest"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
	//   "409":
 | 
						//   "409":
 | 
				
			||||||
@@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) {
 | 
				
			|||||||
	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 | 
				
			||||||
			return
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
		}
 | 
								ctx.Error(http.StatusForbidden, "BlockedUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 | 
								ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) {
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if user_model.IsErrUserNotExist(err) {
 | 
								if user_model.IsErrUserNotExist(err) {
 | 
				
			||||||
				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
 | 
									ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
 | 
				
			||||||
 | 
								} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
									ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 | 
									ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
package repo
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.InternalServerError(err)
 | 
								ctx.InternalServerError(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										98
									
								
								routers/api/v1/shared/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								routers/api/v1/shared/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package shared
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/api/v1/utils"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/convert"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
 | 
				
			||||||
 | 
						blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
 | 
				
			||||||
 | 
							ListOptions: utils.GetListOptions(ctx),
 | 
				
			||||||
 | 
							BlockerID:   blocker.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						users := make([]*api.User, 0, len(blocks))
 | 
				
			||||||
 | 
						for _, b := range blocks {
 | 
				
			||||||
 | 
							users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.SetTotalCountHeader(total)
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, &users)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
 | 
				
			||||||
 | 
						blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.NotFound("GetUserByName", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						status := http.StatusNotFound
 | 
				
			||||||
 | 
						blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if blocking != nil {
 | 
				
			||||||
 | 
							status = http.StatusNoContent
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(status)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
 | 
				
			||||||
 | 
						blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.NotFound("GetUserByName", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusBadRequest, "BlockUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "BlockUser", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
 | 
				
			||||||
 | 
						blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.NotFound("GetUserByName", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusBadRequest, "UnblockUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								routers/api/v1/user/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								routers/api/v1/user/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/api/v1/shared"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ListBlocks(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /user/blocks user userListBlocks
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: List users blocked by the authenticated user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: page
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page number of results to return (1-based)
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// - name: limit
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page size of results
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/UserList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.ListBlocks(ctx, ctx.Doer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CheckUserBlock(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Check if a user is blocked by the authenticated user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to check
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.CheckUserBlock(ctx, ctx.Doer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation PUT /user/blocks/{username} user userBlockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Block a user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to block
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: note
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: optional note for the block
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.BlockUser(ctx, ctx.Doer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnblockUser(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Unblock a user
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: user to unblock
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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"
 | 
				
			||||||
@@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
						if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "FollowUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
								ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Status(http.StatusNoContent)
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,10 +5,9 @@
 | 
				
			|||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	std_context "context"
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
					 | 
				
			||||||
	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"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
@@ -20,8 +19,12 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// getStarredRepos returns the repos that the user with the specified userID has
 | 
					// getStarredRepos returns the repos that the user with the specified userID has
 | 
				
			||||||
// starred
 | 
					// starred
 | 
				
			||||||
func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
 | 
					func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
 | 
				
			||||||
	starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
 | 
						starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
 | 
				
			||||||
 | 
							ListOptions:    utils.GetListOptions(ctx),
 | 
				
			||||||
 | 
							StarrerID:      user.ID,
 | 
				
			||||||
 | 
							IncludePrivate: private,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private := ctx.ContextUser.ID == ctx.Doer.ID
 | 
						private := ctx.ContextUser.ID == ctx.Doer.ID
 | 
				
			||||||
	repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
 | 
						repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 | 
							ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "200":
 | 
						//   "200":
 | 
				
			||||||
	//     "$ref": "#/responses/RepositoryList"
 | 
						//     "$ref": "#/responses/RepositoryList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
 | 
						repos, err := getStarredRepos(ctx, ctx.Doer, true)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 | 
							ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "204":
 | 
						//   "204":
 | 
				
			||||||
	//     "$ref": "#/responses/empty"
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
 | 
						err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 | 
								ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Status(http.StatusNoContent)
 | 
						ctx.Status(http.StatusNoContent)
 | 
				
			||||||
@@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
 | 
						err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 | 
							ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,9 @@
 | 
				
			|||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	std_context "context"
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
					 | 
				
			||||||
	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"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
@@ -18,8 +17,12 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getWatchedRepos returns the repos that the user with the specified userID is watching
 | 
					// getWatchedRepos returns the repos that the user with the specified userID is watching
 | 
				
			||||||
func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
 | 
					func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
 | 
				
			||||||
	watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
 | 
						watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
 | 
				
			||||||
 | 
							ListOptions:    utils.GetListOptions(ctx),
 | 
				
			||||||
 | 
							WatcherID:      user.ID,
 | 
				
			||||||
 | 
							IncludePrivate: private,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, 0, err
 | 
							return nil, 0, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private := ctx.ContextUser.ID == ctx.Doer.ID
 | 
						private := ctx.ContextUser.ID == ctx.Doer.ID
 | 
				
			||||||
	repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
 | 
						repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 | 
							ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "200":
 | 
						//   "200":
 | 
				
			||||||
	//     "$ref": "#/responses/RepositoryList"
 | 
						//     "$ref": "#/responses/RepositoryList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
 | 
						repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 | 
							ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
 | 
				
			|||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "200":
 | 
						//   "200":
 | 
				
			||||||
	//     "$ref": "#/responses/WatchInfo"
 | 
						//     "$ref": "#/responses/WatchInfo"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
 | 
						err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusForbidden, "BlockedUser", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
 | 
								ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.JSON(http.StatusOK, api.WatchInfo{
 | 
						ctx.JSON(http.StatusOK, api.WatchInfo{
 | 
				
			||||||
@@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
 | 
						err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
 | 
							ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								routers/web/org/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								routers/web/org/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("user.block.list")
 | 
				
			||||||
 | 
						ctx.Data["PageIsOrgSettings"] = true
 | 
				
			||||||
 | 
						ctx.Data["PageIsSettingsBlockedUsers"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared_user.BlockedUsers(ctx, ctx.ContextUser)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsersPost(ctx *context.Context) {
 | 
				
			||||||
 | 
						shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/models/organization"
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// MembersAction response for operation to a member of organization
 | 
					// MembersAction response for operation to a member of organization
 | 
				
			||||||
func MembersAction(ctx *context.Context) {
 | 
					func MembersAction(ctx *context.Context) {
 | 
				
			||||||
	uid := ctx.FormInt64("uid")
 | 
						member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
 | 
				
			||||||
	if uid == 0 {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("GetUserByID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if member == nil {
 | 
				
			||||||
		ctx.Redirect(ctx.Org.OrgLink + "/members")
 | 
							ctx.Redirect(ctx.Org.OrgLink + "/members")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	org := ctx.Org.Organization
 | 
						org := ctx.Org.Organization
 | 
				
			||||||
	var err error
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "private":
 | 
						case "private":
 | 
				
			||||||
		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
 | 
							if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 | 
				
			||||||
			ctx.Error(http.StatusNotFound)
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
 | 
							err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
 | 
				
			||||||
	case "public":
 | 
						case "public":
 | 
				
			||||||
		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
 | 
							if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 | 
				
			||||||
			ctx.Error(http.StatusNotFound)
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
 | 
							err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
 | 
				
			||||||
	case "remove":
 | 
						case "remove":
 | 
				
			||||||
		if !ctx.Org.IsOwner {
 | 
							if !ctx.Org.IsOwner {
 | 
				
			||||||
			ctx.Error(http.StatusNotFound)
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = models.RemoveOrgUser(ctx, org.ID, uid)
 | 
							err = models.RemoveOrgUser(ctx, org, member)
 | 
				
			||||||
		if organization.IsErrLastOrgOwner(err) {
 | 
							if organization.IsErrLastOrgOwner(err) {
 | 
				
			||||||
			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
								ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
			ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
 | 
								ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	case "leave":
 | 
						case "leave":
 | 
				
			||||||
		err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
 | 
							err = models.RemoveOrgUser(ctx, org, ctx.Doer)
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
 | 
								ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
 | 
				
			||||||
			ctx.JSON(http.StatusOK, map[string]any{
 | 
								ctx.JSON(http.StatusOK, map[string]any{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package org
 | 
					package org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
@@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
			ctx.Error(http.StatusNotFound)
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
 | 
							err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 | 
				
			||||||
	case "leave":
 | 
						case "leave":
 | 
				
			||||||
		err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
 | 
							err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if org_model.IsErrLastOrgOwner(err) {
 | 
								if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
									ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
@@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		uid := ctx.FormInt64("uid")
 | 
							user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
 | 
				
			||||||
		if uid == 0 {
 | 
							if user == nil {
 | 
				
			||||||
			ctx.Redirect(ctx.Org.OrgLink + "/teams")
 | 
								ctx.Redirect(ctx.Org.OrgLink + "/teams")
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
 | 
							err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if org_model.IsErrLastOrgOwner(err) {
 | 
								if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
									ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
@@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
		if ctx.Org.Team.IsMember(ctx, u.ID) {
 | 
							if ctx.Org.Team.IsMember(ctx, u.ID) {
 | 
				
			||||||
			ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
 | 
								ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
 | 
								err = models.AddTeamMember(ctx, ctx.Org.Team, u)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		page = "team"
 | 
							page = "team"
 | 
				
			||||||
@@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if org_model.IsErrLastOrgOwner(err) {
 | 
							if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
								ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
								log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
				
			||||||
			ctx.JSON(http.StatusOK, map[string]any{
 | 
								ctx.JSON(http.StatusOK, map[string]any{
 | 
				
			||||||
@@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
 | 
						if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
 | 
				
			||||||
		ctx.ServerError("AddTeamMember", err)
 | 
							ctx.ServerError("AddTeamMember", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,6 +57,7 @@ import (
 | 
				
			|||||||
	issue_service "code.gitea.io/gitea/services/issue"
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@@ -1258,9 +1259,11 @@ 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 repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
		}
 | 
								ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("NewIssue", err)
 | 
								ctx.ServerError("NewIssue", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2047,6 +2050,10 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Data["Tags"] = tags
 | 
						ctx.Data["Tags"] = tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
 | 
				
			||||||
 | 
							return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.HTML(http.StatusOK, tplIssueView)
 | 
						ctx.HTML(http.StatusOK, tplIssueView)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2250,7 +2257,11 @@ func UpdateIssueContent(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
 | 
						if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("ChangeContent", err)
 | 
								ctx.ServerError("ChangeContent", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3108,7 +3119,11 @@ func NewComment(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 | 
						comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("CreateIssueComment", err)
 | 
								ctx.ServerError("CreateIssueComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3152,7 +3167,11 @@ func UpdateCommentContent(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
 | 
						if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("UpdateComment", err)
 | 
								ctx.ServerError("UpdateComment", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3260,9 +3279,9 @@ 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) || errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -3367,9 +3386,9 @@ 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, form.Content)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if issues_model.IsErrForbiddenIssueReaction(err) {
 | 
								if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
				ctx.ServerError("ChangeIssueReaction", err)
 | 
									ctx.ServerError("ChangeIssueReaction", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,6 +47,7 @@ import (
 | 
				
			|||||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
						notify_service "code.gitea.io/gitea/services/notify"
 | 
				
			||||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gobwas/glob"
 | 
						"github.com/gobwas/glob"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) {
 | 
				
			|||||||
			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
 | 
				
			||||||
		case db.IsErrNamePatternNotAllowed(err):
 | 
							case db.IsErrNamePatternNotAllowed(err):
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
 | 
				
			||||||
 | 
							case errors.Is(err, user_model.ErrBlockedUser):
 | 
				
			||||||
 | 
								ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
			ctx.ServerError("ForkPost", err)
 | 
								ctx.ServerError("ForkPost", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	upload.AddUploadContext(ctx, "comment")
 | 
						upload.AddUploadContext(ctx, "comment")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
 | 
				
			||||||
 | 
							return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.HTML(http.StatusOK, tplPullFiles)
 | 
						ctx.HTML(http.StatusOK, tplPullFiles)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 | 
				
			|||||||
	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
						if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 | 
				
			||||||
		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
							if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 | 
				
			||||||
			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
								ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		} else if git.IsErrPushRejected(err) {
 | 
							} else if git.IsErrPushRejected(err) {
 | 
				
			||||||
			pushrejErr := err.(*git.ErrPushRejected)
 | 
								pushrejErr := err.(*git.ErrPushRejected)
 | 
				
			||||||
			message := pushrejErr.Message
 | 
								message := pushrejErr.Message
 | 
				
			||||||
@@ -1501,9 +1507,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 | 
				
			|||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			ctx.JSONError(flashError)
 | 
								ctx.JSONError(flashError)
 | 
				
			||||||
 | 
							} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 | 
				
			||||||
 | 
									"Message": ctx.Tr("repo.pulls.push_rejected"),
 | 
				
			||||||
 | 
									"Summary": ctx.Tr("repo.pulls.new.blocked_user"),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("CompareAndPullRequest.HTMLString", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		ctx.ServerError("NewPullRequest", err)
 | 
								ctx.JSONError(flashError)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -313,13 +313,13 @@ func Action(ctx *context.Context) {
 | 
				
			|||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "watch":
 | 
						case "watch":
 | 
				
			||||||
		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
 | 
							err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 | 
				
			||||||
	case "unwatch":
 | 
						case "unwatch":
 | 
				
			||||||
		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
 | 
							err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 | 
				
			||||||
	case "star":
 | 
						case "star":
 | 
				
			||||||
		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
 | 
							err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 | 
				
			||||||
	case "unstar":
 | 
						case "unstar":
 | 
				
			||||||
		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
 | 
							err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 | 
				
			||||||
	case "accept_transfer":
 | 
						case "accept_transfer":
 | 
				
			||||||
		err = acceptOrRejectRepoTransfer(ctx, true)
 | 
							err = acceptOrRejectRepoTransfer(ctx, true)
 | 
				
			||||||
	case "reject_transfer":
 | 
						case "reject_transfer":
 | 
				
			||||||
@@ -336,9 +336,13 @@ func Action(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
 | 
								ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
	case "watch", "unwatch":
 | 
						case "watch", "unwatch":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,10 @@
 | 
				
			|||||||
package setting
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/models/organization"
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
	"code.gitea.io/gitea/models/perm"
 | 
						"code.gitea.io/gitea/models/perm"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
@@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
 | 
						ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
 | 
				
			||||||
	ctx.Data["PageIsSettingsCollaboration"] = true
 | 
						ctx.Data["PageIsSettingsCollaboration"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
 | 
						users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("GetCollaborators", err)
 | 
							ctx.ServerError("GetCollaborators", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -101,7 +101,12 @@ 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 {
 | 
				
			||||||
 | 
							if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			ctx.ServerError("AddCollaborator", err)
 | 
								ctx.ServerError("AddCollaborator", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,11 +131,20 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// DeleteCollaboration delete a collaboration for a repository
 | 
					// DeleteCollaboration delete a collaboration for a repository
 | 
				
			||||||
func DeleteCollaboration(ctx *context.Context) {
 | 
					func DeleteCollaboration(ctx *context.Context) {
 | 
				
			||||||
	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
 | 
						if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
 | 
				
			||||||
 | 
							if user_model.IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.ServerError("GetUserByName", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
 | 
				
			||||||
			ctx.Flash.Error("DeleteCollaboration: " + err.Error())
 | 
								ctx.Flash.Error("DeleteCollaboration: " + err.Error())
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
 | 
								ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
 | 
						ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package setting
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@@ -782,6 +783,8 @@ func SettingsPost(ctx *context.Context) {
 | 
				
			|||||||
				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)
 | 
				
			||||||
 | 
								} else if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
									ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.ServerError("TransferOwnership", err)
 | 
									ctx.ServerError("TransferOwnership", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										76
									
								
								routers/web/shared/user/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								routers/web/shared/user/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
 | 
						user_service "code.gitea.io/gitea/services/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
 | 
				
			||||||
 | 
						blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
 | 
				
			||||||
 | 
							BlockerID: blocker.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("FindBlockings", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("LoadAttributes", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Data["UserBlocks"] = blocks
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
 | 
				
			||||||
 | 
						form := web.GetForm(ctx).(*forms.BlockUserForm)
 | 
				
			||||||
 | 
						if ctx.HasError() {
 | 
				
			||||||
 | 
							ctx.ServerError("FormValidation", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blockee, err := user_model.GetUserByName(ctx, form.Blockee)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetUserByName", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch form.Action {
 | 
				
			||||||
 | 
						case "block":
 | 
				
			||||||
 | 
							if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
 | 
				
			||||||
 | 
									ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									ctx.ServerError("BlockUser", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "unblock":
 | 
				
			||||||
 | 
							if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
 | 
				
			||||||
 | 
									ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									ctx.ServerError("UnblockUser", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "note":
 | 
				
			||||||
 | 
							block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("GetBlocking", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if block != nil {
 | 
				
			||||||
 | 
								if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("UpdateBlockingNote", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -72,6 +72,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 | 
				
			|||||||
	if _, ok := ctx.Data["NumFollowing"]; !ok {
 | 
						if _, ok := ctx.Data["NumFollowing"]; !ok {
 | 
				
			||||||
		_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
 | 
							_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.Doer != nil {
 | 
				
			||||||
 | 
							if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("GetBlocking", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Data["UserBlocking"] = block
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
 | 
					func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -339,7 +339,7 @@ func Action(ctx *context.Context) {
 | 
				
			|||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	switch ctx.FormString("action") {
 | 
						switch ctx.FormString("action") {
 | 
				
			||||||
	case "follow":
 | 
						case "follow":
 | 
				
			||||||
		err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 | 
							err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
 | 
				
			||||||
	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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								routers/web/user/setting/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								routers/web/user/setting/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						shared_user "code.gitea.io/gitea/routers/web/shared/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsers(ctx *context.Context) {
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("user.block.list")
 | 
				
			||||||
 | 
						ctx.Data["PageIsSettingsBlockedUsers"] = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						shared_user.BlockedUsers(ctx, ctx.Doer)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockedUsersPost(ctx *context.Context) {
 | 
				
			||||||
 | 
						shared_user.BlockedUsersPost(ctx, ctx.Doer)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) {
 | 
				
			|||||||
			})
 | 
								})
 | 
				
			||||||
			addWebhookEditRoutes()
 | 
								addWebhookEditRoutes()
 | 
				
			||||||
		}, webhooksEnabled)
 | 
							}, webhooksEnabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.Group("/blocked_users", func() {
 | 
				
			||||||
 | 
								m.Get("", user_setting.BlockedUsers)
 | 
				
			||||||
 | 
								m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
						}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m.Group("/user", func() {
 | 
						m.Group("/user", func() {
 | 
				
			||||||
@@ -945,6 +950,11 @@ func registerRoutes(m *web.Route) {
 | 
				
			|||||||
						m.Post("/rebuild", org.RebuildCargoIndex)
 | 
											m.Post("/rebuild", org.RebuildCargoIndex)
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
				}, packagesEnabled)
 | 
									}, packagesEnabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									m.Group("/blocked_users", func() {
 | 
				
			||||||
 | 
										m.Get("", org.BlockedUsers)
 | 
				
			||||||
 | 
										m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
			}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
 | 
								}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
 | 
				
			||||||
		}, context.OrgAssignment(true, true))
 | 
							}, context.OrgAssignment(true, true))
 | 
				
			||||||
	}, reqSignIn)
 | 
						}, reqSignIn)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if action == syncAdd && !isMember {
 | 
								if action == syncAdd && !isMember {
 | 
				
			||||||
				if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
 | 
									if err := models.AddTeamMember(ctx, team, user); err != nil {
 | 
				
			||||||
					log.Error("group sync: Could not add user to team: %v", err)
 | 
										log.Error("group sync: Could not add user to team: %v", err)
 | 
				
			||||||
					return err
 | 
										return err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else if action == syncRemove && isMember {
 | 
								} else if action == syncRemove && isMember {
 | 
				
			||||||
				if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
 | 
									if err := models.RemoveTeamMember(ctx, team, user); err != nil {
 | 
				
			||||||
					log.Error("group sync: Could not remove user from team: %v", err)
 | 
										log.Error("group sync: Could not remove user from team: %v", err)
 | 
				
			||||||
					return err
 | 
										return err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -449,3 +449,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
 | 
				
			|||||||
	ctx := context.GetValidateContext(req)
 | 
						ctx := context.GetValidateContext(req)
 | 
				
			||||||
	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 | 
						return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BlockUserForm struct {
 | 
				
			||||||
 | 
						Action  string `binding:"Required;In(block,unblock,note)"`
 | 
				
			||||||
 | 
						Blockee string `binding:"Required"`
 | 
				
			||||||
 | 
						Note    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
 | 
				
			||||||
 | 
						ctx := context.GetValidateContext(req)
 | 
				
			||||||
 | 
						return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						access_model "code.gitea.io/gitea/models/perm/access"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						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/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 | 
				
			|||||||
		return fmt.Errorf("cannot create reference with empty commit SHA")
 | 
							return fmt.Errorf("cannot create reference with empty commit SHA")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
 | 
				
			||||||
 | 
							if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if same reference from same commit has already existed.
 | 
						// Check if same reference from same commit has already existed.
 | 
				
			||||||
	has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
 | 
						has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
 | 
				
			||||||
		Type:      issues_model.CommentTypeCommitRef,
 | 
							Type:      issues_model.CommentTypeCommitRef,
 | 
				
			||||||
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CreateIssueComment creates a plain issue comment.
 | 
					// 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) {
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
 | 
				
			||||||
 | 
							if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
 | 
				
			||||||
 | 
								return nil, user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
						comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 | 
				
			||||||
		Type:        issues_model.CommentTypeComment,
 | 
							Type:        issues_model.CommentTypeComment,
 | 
				
			||||||
		Doer:        doer,
 | 
							Doer:        doer,
 | 
				
			||||||
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// UpdateComment updates information of comment.
 | 
					// UpdateComment updates information of comment.
 | 
				
			||||||
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
 | 
					func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
 | 
				
			||||||
 | 
						if err := c.LoadIssue(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := c.Issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
 | 
				
			||||||
 | 
							if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
 | 
						needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
 | 
				
			||||||
	if needsContentHistory {
 | 
						if needsContentHistory {
 | 
				
			||||||
		hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
 | 
							hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ package issue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html"
 | 
						"html"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
 | 
								message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
 | 
				
			||||||
			if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
 | 
								if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
 | 
				
			||||||
 | 
									if errors.Is(err, user_model.ErrBlockedUser) {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,12 +7,23 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						access_model "code.gitea.io/gitea/models/perm/access"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
						notify_service "code.gitea.io/gitea/services/notify"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ChangeContent changes issue content, as the given user.
 | 
					// ChangeContent changes issue content, as the given user.
 | 
				
			||||||
func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
 | 
					func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
 | 
				
			||||||
 | 
							if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	oldContent := issue.Content
 | 
						oldContent := issue.Content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
 | 
						if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ import (
 | 
				
			|||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	system_model "code.gitea.io/gitea/models/system"
 | 
						system_model "code.gitea.io/gitea/models/system"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/container"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
						notify_service "code.gitea.io/gitea/services/notify"
 | 
				
			||||||
@@ -22,6 +23,14 @@ 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 {
 | 
				
			||||||
 | 
						if err := issue.LoadPoster(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 | 
						if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -57,6 +66,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
 | 
				
			||||||
 | 
							if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
 | 
						if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -93,31 +112,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
 | 
				
			|||||||
// Pass one or more user logins to replace the set of assignees on this Issue.
 | 
					// Pass one or more user logins to replace the set of assignees on this Issue.
 | 
				
			||||||
// Send an empty array ([]) to clear all assignees from the Issue.
 | 
					// Send an empty array ([]) to clear all assignees from the Issue.
 | 
				
			||||||
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
 | 
					func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
 | 
				
			||||||
	var allNewAssignees []*user_model.User
 | 
						uniqueAssignees := container.SetOf(multipleAssignees...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Keep the old assignee thingy for compatibility reasons
 | 
						// Keep the old assignee thingy for compatibility reasons
 | 
				
			||||||
	if oneAssignee != "" {
 | 
						if oneAssignee != "" {
 | 
				
			||||||
		// Prevent double adding assignees
 | 
							uniqueAssignees.Add(oneAssignee)
 | 
				
			||||||
		var isDouble bool
 | 
					 | 
				
			||||||
		for _, assignee := range multipleAssignees {
 | 
					 | 
				
			||||||
			if assignee == oneAssignee {
 | 
					 | 
				
			||||||
				isDouble = true
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if !isDouble {
 | 
					 | 
				
			||||||
			multipleAssignees = append(multipleAssignees, oneAssignee)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Loop through all assignees to add them
 | 
						// Loop through all assignees to add them
 | 
				
			||||||
	for _, assigneeName := range multipleAssignees {
 | 
						allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
 | 
				
			||||||
 | 
						for _, assigneeName := range uniqueAssignees.Values() {
 | 
				
			||||||
		assignee, err := user_model.GetUserByName(ctx, assigneeName)
 | 
							assignee, err := user_model.GetUserByName(ctx, assigneeName)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
 | 
				
			||||||
 | 
								return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		allNewAssignees = append(allNewAssignees, assignee)
 | 
							allNewAssignees = append(allNewAssignees, assignee)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										50
									
								
								services/issue/reaction.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								services/issue/reaction.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package issue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateIssueReaction creates a reaction on an issue.
 | 
				
			||||||
 | 
					func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
 | 
				
			||||||
 | 
						if err := issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:    content,
 | 
				
			||||||
 | 
							DoerID:  doer.ID,
 | 
				
			||||||
 | 
							IssueID: issue.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateCommentReaction creates a reaction on a comment.
 | 
				
			||||||
 | 
					func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
 | 
				
			||||||
 | 
						if err := comment.LoadIssue(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := comment.Issue.LoadRepo(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
 | 
				
			||||||
 | 
							Type:      content,
 | 
				
			||||||
 | 
							DoerID:    doer.ID,
 | 
				
			||||||
 | 
							IssueID:   comment.Issue.ID,
 | 
				
			||||||
 | 
							CommentID: comment.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
					// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
				
			||||||
// SPDX-License-Identifier: MIT
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package issues_test
 | 
					package issue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
@@ -16,13 +16,13 @@ import (
 | 
				
			|||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
 | 
					func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
 | 
				
			||||||
	var reaction *issues_model.Reaction
 | 
						var reaction *issues_model.Reaction
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	if commentID == 0 {
 | 
						if comment == nil {
 | 
				
			||||||
		reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
 | 
							reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
 | 
							reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.NotNil(t, reaction)
 | 
						assert.NotNil(t, reaction)
 | 
				
			||||||
@@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						addReaction(t, user1, issue, nil, "heart")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, 0, "heart")
 | 
						unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 | 
				
			||||||
 | 
					 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestIssueAddDuplicateReaction(t *testing.T) {
 | 
					func TestIssueAddDuplicateReaction(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						addReaction(t, user1, issue, nil, "heart")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, 0, "heart")
 | 
						reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
 | 
				
			||||||
 | 
					 | 
				
			||||||
	reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
 | 
					 | 
				
			||||||
		DoerID:  user1.ID,
 | 
					 | 
				
			||||||
		IssueID: issue1ID,
 | 
					 | 
				
			||||||
		Type:    "heart",
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	assert.Error(t, err)
 | 
						assert.Error(t, err)
 | 
				
			||||||
	assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
 | 
						assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
 | 
						existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 | 
				
			||||||
	assert.Equal(t, existingR.ID, reaction.ID)
 | 
						assert.Equal(t, existingR.ID, reaction.ID)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						addReaction(t, user1, issue, nil, "heart")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, 0, "heart")
 | 
						err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
 | 
				
			||||||
 | 
					 | 
				
			||||||
	err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart")
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
 | 
						unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestIssueReactionCount(t *testing.T) {
 | 
					func TestIssueReactionCount(t *testing.T) {
 | 
				
			||||||
@@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) {
 | 
				
			|||||||
	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
	ghost := user_model.NewGhostUser()
 | 
						ghost := user_model.NewGhostUser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issueID int64 = 2
 | 
						issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issueID, 0, "heart")
 | 
						addReaction(t, user1, issue, nil, "heart")
 | 
				
			||||||
	addReaction(t, user2.ID, issueID, 0, "heart")
 | 
						addReaction(t, user2, issue, nil, "heart")
 | 
				
			||||||
	addReaction(t, org3.ID, issueID, 0, "heart")
 | 
						addReaction(t, org3, issue, nil, "heart")
 | 
				
			||||||
	addReaction(t, org3.ID, issueID, 0, "+1")
 | 
						addReaction(t, org3, issue, nil, "+1")
 | 
				
			||||||
	addReaction(t, user4.ID, issueID, 0, "+1")
 | 
						addReaction(t, user4, issue, nil, "+1")
 | 
				
			||||||
	addReaction(t, user4.ID, issueID, 0, "heart")
 | 
						addReaction(t, user4, issue, nil, "heart")
 | 
				
			||||||
	addReaction(t, ghost.ID, issueID, 0, "-1")
 | 
						addReaction(t, ghost, issue, nil, "-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 | 
						reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 | 
				
			||||||
		IssueID: issueID,
 | 
							IssueID: issue.ID,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, reactionsList, 7)
 | 
						assert.Len(t, reactionsList, 7)
 | 
				
			||||||
@@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						addReaction(t, user1, nil, comment, "heart")
 | 
				
			||||||
	var comment1ID int64 = 1
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
 | 
						unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 | 
				
			||||||
 | 
					 | 
				
			||||||
	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestIssueCommentDeleteReaction(t *testing.T) {
 | 
					func TestIssueCommentDeleteReaction(t *testing.T) {
 | 
				
			||||||
@@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
 | 
				
			|||||||
	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
						org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
				
			||||||
	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 | 
				
			||||||
	var comment1ID int64 = 1
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
 | 
						addReaction(t, user1, nil, comment, "heart")
 | 
				
			||||||
	addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
 | 
						addReaction(t, user2, nil, comment, "heart")
 | 
				
			||||||
	addReaction(t, org3.ID, issue1ID, comment1ID, "heart")
 | 
						addReaction(t, org3, nil, comment, "heart")
 | 
				
			||||||
	addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
 | 
						addReaction(t, user4, nil, comment, "+1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 | 
						reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 | 
				
			||||||
		IssueID:   issue1ID,
 | 
							IssueID:   comment.IssueID,
 | 
				
			||||||
		CommentID: comment1ID,
 | 
							CommentID: comment.ID,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Len(t, reactionsList, 4)
 | 
						assert.Len(t, reactionsList, 4)
 | 
				
			||||||
@@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) {
 | 
				
			|||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var issue1ID int64 = 1
 | 
						addReaction(t, user1, nil, comment, "heart")
 | 
				
			||||||
	var comment1ID int64 = 1
 | 
						assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
 | 
						unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 | 
				
			||||||
	assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -40,6 +40,14 @@ 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 {
 | 
				
			||||||
 | 
						if err := issue.LoadPoster(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 | 
						prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if !git_model.IsErrBranchNotExist(err) {
 | 
							if !git_model.IsErrBranchNotExist(err) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,13 +11,14 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	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"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeleteCollaboration removes collaboration relation between the user and repository.
 | 
					// DeleteCollaboration removes collaboration relation between the user and repository.
 | 
				
			||||||
func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
 | 
					func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
 | 
				
			||||||
	collaboration := &repo_model.Collaboration{
 | 
						collaboration := &repo_model.Collaboration{
 | 
				
			||||||
		RepoID: repo.ID,
 | 
							RepoID: repo.ID,
 | 
				
			||||||
		UserID: uid,
 | 
							UserID: collaborator.ID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext(ctx)
 | 
						ctx, committer, err := db.TxContext(ctx)
 | 
				
			||||||
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
 | 
				
			|||||||
	} else if has == 0 {
 | 
						} else if has == 0 {
 | 
				
			||||||
		return committer.Commit()
 | 
							return committer.Commit()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := repo.LoadOwner(ctx); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
 | 
						if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
 | 
						if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
 | 
						if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Unassign a user from any issue (s)he has been assigned to in the repository
 | 
						// Unassign a user from any issue (s)he has been assigned to in the repository
 | 
				
			||||||
	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
 | 
						if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
 | 
				
			||||||
		return err
 | 
							return 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"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -16,13 +17,15 @@ import (
 | 
				
			|||||||
func TestRepository_DeleteCollaboration(t *testing.T) {
 | 
					func TestRepository_DeleteCollaboration(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
 | 
						repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
 | 
				
			||||||
	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
 | 
					 | 
				
			||||||
	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
 | 
					 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
 | 
						assert.NoError(t, repo.LoadOwner(db.DefaultContext))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
 | 
						assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
 | 
				
			||||||
 | 
						unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
						unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -365,24 +365,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
 | 
						teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
 | 
				
			||||||
 | 
							TeamID: t.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("getTeamUsersByTeamID: %w", err)
 | 
							return fmt.Errorf("GetTeamMembers: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, teamUser := range teamUsers {
 | 
						for _, member := range teamMembers {
 | 
				
			||||||
		has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
 | 
							has, err := access_model.HasAccess(ctx, member.ID, repo)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		} else if has {
 | 
							} else if has {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
 | 
							if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Remove all IssueWatches a user has subscribed to in the repositories
 | 
							// Remove all IssueWatches a user has subscribed to in the repositories
 | 
				
			||||||
		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
 | 
							if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,6 +53,14 @@ type ForkRepoOptions struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ForkRepository forks a repository
 | 
					// ForkRepository forks a repository
 | 
				
			||||||
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
 | 
					func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
 | 
				
			||||||
 | 
						if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
 | 
				
			||||||
 | 
							return nil, user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fork is prohibited, if user has reached maximum limit of repositories
 | 
						// Fork is prohibited, if user has reached maximum limit of repositories
 | 
				
			||||||
	if !owner.CanForkRepo() {
 | 
						if !owner.CanForkRepo() {
 | 
				
			||||||
		return nil, repo_model.ErrReachLimitOfRepo{
 | 
							return nil, repo_model.ErrReachLimitOfRepo{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -139,9 +139,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Remove redundant collaborators.
 | 
						// Remove redundant collaborators.
 | 
				
			||||||
	collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
 | 
						collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("getCollaborators: %w", err)
 | 
							return fmt.Errorf("GetCollaborators: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Dummy object.
 | 
						// Dummy object.
 | 
				
			||||||
@@ -201,13 +201,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 | 
				
			|||||||
		return fmt.Errorf("decrease old owner repository count: %w", err)
 | 
							return fmt.Errorf("decrease old owner repository count: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
 | 
						if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 | 
				
			||||||
		return fmt.Errorf("watchRepo: %w", err)
 | 
							return fmt.Errorf("watchRepo: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Remove watch for organization.
 | 
						// Remove watch for organization.
 | 
				
			||||||
	if oldOwner.IsOrganization() {
 | 
						if oldOwner.IsOrganization() {
 | 
				
			||||||
		if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
 | 
							if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
 | 
				
			||||||
			return fmt.Errorf("watchRepo [false]: %w", err)
 | 
								return fmt.Errorf("watchRepo [false]: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -371,6 +371,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
 | 
				
			|||||||
		return TransferOwnership(ctx, doer, newOwner, repo, teams)
 | 
							return TransferOwnership(ctx, doer, newOwner, repo, teams)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
 | 
				
			||||||
 | 
							return user_model.ErrBlockedUser
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If new owner is an org and user can create repos he can transfer directly too
 | 
						// If new owner is an org and user can create repos he can transfer directly too
 | 
				
			||||||
	if newOwner.IsOrganization() {
 | 
						if newOwner.IsOrganization() {
 | 
				
			||||||
		allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
 | 
							allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										308
									
								
								services/user/block.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								services/user/block.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,308 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						org_model "code.gitea.io/gitea/models/organization"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
 | 
				
			||||||
 | 
						if blocker.ID == blockee.ID {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if doer.ID == blockee.ID {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if blockee.IsOrganization() {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if blocker.IsOrganization() {
 | 
				
			||||||
 | 
							org := org_model.OrgFromUser(blocker)
 | 
				
			||||||
 | 
							if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if !doer.IsAdmin && doer.ID != blocker.ID {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
 | 
				
			||||||
 | 
						if doer.ID == blockee.ID {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if blocker.IsOrganization() {
 | 
				
			||||||
 | 
							org := org_model.OrgFromUser(blocker)
 | 
				
			||||||
 | 
							if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if !doer.IsAdmin && doer.ID != blocker.ID {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
 | 
				
			||||||
 | 
						if blockee.IsOrganization() {
 | 
				
			||||||
 | 
							return user_model.ErrBlockOrganization
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !CanBlockUser(ctx, doer, blocker, blockee) {
 | 
				
			||||||
 | 
							return user_model.ErrCanNotBlock
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return db.WithTx(ctx, func(ctx context.Context) error {
 | 
				
			||||||
 | 
							// unfollow each other
 | 
				
			||||||
 | 
							if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// unstar each other
 | 
				
			||||||
 | 
							if err := unstarRepos(ctx, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := unstarRepos(ctx, blockee, blocker); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// unwatch each others repositories
 | 
				
			||||||
 | 
							if err := unwatchRepos(ctx, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := unwatchRepos(ctx, blockee, blocker); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// unassign each other from issues
 | 
				
			||||||
 | 
							if err := unassignIssues(ctx, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := unassignIssues(ctx, blockee, blocker); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// remove each other from repository collaborations
 | 
				
			||||||
 | 
							if err := removeCollaborations(ctx, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := removeCollaborations(ctx, blockee, blocker); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// cancel each other repository transfers
 | 
				
			||||||
 | 
							if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return db.Insert(ctx, &user_model.Blocking{
 | 
				
			||||||
 | 
								BlockerID: blocker.ID,
 | 
				
			||||||
 | 
								BlockeeID: blockee.ID,
 | 
				
			||||||
 | 
								Note:      note,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
 | 
				
			||||||
 | 
						opts := &repo_model.StarredReposOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{
 | 
				
			||||||
 | 
								Page:     1,
 | 
				
			||||||
 | 
								PageSize: 25,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							StarrerID:   starrer.ID,
 | 
				
			||||||
 | 
							RepoOwnerID: repoOwner.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							repos, err := repo_model.GetStarredRepos(ctx, opts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(repos) == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, repo := range repos {
 | 
				
			||||||
 | 
								if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							opts.Page++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
 | 
				
			||||||
 | 
						opts := &repo_model.WatchedReposOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{
 | 
				
			||||||
 | 
								Page:     1,
 | 
				
			||||||
 | 
								PageSize: 25,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							WatcherID:   watcher.ID,
 | 
				
			||||||
 | 
							RepoOwnerID: repoOwner.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(repos) == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, repo := range repos {
 | 
				
			||||||
 | 
								if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							opts.Page++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
 | 
				
			||||||
 | 
						transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
 | 
				
			||||||
 | 
							SenderID:    sender.ID,
 | 
				
			||||||
 | 
							RecipientID: recipient.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, transfer := range transfers {
 | 
				
			||||||
 | 
							repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
 | 
				
			||||||
 | 
						opts := &issues_model.AssignedIssuesOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{
 | 
				
			||||||
 | 
								Page:     1,
 | 
				
			||||||
 | 
								PageSize: 25,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AssigneeID:  assignee.ID,
 | 
				
			||||||
 | 
							RepoOwnerID: repoOwner.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(issues) == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, issue := range issues {
 | 
				
			||||||
 | 
								if err := issue.LoadAssignees(ctx); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							opts.Page++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
 | 
				
			||||||
 | 
						opts := &repo_model.FindCollaborationOptions{
 | 
				
			||||||
 | 
							ListOptions: db.ListOptions{
 | 
				
			||||||
 | 
								Page:     1,
 | 
				
			||||||
 | 
								PageSize: 25,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							CollaboratorID: collaborator.ID,
 | 
				
			||||||
 | 
							RepoOwnerID:    repoOwner.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(collaborations) == 0 {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, collaboration := range collaborations {
 | 
				
			||||||
 | 
								repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							opts.Page++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
 | 
				
			||||||
 | 
						if blockee.IsOrganization() {
 | 
				
			||||||
 | 
							return user_model.ErrBlockOrganization
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !CanUnblockUser(ctx, doer, blocker, blockee) {
 | 
				
			||||||
 | 
							return user_model.ErrCanNotUnblock
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return db.WithTx(ctx, func(ctx context.Context) error {
 | 
				
			||||||
 | 
							block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if block != nil {
 | 
				
			||||||
 | 
								_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								services/user/block_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								services/user/block_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCanBlockUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
				
			||||||
 | 
						user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 | 
				
			||||||
 | 
						org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Doer can't self block
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
 | 
				
			||||||
 | 
						// Blocker can't be blockee
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
 | 
				
			||||||
 | 
						// Can't block already blocked user
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
 | 
				
			||||||
 | 
						// Blockee can't be an organization
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
 | 
				
			||||||
 | 
						// Doer must be blocker or admin
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
 | 
				
			||||||
 | 
						// Organization can't block a member
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
 | 
				
			||||||
 | 
						// Doer must be organization owner or admin if blocker is an organization
 | 
				
			||||||
 | 
						assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
 | 
				
			||||||
 | 
						assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
 | 
				
			||||||
 | 
						assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCanUnblockUser(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
						user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
						user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
 | 
				
			||||||
 | 
						user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 | 
				
			||||||
 | 
						org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Doer can't self unblock
 | 
				
			||||||
 | 
						assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
 | 
				
			||||||
 | 
						// Can't unblock not blocked user
 | 
				
			||||||
 | 
						assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
 | 
				
			||||||
 | 
						// Doer must be blocker or admin
 | 
				
			||||||
 | 
						assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
 | 
				
			||||||
 | 
						// Doer must be organization owner or admin if blocker is an organization
 | 
				
			||||||
 | 
						assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
 | 
				
			||||||
 | 
						assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
 | 
				
			||||||
 | 
						assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -92,6 +92,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.Blocking{BlockerID: u.ID},
 | 
				
			||||||
 | 
							&user_model.Blocking{BlockeeID: u.ID},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return fmt.Errorf("deleteBeans: %w", err)
 | 
							return fmt.Errorf("deleteBeans: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 | 
				
			|||||||
				break
 | 
									break
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			for _, org := range orgs {
 | 
								for _, org := range orgs {
 | 
				
			||||||
				if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
 | 
									if err := models.RemoveOrgUser(ctx, org, u); err != nil {
 | 
				
			||||||
					if organization.IsErrLastOrgOwner(err) {
 | 
										if organization.IsErrLastOrgOwner(err) {
 | 
				
			||||||
						err = org_service.DeleteOrganization(ctx, org, true)
 | 
											err = org_service.DeleteOrganization(ctx, org, true)
 | 
				
			||||||
						if err != nil {
 | 
											if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,8 @@ func TestDeleteUser(t *testing.T) {
 | 
				
			|||||||
		orgUsers := make([]*organization.OrgUser, 0, 10)
 | 
							orgUsers := make([]*organization.OrgUser, 0, 10)
 | 
				
			||||||
		assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
 | 
							assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
 | 
				
			||||||
		for _, orgUser := range orgUsers {
 | 
							for _, orgUser := range orgUsers {
 | 
				
			||||||
			if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
 | 
								org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
 | 
				
			||||||
 | 
								if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
 | 
				
			||||||
				assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
									assert.True(t, organization.IsErrLastOrgOwner(err))
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								templates/org/settings/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								templates/org/settings/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}}
 | 
				
			||||||
 | 
					<div class="org-setting-content">
 | 
				
			||||||
 | 
						{{template "shared/user/blocked_users" .}}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "org/settings/layout_footer" .}}
 | 
				
			||||||
@@ -17,6 +17,9 @@
 | 
				
			|||||||
			{{ctx.Locale.Tr "settings.applications"}}
 | 
								{{ctx.Locale.Tr "settings.applications"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
 | 
							<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
 | 
				
			||||||
 | 
								{{ctx.Locale.Tr "user.block.list"}}
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
		{{if .EnablePackages}}
 | 
							{{if .EnablePackages}}
 | 
				
			||||||
		<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
 | 
							<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
 | 
				
			||||||
			{{ctx.Locale.Tr "packages.title"}}
 | 
								{{ctx.Locale.Tr "packages.title"}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -251,5 +251,6 @@
 | 
				
			|||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
	{{if (not .DiffNotAvailable)}}
 | 
						{{if (not .DiffNotAvailable)}}
 | 
				
			||||||
		{{template "repo/issue/view_content/reference_issue_dialog" .}}
 | 
							{{template "repo/issue/view_content/reference_issue_dialog" .}}
 | 
				
			||||||
 | 
							{{template "shared/user/block_user_dialog" .}}
 | 
				
			||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -170,6 +170,7 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{template "repo/issue/view_content/reference_issue_dialog" .}}
 | 
					{{template "repo/issue/view_content/reference_issue_dialog" .}}
 | 
				
			||||||
 | 
					{{template "shared/user/block_user_dialog" .}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="gt-hidden" id="no-content">
 | 
					<div class="gt-hidden" id="no-content">
 | 
				
			||||||
	<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 | 
						<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,10 @@
 | 
				
			|||||||
			{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
								{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
		<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
 | 
							<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
 | 
				
			||||||
		{{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}}
 | 
							{{if .ctxData.IsSigned}}
 | 
				
			||||||
 | 
								{{$needDivider := false}}
 | 
				
			||||||
 | 
								{{if not .ctxData.Repository.IsArchived}}
 | 
				
			||||||
 | 
									{{$needDivider = true}}
 | 
				
			||||||
				<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
 | 
									<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
 | 
				
			||||||
				{{if not .ctxData.UnitIssuesGlobalDisabled}}
 | 
									{{if not .ctxData.UnitIssuesGlobalDisabled}}
 | 
				
			||||||
					<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
 | 
										<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
 | 
				
			||||||
@@ -23,5 +26,19 @@
 | 
				
			|||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
 | 
								{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
 | 
				
			||||||
 | 
								{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
 | 
				
			||||||
 | 
								{{if or $canOrgBlock $canUserBlock}}
 | 
				
			||||||
 | 
									{{if $needDivider}}
 | 
				
			||||||
 | 
										<div class="divider"></div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
									{{if $canUserBlock}}
 | 
				
			||||||
 | 
									<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
									{{if $canOrgBlock}}
 | 
				
			||||||
 | 
									<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
 | 
							{{end}}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								templates/shared/user/block_user_dialog.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								templates/shared/user/block_user_dialog.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					<div class="ui small modal" id="block-user-modal">
 | 
				
			||||||
 | 
						<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div>
 | 
				
			||||||
 | 
							<form class="ui form modal-form" method="post">
 | 
				
			||||||
 | 
								{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
								<input type="hidden" name="action" value="block" />
 | 
				
			||||||
 | 
								<input type="hidden" name="blockee" class="modal-blockee" />
 | 
				
			||||||
 | 
								<div class="field">
 | 
				
			||||||
 | 
									<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="field">
 | 
				
			||||||
 | 
									<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
 | 
				
			||||||
 | 
									<input id="block-note" name="note">
 | 
				
			||||||
 | 
									<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="text right actions">
 | 
				
			||||||
 | 
									<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
 | 
				
			||||||
 | 
									<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										83
									
								
								templates/shared/user/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								templates/shared/user/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					<h4 class="ui top attached header">
 | 
				
			||||||
 | 
						{{ctx.Locale.Tr "user.block.title"}}
 | 
				
			||||||
 | 
					</h4>
 | 
				
			||||||
 | 
					<div class="ui attached segment">
 | 
				
			||||||
 | 
						<p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
 | 
				
			||||||
 | 
						<ul>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
 | 
				
			||||||
 | 
							<li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
 | 
				
			||||||
 | 
						</ul>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="ui segment">
 | 
				
			||||||
 | 
						<form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
 | 
				
			||||||
 | 
							{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
							<input type="hidden" name="action" value="block" />
 | 
				
			||||||
 | 
							<div id="search-user-box" class="field ui fluid search input">
 | 
				
			||||||
 | 
								<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
 | 
				
			||||||
 | 
								<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="field">
 | 
				
			||||||
 | 
								<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
 | 
				
			||||||
 | 
								<input name="note">
 | 
				
			||||||
 | 
								<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<h4 class="ui top attached header">
 | 
				
			||||||
 | 
						{{ctx.Locale.Tr "user.block.list"}}
 | 
				
			||||||
 | 
					</h4>
 | 
				
			||||||
 | 
					<div class="ui attached segment">
 | 
				
			||||||
 | 
						<div class="flex-list">
 | 
				
			||||||
 | 
							{{range .UserBlocks}}
 | 
				
			||||||
 | 
								<div class="flex-item">
 | 
				
			||||||
 | 
									<div class="flex-item-leading">
 | 
				
			||||||
 | 
										{{ctx.AvatarUtils.Avatar .Blockee}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="flex-item-main">
 | 
				
			||||||
 | 
										<div class="flex-item-title">
 | 
				
			||||||
 | 
											<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										{{if .Note}}
 | 
				
			||||||
 | 
										<div class="flex-item-body">
 | 
				
			||||||
 | 
											<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="flex-item-trailing">
 | 
				
			||||||
 | 
										<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
 | 
				
			||||||
 | 
										<form action="{{$.Link}}" method="post">
 | 
				
			||||||
 | 
											{{$.CsrfTokenHtml}}
 | 
				
			||||||
 | 
											<input type="hidden" name="action" value="unblock" />
 | 
				
			||||||
 | 
											<input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
 | 
				
			||||||
 | 
											<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
 | 
				
			||||||
 | 
										</form>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{{else}}
 | 
				
			||||||
 | 
								<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
 | 
				
			||||||
 | 
							{{end}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="ui small modal" id="block-user-note-modal">
 | 
				
			||||||
 | 
						<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<form class="ui form" action="{{$.Link}}" method="post">
 | 
				
			||||||
 | 
								{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
								<input type="hidden" name="action" value="note" />
 | 
				
			||||||
 | 
								<input type="hidden" name="blockee" class="modal-blockee" />
 | 
				
			||||||
 | 
								<div class="field">
 | 
				
			||||||
 | 
									<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
 | 
				
			||||||
 | 
									<input name="note" class="modal-note" />
 | 
				
			||||||
 | 
									<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="text right actions">
 | 
				
			||||||
 | 
									<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
 | 
				
			||||||
 | 
									<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -27,6 +27,12 @@
 | 
				
			|||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="extra content gt-word-break">
 | 
						<div class="extra content gt-word-break">
 | 
				
			||||||
		<ul>
 | 
							<ul>
 | 
				
			||||||
 | 
								{{if .UserBlocking}}
 | 
				
			||||||
 | 
									<li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li>
 | 
				
			||||||
 | 
									{{if .UserBlocking.Note}}
 | 
				
			||||||
 | 
										<li class="text small red">{{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}</li>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
			{{if .ContextUser.Location}}
 | 
								{{if .ContextUser.Location}}
 | 
				
			||||||
				<li>
 | 
									<li>
 | 
				
			||||||
					{{svg "octicon-location"}}
 | 
										{{svg "octicon-location"}}
 | 
				
			||||||
@@ -109,7 +115,8 @@
 | 
				
			|||||||
			</li>
 | 
								</li>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
			{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
 | 
								{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
 | 
				
			||||||
			<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" >
 | 
									{{if not .UserBlocking}}
 | 
				
			||||||
 | 
									<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
 | 
				
			||||||
					{{if $.IsFollowing}}
 | 
										{{if $.IsFollowing}}
 | 
				
			||||||
						<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
 | 
											<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
 | 
				
			||||||
							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
 | 
												{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
 | 
				
			||||||
@@ -121,6 +128,16 @@
 | 
				
			|||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
 | 
									<li>
 | 
				
			||||||
 | 
										{{if not .UserBlocking}}
 | 
				
			||||||
 | 
											<a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a>
 | 
				
			||||||
 | 
										{{else}}
 | 
				
			||||||
 | 
											<a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
		</ul>
 | 
							</ul>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{template "shared/user/block_user_dialog" .}}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										283
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										283
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -1955,6 +1955,151 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/orgs/{org}/blocks": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "List users blocked by the organization",
 | 
				
			||||||
 | 
					        "operationId": "organizationListBlocks",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the organization",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "integer",
 | 
				
			||||||
 | 
					            "description": "page number of results to return (1-based)",
 | 
				
			||||||
 | 
					            "name": "page",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "integer",
 | 
				
			||||||
 | 
					            "description": "page size of results",
 | 
				
			||||||
 | 
					            "name": "limit",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/UserList"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "/orgs/{org}/blocks/{username}": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Check if a user is blocked by the organization",
 | 
				
			||||||
 | 
					        "operationId": "organizationCheckUserBlock",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the organization",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to check",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Block a user",
 | 
				
			||||||
 | 
					        "operationId": "organizationBlockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the organization",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to block",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "optional note for the block",
 | 
				
			||||||
 | 
					            "name": "note",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "delete": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "organization"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Unblock a user",
 | 
				
			||||||
 | 
					        "operationId": "organizationUnblockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "name of the organization",
 | 
				
			||||||
 | 
					            "name": "org",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to unblock",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/orgs/{org}/hooks": {
 | 
					    "/orgs/{org}/hooks": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
@@ -4340,6 +4485,9 @@
 | 
				
			|||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -6692,6 +6840,9 @@
 | 
				
			|||||||
          "400": {
 | 
					          "400": {
 | 
				
			||||||
            "$ref": "#/responses/error"
 | 
					            "$ref": "#/responses/error"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/error"
 | 
					            "$ref": "#/responses/error"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -10461,6 +10612,9 @@
 | 
				
			|||||||
          "201": {
 | 
					          "201": {
 | 
				
			||||||
            "$ref": "#/responses/PullRequest"
 | 
					            "$ref": "#/responses/PullRequest"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -12959,6 +13113,9 @@
 | 
				
			|||||||
          "200": {
 | 
					          "200": {
 | 
				
			||||||
            "$ref": "#/responses/WatchInfo"
 | 
					            "$ref": "#/responses/WatchInfo"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -14513,6 +14670,9 @@
 | 
				
			|||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -15081,6 +15241,123 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/user/blocks": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "List users blocked by the authenticated user",
 | 
				
			||||||
 | 
					        "operationId": "userListBlocks",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "integer",
 | 
				
			||||||
 | 
					            "description": "page number of results to return (1-based)",
 | 
				
			||||||
 | 
					            "name": "page",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "integer",
 | 
				
			||||||
 | 
					            "description": "page size of results",
 | 
				
			||||||
 | 
					            "name": "limit",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/UserList"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "/user/blocks/{username}": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Check if a user is blocked by the authenticated user",
 | 
				
			||||||
 | 
					        "operationId": "userCheckUserBlock",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to check",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "put": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Block a user",
 | 
				
			||||||
 | 
					        "operationId": "userBlockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to block",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "optional note for the block",
 | 
				
			||||||
 | 
					            "name": "note",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "delete": {
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Unblock a user",
 | 
				
			||||||
 | 
					        "operationId": "userUnblockUser",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "user to unblock",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "204": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "422": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/user/emails": {
 | 
					    "/user/emails": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
@@ -15258,6 +15535,9 @@
 | 
				
			|||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -15965,6 +16245,9 @@
 | 
				
			|||||||
          "204": {
 | 
					          "204": {
 | 
				
			||||||
            "$ref": "#/responses/empty"
 | 
					            "$ref": "#/responses/empty"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "403": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "404": {
 | 
					          "404": {
 | 
				
			||||||
            "$ref": "#/responses/notFound"
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								templates/user/settings/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								templates/user/settings/blocked_users.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
 | 
				
			||||||
 | 
						<div class="user-setting-content">
 | 
				
			||||||
 | 
							{{template "shared/user/blocked_users" .}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					{{template "user/settings/layout_footer" .}}
 | 
				
			||||||
@@ -13,6 +13,9 @@
 | 
				
			|||||||
		<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
 | 
							<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
 | 
				
			||||||
			{{ctx.Locale.Tr "settings.security"}}
 | 
								{{ctx.Locale.Tr "settings.security"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
 | 
							<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
 | 
				
			||||||
 | 
								{{ctx.Locale.Tr "user.block.list"}}
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
		<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
 | 
							<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
 | 
				
			||||||
			{{ctx.Locale.Tr "settings.applications"}}
 | 
								{{ctx.Locale.Tr "settings.applications"}}
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user