mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +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{ | ||||||
| 		RepoID: ctx.Repo.Repository.ID, | 		ListOptions: utils.GetListOptions(ctx), | ||||||
|  | 		RepoID:      ctx.Repo.Repository.ID, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.InternalServerError(err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) |  | ||||||
| 	if err != nil { | 	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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "AddCollaborator", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 			ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) | 			if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 				ctx.Error(http.StatusForbidden, "UpdateAssignees", err) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) | ||||||
|  | 			} | ||||||
| 			return | 			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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "CreateIssueComment", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateComment", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "UpdateComment", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "UpdateComment", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.ServerError("UpdateComment", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "UpdateComment", err) | ||||||
|  | 		} else { | ||||||
|  | 			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 | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ctx.InternalServerError(err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "BlockedUser", err) | ||||||
|  | 		} else { | ||||||
|  | 			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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "FollowUser", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "FollowUser", err) | ||||||
|  | 		} else { | ||||||
|  | 			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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "StarRepo", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "BlockedUser", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "StarRepo", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "WatchRepo", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Error(http.StatusForbidden, "BlockedUser", err) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "WatchRepo", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.ServerError("ChangeContent", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("ChangeContent", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.ServerError("CreateIssueComment", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("CreateIssueComment", err) | ||||||
|  | 		} | ||||||
| 		return | 		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 { | ||||||
| 		ctx.ServerError("UpdateComment", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("UpdateComment", err) | ||||||
|  | 		} | ||||||
| 		return | 		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) | ||||||
| 			return | 		} else if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ | ||||||
|  | 				"Message": ctx.Tr("repo.pulls.push_rejected"), | ||||||
|  | 				"Summary": ctx.Tr("repo.pulls.new.blocked_user"), | ||||||
|  | 			}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("CompareAndPullRequest.HTMLString", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			ctx.JSONError(flashError) | ||||||
| 		} | 		} | ||||||
| 		ctx.ServerError("NewPullRequest", err) |  | ||||||
| 		return | 		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,8 +336,12 @@ func Action(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
| 		return | 			ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch ctx.Params(":action") { | 	switch ctx.Params(":action") { | ||||||
|   | |||||||
| @@ -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 { | ||||||
| 		ctx.ServerError("AddCollaborator", err) | 		if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
|  | 			ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user")) | ||||||
|  | 			ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("AddCollaborator", err) | ||||||
|  | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -126,10 +131,19 @@ 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 { | ||||||
| 		ctx.Flash.Error("DeleteCollaboration: " + err.Error()) | 		if user_model.IsErrUserNotExist(err) { | ||||||
|  | 			ctx.Flash.Error(ctx.Tr("form.user_not_exist")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("GetUserByName", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) | 		if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil { | ||||||
|  | 			ctx.Flash.Error("DeleteCollaboration: " + err.Error()) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration") | 	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,16 +10,33 @@ | |||||||
| 			{{$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}} | ||||||
| 			<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> | 			{{$needDivider := false}} | ||||||
| 			{{if not .ctxData.UnitIssuesGlobalDisabled}} | 			{{if not .ctxData.Repository.IsArchived}} | ||||||
| 				<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> | 				{{$needDivider = true}} | ||||||
|  | 				<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div> | ||||||
|  | 				{{if not .ctxData.UnitIssuesGlobalDisabled}} | ||||||
|  | 					<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div> | ||||||
|  | 				{{end}} | ||||||
|  | 				{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}} | ||||||
|  | 					<div class="divider"></div> | ||||||
|  | 					<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div> | ||||||
|  | 					{{if .delete}} | ||||||
|  | 						<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div> | ||||||
|  | 					{{end}} | ||||||
|  | 				{{end}} | ||||||
| 			{{end}} | 			{{end}} | ||||||
| 			{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}} | 			{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}} | ||||||
| 				<div class="divider"></div> | 			{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}} | ||||||
| 				<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div> | 			{{if or $canOrgBlock $canUserBlock}} | ||||||
| 				{{if .delete}} | 				{{if $needDivider}} | ||||||
| 					<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div> | 					<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}} | 			{{end}} | ||||||
| 		{{end}} | 		{{end}} | ||||||
|   | |||||||
							
								
								
									
										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,18 +115,29 @@ | |||||||
| 			</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}} | ||||||
| 				{{if $.IsFollowing}} | 				<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card"> | ||||||
| 					<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button"> | 					{{if $.IsFollowing}} | ||||||
| 						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}} | 						<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button"> | ||||||
| 					</button> | 							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}} | ||||||
| 				{{else}} | 						</button> | ||||||
| 					<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button"> | 					{{else}} | ||||||
| 						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}} | 						<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button"> | ||||||
| 					</button> | 							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}} | ||||||
|  | 						</button> | ||||||
|  | 					{{end}} | ||||||
|  | 				</li> | ||||||
| 				{{end}} | 				{{end}} | ||||||
| 			</li> | 				<li> | ||||||
|  | 					{{if not .UserBlocking}} | ||||||
|  | 						<a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a> | ||||||
|  | 					{{else}} | ||||||
|  | 						<a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a> | ||||||
|  | 					{{end}} | ||||||
|  | 				</li> | ||||||
| 			{{end}} | 			{{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
	 KN4CK3R
					KN4CK3R