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:


![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b)

---------

Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
KN4CK3R 2024-03-04 09:16:03 +01:00 committed by GitHub
parent 8e12ba34ba
commit c337ff0ec7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 2878 additions and 548 deletions

View 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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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)

View File

@ -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)
} }

View File

@ -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

View File

@ -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{

View File

@ -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

View 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{})
}

View File

@ -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
} }
} }

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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{})
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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())

View File

@ -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) {

View File

@ -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
} }
} }

View File

@ -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)

View File

@ -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 {
sess = sess.And("is_private=?", false)
} }
if listOptions.Page != 0 { type WatchedReposOptions struct {
sess = db.SetSessionPagination(sess, &listOptions) db.ListOptions
WatcherID int64
repos := make([]*Repository, 0, listOptions.PageSize) RepoOwnerID int64
return repos, sess.Find(&repos) IncludePrivate bool
} }
repos := make([]*Repository, 0, 10) func (opts *WatchedReposOptions) ToConds() builder.Cond {
return repos, sess.Find(&repos) var cond builder.Cond = builder.Eq{
"watch.user_id": opts.WatcherID,
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{
"repository.is_private": false,
})
}
return cond.And(builder.Neq{
"watch.mode": WatchModeDont,
})
}
func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
return nil
},
}
} }
// 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

View File

@ -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) {

View File

@ -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.

View File

@ -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)
}

View File

@ -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
View 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
}

View File

@ -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()

View File

@ -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

View File

@ -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{})
} }

View File

@ -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,

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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
View 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())
}

View File

@ -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)

View File

@ -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
} }

View File

@ -8,7 +8,6 @@ import (
"errors" "errors"
"net/http" "net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{ collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
}) })
if err != nil {
ctx.InternalServerError(err)
return
}
collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return return
@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer) users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
} }
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, users) ctx.JSON(http.StatusOK, users)
} }
@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
// responses: // responses:
// "204": // "204":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422": // "422":
@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
} }
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil { if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "AddCollaborator", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
}
return return
} }
@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
return return
} }
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil { if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
return return
} }

View File

@ -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)
} }

View File

@ -5,6 +5,7 @@
package repo package repo
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueOption) form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) { if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) {
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
return } else if errors.Is(err, user_model.ErrBlockedUser) {
} ctx.Error(http.StatusForbidden, "NewIssue", err)
} else {
ctx.Error(http.StatusInternalServerError, "NewIssue", err) ctx.Error(http.StatusInternalServerError, "NewIssue", err)
}
return return
} }
@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
}
return return
} }
} }

View File

@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueCommentOption) form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil { if err != nil {
@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
}
return return
} }
@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.EditIssueCommentOption) form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form) editIssueComment(ctx, *form)
} }
@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
oldContent := comment.Content oldContent := comment.Content
comment.Content = form.Body comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateComment", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateComment", err) ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
}
return return
} }

View File

@ -4,10 +4,12 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment" // "$ref": "#/responses/Attachment"
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "423": // "423":
@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
} }
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil { if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateComment", err)
} else {
ctx.ServerError("UpdateComment", err) ctx.ServerError("UpdateComment", err)
}
return return
} }

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -4,6 +4,7 @@
package repo package repo
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
return return
} }
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "BlockedUser", err)
} else {
ctx.InternalServerError(err) ctx.InternalServerError(err)
}
return return
} }

View 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)
}

View 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)
}

View File

@ -5,6 +5,7 @@
package user package user
import ( import (
"errors"
"net/http" "net/http"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
// responses: // responses:
// "204": // "204":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "FollowUser", err)
} else {
ctx.Error(http.StatusInternalServerError, "FollowUser", err) ctx.Error(http.StatusInternalServerError, "FollowUser", err)
}
return return
} }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)

View File

@ -5,10 +5,9 @@
package user package user
import ( import (
std_context "context" "errors"
"net/http" "net/http"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -20,8 +19,12 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has // getStarredRepos returns the repos that the user with the specified userID has
// starred // starred
func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID,
IncludePrivate: private,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID private := ctx.ContextUser.ID == ctx.Doer.ID
repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
return return
@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
// "200": // "200":
// "$ref": "#/responses/RepositoryList" // "$ref": "#/responses/RepositoryList"
repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) repos, err := getStarredRepos(ctx, ctx.Doer, true)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
} }
@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
// responses: // responses:
// "204": // "204":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "BlockedUser", err)
} else {
ctx.Error(http.StatusInternalServerError, "StarRepo", err) ctx.Error(http.StatusInternalServerError, "StarRepo", err)
}
return return
} }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err) ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return return

View File

@ -4,10 +4,9 @@
package user package user
import ( import (
std_context "context" "errors"
"net/http" "net/http"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -18,8 +17,12 @@ import (
) )
// getWatchedRepos returns the repos that the user with the specified userID is watching // getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) { func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions) watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID,
IncludePrivate: private,
})
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID private := ctx.ContextUser.ID == ctx.Doer.ID
repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
} }
@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
// "200": // "200":
// "$ref": "#/responses/RepositoryList" // "$ref": "#/responses/RepositoryList"
repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
} }
@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/WatchInfo" // "$ref": "#/responses/WatchInfo"
// "403":
// "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "BlockedUser", err)
} else {
ctx.Error(http.StatusInternalServerError, "WatchRepo", err) ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
}
return return
} }
ctx.JSON(http.StatusOK, api.WatchInfo{ ctx.JSON(http.StatusOK, api.WatchInfo{
@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err) ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
return return

38
routers/web/org/block.go Normal file
View 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")
}

View File

@ -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{

View File

@ -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
} }

View File

@ -57,6 +57,7 @@ import (
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
user_service "code.gitea.io/gitea/services/user"
) )
const ( const (
@ -1258,9 +1259,11 @@ func NewIssuePost(ctx *context.Context) {
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil { if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
return } else if errors.Is(err, user_model.ErrBlockedUser) {
} ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
} else {
ctx.ServerError("NewIssue", err) ctx.ServerError("NewIssue", err)
}
return return
} }
@ -2047,6 +2050,10 @@ func ViewIssue(ctx *context.Context) {
} }
ctx.Data["Tags"] = tags ctx.Data["Tags"] = tags
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
ctx.HTML(http.StatusOK, tplIssueView) ctx.HTML(http.StatusOK, tplIssueView)
} }
@ -2250,7 +2257,11 @@ func UpdateIssueContent(ctx *context.Context) {
} }
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil { if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
} else {
ctx.ServerError("ChangeContent", err) ctx.ServerError("ChangeContent", err)
}
return return
} }
@ -3108,7 +3119,11 @@ func NewComment(ctx *context.Context) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments) comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else {
ctx.ServerError("CreateIssueComment", err) ctx.ServerError("CreateIssueComment", err)
}
return return
} }
@ -3152,7 +3167,11 @@ func UpdateCommentContent(ctx *context.Context) {
return return
} }
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else {
ctx.ServerError("UpdateComment", err) ctx.ServerError("UpdateComment", err)
}
return return
} }
@ -3260,9 +3279,9 @@ func ChangeIssueReaction(ctx *context.Context) {
switch ctx.Params(":action") { switch ctx.Params(":action") {
case "react": case "react":
reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content) reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
if err != nil { if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err) ctx.ServerError("ChangeIssueReaction", err)
return return
} }
@ -3367,9 +3386,9 @@ func ChangeCommentReaction(ctx *context.Context) {
switch ctx.Params(":action") { switch ctx.Params(":action") {
case "react": case "react":
reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content) reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
if err != nil { if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err) ctx.ServerError("ChangeIssueReaction", err)
return return
} }

View File

@ -47,6 +47,7 @@ import (
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
user_service "code.gitea.io/gitea/services/user"
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
case db.IsErrNamePatternNotAllowed(err): case db.IsErrNamePatternNotAllowed(err):
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
case errors.Is(err, user_model.ErrBlockedUser):
ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
default: default:
ctx.ServerError("ForkPost", err) ctx.ServerError("ForkPost", err)
} }
@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} }
upload.AddUploadContext(ctx, "comment") upload.AddUploadContext(ctx, "comment")
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
ctx.HTML(http.StatusOK, tplPullFiles) ctx.HTML(http.StatusOK, tplPullFiles)
} }
@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil { if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
return
} else if git.IsErrPushRejected(err) { } else if git.IsErrPushRejected(err) {
pushrejErr := err.(*git.ErrPushRejected) pushrejErr := err.(*git.ErrPushRejected)
message := pushrejErr.Message message := pushrejErr.Message
@ -1501,9 +1507,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return return
} }
ctx.JSONError(flashError) ctx.JSONError(flashError)
} else if errors.Is(err, user_model.ErrBlockedUser) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.new.blocked_user"),
})
if err != nil {
ctx.ServerError("CompareAndPullRequest.HTMLString", err)
return return
} }
ctx.ServerError("NewPullRequest", err) ctx.JSONError(flashError)
}
return return
} }

View File

@ -313,13 +313,13 @@ func Action(ctx *context.Context) {
var err error var err error
switch ctx.Params(":action") { switch ctx.Params(":action") {
case "watch": case "watch":
err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unwatch": case "unwatch":
err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "star": case "star":
err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unstar": case "unstar":
err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "accept_transfer": case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true) err = acceptOrRejectRepoTransfer(ctx, true)
case "reject_transfer": case "reject_transfer":
@ -336,9 +336,13 @@ func Action(ctx *context.Context) {
} }
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
} else {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
return return
} }
}
switch ctx.Params(":action") { switch ctx.Params(":action") {
case "watch", "unwatch": case "watch", "unwatch":

View File

@ -4,10 +4,10 @@
package setting package setting
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
ctx.Data["PageIsSettingsCollaboration"] = true ctx.Data["PageIsSettingsCollaboration"] = true
users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil { if err != nil {
ctx.ServerError("GetCollaborators", err) ctx.ServerError("GetCollaborators", err)
return return
@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
} }
if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
} else {
ctx.ServerError("AddCollaborator", err) ctx.ServerError("AddCollaborator", err)
}
return return
} }
@ -126,11 +131,20 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
// DeleteCollaboration delete a collaboration for a repository // DeleteCollaboration delete a collaboration for a repository
func DeleteCollaboration(ctx *context.Context) { func DeleteCollaboration(ctx *context.Context) {
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
} else {
ctx.ServerError("GetUserByName", err)
return
}
} else {
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Flash.Error("DeleteCollaboration: " + err.Error()) ctx.Flash.Error("DeleteCollaboration: " + err.Error())
} else { } else {
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
} }
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration") ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
} }

View File

@ -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)
} }

View 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
}
}
}
}

View File

@ -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()) {

View File

@ -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)
} }

View 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")
}

View File

@ -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)

View File

@ -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
} }

View File

@ -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)
}

View File

@ -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)

View File

@ -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
} }

View File

@ -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 {

View File

@ -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)
} }

View 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,
})
}

View File

@ -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})
} }

View File

@ -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) {

View File

@ -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
} }

View File

@ -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})
} }

View File

@ -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
} }
} }

View File

@ -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{

View File

@ -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
View 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
})
}

View 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))
}

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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
} }

View 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" .}}

View File

@ -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"}}

View File

@ -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>

View File

@ -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>

View File

@ -10,7 +10,10 @@
{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}} {{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
{{end}} {{end}}
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div> <div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
{{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}} {{if .ctxData.IsSigned}}
{{$needDivider := false}}
{{if not .ctxData.Repository.IsArchived}}
{{$needDivider = true}}
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div> <div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
{{if not .ctxData.UnitIssuesGlobalDisabled}} {{if not .ctxData.UnitIssuesGlobalDisabled}}
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div> <div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
@ -23,5 +26,19 @@
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
{{if or $canOrgBlock $canUserBlock}}
{{if $needDivider}}
<div class="divider"></div>
{{end}}
{{if $canUserBlock}}
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
{{end}}
{{if $canOrgBlock}}
<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
{{end}}
{{end}}
{{end}}
</div> </div>
</div> </div>

View 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>

View 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>

View File

@ -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,6 +115,7 @@
</li> </li>
{{end}} {{end}}
{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}} {{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
{{if not .UserBlocking}}
<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card"> <li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
{{if $.IsFollowing}} {{if $.IsFollowing}}
<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button"> <button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
@ -121,6 +128,16 @@
{{end}} {{end}}
</li> </li>
{{end}} {{end}}
<li>
{{if not .UserBlocking}}
<a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a>
{{else}}
<a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a>
{{end}}
</li>
{{end}}
</ul> </ul>
</div> </div>
</div> </div>
{{template "shared/user/block_user_dialog" .}}

View File

@ -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"
} }

View 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" .}}

View File

@ -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