mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-26 13:29:12 -05:00
Merge remote-tracking branch 'forgejo/v1.20/forgejo-moderation' into v1.20/forgejo
This commit is contained in:
commit
1371196064
56 changed files with 1642 additions and 64 deletions
|
@ -580,7 +580,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
|||
|
||||
if repoChanged {
|
||||
// Add feeds for user self and all watchers.
|
||||
watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
|
||||
watchers, err = repo_model.GetWatchersExcludeBlocked(ctx, act.RepoID, act.ActUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get watchers: %w", err)
|
||||
}
|
||||
|
|
|
@ -235,6 +235,15 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
|||
for _, id := range issueUnWatches {
|
||||
toNotify.Remove(id)
|
||||
}
|
||||
|
||||
// Remove users who have the notification author blocked.
|
||||
blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range blockedAuthorIDs {
|
||||
toNotify.Remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
err = issue.LoadRepo(ctx)
|
||||
|
|
5
models/fixtures/forgejo_blocked_user.yml
Normal file
5
models/fixtures/forgejo_blocked_user.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
-
|
||||
id: 1
|
||||
user_id: 4
|
||||
block_id: 1
|
||||
created_unix: 1671607299
|
|
@ -37,7 +37,7 @@
|
|||
lower_name: repo2
|
||||
name: repo2
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_watches: 1
|
||||
num_stars: 1
|
||||
num_forks: 0
|
||||
num_issues: 2
|
||||
|
|
|
@ -27,3 +27,9 @@
|
|||
user_id: 11
|
||||
repo_id: 1
|
||||
mode: 3 # auto
|
||||
|
||||
-
|
||||
id: 6
|
||||
user_id: 4
|
||||
repo_id: 2
|
||||
mode: 1 # normal
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -34,7 +35,9 @@ func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration {
|
|||
|
||||
// This is a sequence of additional Forgejo migrations.
|
||||
// Add new migrations to the bottom of the list.
|
||||
var migrations = []*Migration{}
|
||||
var migrations = []*Migration{
|
||||
NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
func GetCurrentDBVersion(x *xorm.Engine) (int64, error) {
|
||||
|
|
21
models/forgejo_migrations/v1_20/v1.go
Normal file
21
models/forgejo_migrations/v1_20/v1.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_v1_20 //nolint:revive
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddForgejoBlockedUser(x *xorm.Engine) error {
|
||||
type ForgejoBlockedUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BlockID int64 `xorm:"index"`
|
||||
UserID int64 `xorm:"index"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
return x.Sync(new(ForgejoBlockedUser))
|
||||
}
|
|
@ -452,6 +452,8 @@ func TestIssue_ResolveMentions(t *testing.T) {
|
|||
testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
|
||||
// Public repo, doer
|
||||
testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
|
||||
// Public repo, blocked user
|
||||
testSuccess("user2", "repo1", "user1", []string{"user4"}, []int64{})
|
||||
// Private repo, team member
|
||||
testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
|
||||
// Private repo, not a team member
|
||||
|
|
|
@ -608,9 +608,11 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
|||
teamusers := make([]*user_model.User, 0, 20)
|
||||
if err := db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.uid = `user`.id").
|
||||
Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
|
||||
In("`team_user`.team_id", checked).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
|
||||
Find(&teamusers); err != nil {
|
||||
return nil, fmt.Errorf("get teams users: %w", err)
|
||||
}
|
||||
|
@ -644,8 +646,10 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
|||
|
||||
unchecked := make([]*user_model.User, 0, len(mentionUsers))
|
||||
if err := db.GetEngine(ctx).
|
||||
Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `user`.id").
|
||||
Where("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doer.ID})).
|
||||
In("`user`.lower_name", mentionUsers).
|
||||
Find(&unchecked); err != nil {
|
||||
return nil, fmt.Errorf("find mentioned users: %w", err)
|
||||
|
|
|
@ -218,12 +218,12 @@ type ReactionOptions struct {
|
|||
}
|
||||
|
||||
// CreateReaction creates reaction for issue or comment.
|
||||
func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
||||
func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
|
||||
if !setting.UI.ReactionsLookup.Contains(opts.Type) {
|
||||
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -240,25 +240,6 @@ func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
|||
return reaction, nil
|
||||
}
|
||||
|
||||
// CreateIssueReaction creates a reaction on issue.
|
||||
func CreateIssueReaction(doerID, issueID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on comment.
|
||||
func CreateCommentReaction(doerID, issueID, commentID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReaction deletes reaction for issue or comment.
|
||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
||||
reaction := &Reaction{
|
||||
|
|
|
@ -19,11 +19,14 @@ import (
|
|||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
|
||||
var reaction *issues_model.Reaction
|
||||
var err error
|
||||
if commentID == 0 {
|
||||
reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content)
|
||||
} else {
|
||||
reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content)
|
||||
}
|
||||
// NOTE: This doesn't do user blocking checking.
|
||||
reaction, err = issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
Type: content,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reaction)
|
||||
}
|
||||
|
@ -49,7 +52,7 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
|
|||
|
||||
addReaction(t, user1.ID, issue1ID, 0, "heart")
|
||||
|
||||
reaction, err := issues_model.CreateReaction(&issues_model.ReactionOptions{
|
||||
reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
|
||||
DoerID: user1.ID,
|
||||
IssueID: issue1ID,
|
||||
Type: "heart",
|
||||
|
|
|
@ -177,3 +177,16 @@ func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull boo
|
|||
Limit(30).
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
// GetWatchedRepoIDsOwnedBy returns the repos owned by a particular user watched by a particular user
|
||||
func GetWatchedRepoIDsOwnedBy(ctx context.Context, userID, ownedByUserID int64) ([]int64, error) {
|
||||
repoIDs := make([]int64, 0, 10)
|
||||
err := db.GetEngine(ctx).
|
||||
Table("repository").
|
||||
Select("`repository`.id").
|
||||
Join("LEFT", "watch", "`repository`.id=`watch`.repo_id").
|
||||
Where("`watch`.user_id=?", userID).
|
||||
And("`watch`.mode<>?", WatchModeDont).
|
||||
And("`repository`.owner_id=?", ownedByUserID).Find(&repoIDs)
|
||||
return repoIDs, err
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -71,3 +72,15 @@ func TestRepoGetReviewers(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
}
|
||||
|
||||
func GetWatchedRepoIDsOwnedBy(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(db.DefaultContext, user1.ID, user2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repoIDs, 1)
|
||||
assert.EqualValues(t, 1, repoIDs[0])
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// WatchMode specifies what kind of watch the user has on a repository
|
||||
|
@ -142,6 +144,21 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
|||
Find(&watches)
|
||||
}
|
||||
|
||||
// GetWatchersExcludeBlocked returns all watchers of given repository, whereby
|
||||
// the doer isn't blocked by one of the watchers.
|
||||
func GetWatchersExcludeBlocked(ctx context.Context, repoID, doerID int64) ([]*Watch, error) {
|
||||
watches := make([]*Watch, 0, 10)
|
||||
return watches, db.GetEngine(ctx).
|
||||
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
|
||||
Join("LEFT", "forgejo_blocked_user", "forgejo_blocked_user.user_id = `watch`.user_id").
|
||||
Where("`watch`.repo_id=?", repoID).
|
||||
And("`watch`.mode<>?", WatchModeDont).
|
||||
And("`user`.is_active=?", true).
|
||||
And("`user`.prohibit_login=?", false).
|
||||
And(builder.Or(builder.IsNull{"`forgejo_blocked_user`.block_id"}, builder.Neq{"`forgejo_blocked_user`.block_id": doerID})).
|
||||
Find(&watches)
|
||||
}
|
||||
|
||||
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
|
@ -184,3 +201,9 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
|
|||
}
|
||||
return watchRepoMode(ctx, watch, WatchModeAuto)
|
||||
}
|
||||
|
||||
// UnwatchRepos will unwatch the user from all given repositories.
|
||||
func UnwatchRepos(ctx context.Context, userID int64, repoIDs []int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("user_id=?", userID).In("repo_id", repoIDs).Delete(&Watch{})
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -43,6 +43,24 @@ func TestGetWatchers(t *testing.T) {
|
|||
assert.Len(t, watches, 0)
|
||||
}
|
||||
|
||||
func TestGetWatchersExcludeBlocked(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
watches, err := repo_model.GetWatchersExcludeBlocked(db.DefaultContext, repo.ID, 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// One watchers are inactive and one watcher is blocked, thus minus 2
|
||||
assert.Len(t, watches, repo.NumWatches-2)
|
||||
for _, watch := range watches {
|
||||
assert.EqualValues(t, repo.ID, watch.RepoID)
|
||||
}
|
||||
|
||||
watches, err = repo_model.GetWatchersExcludeBlocked(db.DefaultContext, unittest.NonexistentID, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watches, 0)
|
||||
}
|
||||
|
||||
func TestRepository_GetWatchers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
@ -137,3 +155,16 @@ func TestWatchRepoMode(t *testing.T) {
|
|||
assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone))
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
|
||||
}
|
||||
|
||||
func TestUnwatchRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
|
||||
|
||||
err := repo_model.UnwatchRepos(db.DefaultContext, 4, []int64{1, 2})
|
||||
assert.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
|
||||
}
|
||||
|
|
91
models/user/block.go
Normal file
91
models/user/block.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// ErrBlockedByUser defines an error stating that the user is not allowed to perform the action because they are blocked.
|
||||
var ErrBlockedByUser = errors.New("user is blocked by the poster or repository owner")
|
||||
|
||||
// BlockedUser represents a blocked user entry.
|
||||
type BlockedUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
// UID of the one who got blocked.
|
||||
BlockID int64 `xorm:"index"`
|
||||
// UID of the one who did the block action.
|
||||
UserID int64 `xorm:"index"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
// TableName provides the real table name
|
||||
func (*BlockedUser) TableName() string {
|
||||
return "forgejo_blocked_user"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(BlockedUser))
|
||||
}
|
||||
|
||||
// IsBlocked returns if userID has blocked blockID.
|
||||
func IsBlocked(ctx context.Context, userID, blockID int64) bool {
|
||||
has, _ := db.GetEngine(ctx).Exist(&BlockedUser{UserID: userID, BlockID: blockID})
|
||||
return has
|
||||
}
|
||||
|
||||
// IsBlockedMultiple returns if one of the userIDs has blocked blockID.
|
||||
func IsBlockedMultiple(ctx context.Context, userIDs []int64, blockID int64) bool {
|
||||
has, _ := db.GetEngine(ctx).In("user_id", userIDs).Exist(&BlockedUser{BlockID: blockID})
|
||||
return has
|
||||
}
|
||||
|
||||
// UnblockUser removes the blocked user entry.
|
||||
func UnblockUser(ctx context.Context, userID, blockID int64) error {
|
||||
_, err := db.GetEngine(ctx).Delete(&BlockedUser{UserID: userID, BlockID: blockID})
|
||||
return err
|
||||
}
|
||||
|
||||
// CountBlockedUsers returns the number of users the user has blocked.
|
||||
func CountBlockedUsers(ctx context.Context, userID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("user_id=?", userID).Count(&BlockedUser{})
|
||||
}
|
||||
|
||||
// ListBlockedUsers returns the users that the user has blocked.
|
||||
// The created_unix field of the user struct is overridden by the creation_unix
|
||||
// field of blockeduser.
|
||||
func ListBlockedUsers(ctx context.Context, userID int64, opts db.ListOptions) ([]*User, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Select("`forgejo_blocked_user`.created_unix, `user`.*").
|
||||
Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id").
|
||||
Where("`forgejo_blocked_user`.user_id=?", userID)
|
||||
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
users := make([]*User, 0, opts.PageSize)
|
||||
|
||||
return users, sess.Find(&users)
|
||||
}
|
||||
|
||||
users := make([]*User, 0, 8)
|
||||
return users, sess.Find(&users)
|
||||
}
|
||||
|
||||
// ListBlockedByUsersID returns the ids of the users that blocked the user.
|
||||
func ListBlockedByUsersID(ctx context.Context, userID int64) ([]int64, error) {
|
||||
users := make([]int64, 0, 8)
|
||||
err := db.GetEngine(ctx).
|
||||
Table("user").
|
||||
Select("`user`.id").
|
||||
Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.user_id").
|
||||
Where("`forgejo_blocked_user`.block_id=?", userID).
|
||||
Find(&users)
|
||||
|
||||
return users, err
|
||||
}
|
75
models/user/block_test.go
Normal file
75
models/user/block_test.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsBlocked(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
|
||||
|
||||
// Simple test cases to ensure the function can also respond with false.
|
||||
assert.False(t, user_model.IsBlocked(db.DefaultContext, 1, 1))
|
||||
assert.False(t, user_model.IsBlocked(db.DefaultContext, 3, 2))
|
||||
}
|
||||
|
||||
func TestIsBlockedMultiple(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4}, 1))
|
||||
assert.True(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{4, 3, 4, 5}, 1))
|
||||
|
||||
// Simple test cases to ensure the function can also respond with false.
|
||||
assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{1}, 1))
|
||||
assert.False(t, user_model.IsBlockedMultiple(db.DefaultContext, []int64{3, 4, 1}, 2))
|
||||
}
|
||||
|
||||
func TestUnblockUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.True(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
|
||||
|
||||
assert.NoError(t, user_model.UnblockUser(db.DefaultContext, 4, 1))
|
||||
|
||||
// Simple test cases to ensure the function can also respond with false.
|
||||
assert.False(t, user_model.IsBlocked(db.DefaultContext, 4, 1))
|
||||
}
|
||||
|
||||
func TestListBlockedUsers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
blockedUsers, err := user_model.ListBlockedUsers(db.DefaultContext, 4, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, blockedUsers, 1) {
|
||||
assert.EqualValues(t, 1, blockedUsers[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBlockedByUsersID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
blockedByUserIDs, err := user_model.ListBlockedByUsersID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, blockedByUserIDs, 1) {
|
||||
assert.EqualValues(t, 4, blockedByUserIDs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountBlockedUsers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
count, err := user_model.CountBlockedUsers(db.DefaultContext, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, count)
|
||||
|
||||
count, err = user_model.CountBlockedUsers(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
}
|
|
@ -4,6 +4,8 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
@ -22,17 +24,26 @@ func init() {
|
|||
|
||||
// IsFollowing returns true if user is following followID.
|
||||
func IsFollowing(userID, followID int64) bool {
|
||||
has, _ := db.GetEngine(db.DefaultContext).Get(&Follow{UserID: userID, FollowID: followID})
|
||||
return IsFollowingCtx(db.DefaultContext, userID, followID)
|
||||
}
|
||||
|
||||
// IsFollowingCtx returns true if user is following followID.
|
||||
func IsFollowingCtx(ctx context.Context, userID, followID int64) bool {
|
||||
has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID})
|
||||
return has
|
||||
}
|
||||
|
||||
// FollowUser marks someone be another's follower.
|
||||
func FollowUser(userID, followID int64) (err error) {
|
||||
if userID == followID || IsFollowing(userID, followID) {
|
||||
func FollowUser(ctx context.Context, userID, followID int64) (err error) {
|
||||
if userID == followID || IsFollowingCtx(ctx, userID, followID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
if IsBlocked(ctx, userID, followID) || IsBlocked(ctx, followID, userID) {
|
||||
return ErrBlockedByUser
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -53,12 +64,12 @@ func FollowUser(userID, followID int64) (err error) {
|
|||
}
|
||||
|
||||
// UnfollowUser unmarks someone as another's follower.
|
||||
func UnfollowUser(userID, followID int64) (err error) {
|
||||
if userID == followID || !IsFollowing(userID, followID) {
|
||||
func UnfollowUser(ctx context.Context, userID, followID int64) (err error) {
|
||||
if userID == followID || !IsFollowingCtx(ctx, userID, followID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -449,13 +449,19 @@ func TestFollowUser(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(followerID, followedID int64) {
|
||||
assert.NoError(t, user_model.FollowUser(followerID, followedID))
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
|
||||
}
|
||||
testSuccess(4, 2)
|
||||
testSuccess(5, 2)
|
||||
|
||||
assert.NoError(t, user_model.FollowUser(2, 2))
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
|
||||
|
||||
// Blocked user.
|
||||
assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 1, 4))
|
||||
assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 4, 1))
|
||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 1, FollowID: 4})
|
||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 4, FollowID: 1})
|
||||
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
}
|
||||
|
@ -464,7 +470,7 @@ func TestUnfollowUser(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(followerID, followedID int64) {
|
||||
assert.NoError(t, user_model.UnfollowUser(followerID, followedID))
|
||||
assert.NoError(t, user_model.UnfollowUser(db.DefaultContext, followerID, followedID))
|
||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
|
||||
}
|
||||
testSuccess(4, 2)
|
||||
|
|
13
modules/structs/moderation.go
Normal file
13
modules/structs/moderation.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// BlockedUser represents a blocked user.
|
||||
type BlockedUser struct {
|
||||
BlockID int64 `json:"block_id"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
}
|
|
@ -591,11 +591,18 @@ overview = Overview
|
|||
following = Following
|
||||
follow = Follow
|
||||
unfollow = Unfollow
|
||||
block = Block
|
||||
unblock = Unblock
|
||||
heatmap.loading = Loading Heatmap…
|
||||
user_bio = Biography
|
||||
disabled_public_activity = This user has disabled the public visibility of the activity.
|
||||
email_visibility.limited = Your email address is visible to all authenticated users
|
||||
email_visibility.private = Your email address is only visible to you and administrators
|
||||
block_user = Block User
|
||||
block_user.detail = Please understand that if you block this user, other actions will be taken. Such as:
|
||||
block_user.detail_1 = You are being unfollowed from this user.
|
||||
block_user.detail_2 = This user cannot interact with your repositories, created issues and comments.
|
||||
follow_blocked_user = You cannot follow this user because you have blocked this user or this user has blocked you.
|
||||
|
||||
form.name_reserved = The username "%s" is reserved.
|
||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
|
||||
|
@ -619,6 +626,7 @@ account_link = Linked Accounts
|
|||
organization = Organizations
|
||||
uid = Uid
|
||||
webauthn = Security Keys
|
||||
blocked_users = Blocked Users
|
||||
|
||||
public_profile = Public Profile
|
||||
biography_placeholder = Tell us a little bit about yourself
|
||||
|
@ -886,6 +894,7 @@ hooks.desc = Add webhooks which will be triggered for <strong>all repositories</
|
|||
|
||||
orgs_none = You are not a member of any organizations.
|
||||
repos_none = You do not own any repositories
|
||||
blocked_users_none = You haven't blocked any users.
|
||||
|
||||
delete_account = Delete Your Account
|
||||
delete_prompt = This operation will permanently delete your user account. It <strong>CANNOT</strong> be undone.
|
||||
|
@ -908,6 +917,10 @@ visibility.limited_tooltip = Visible to authenticated users only
|
|||
visibility.private = Private
|
||||
visibility.private_tooltip = Visible only to organization members
|
||||
|
||||
blocked_since = Blocked since %s
|
||||
user_unblock_success = The user has been unblocked successfully.
|
||||
user_block_success = The user has been blocked successfully.
|
||||
|
||||
[repo]
|
||||
new_repo_helper = A repository contains all project files, including revision history. Already have it elsewhere? <a href="%s">Migrate repository.</a>
|
||||
owner = Owner
|
||||
|
@ -1630,6 +1643,7 @@ issues.content_history.delete_from_history = Delete from history
|
|||
issues.content_history.delete_from_history_confirm = Delete from history?
|
||||
issues.content_history.options = Options
|
||||
issues.reference_link = Reference: %s
|
||||
issues.blocked_by_user = You cannot create a issue on this repository because you are blocked by the repository owner.
|
||||
|
||||
compare.compare_base = base
|
||||
compare.compare_head = compare
|
||||
|
@ -1702,6 +1716,7 @@ pulls.reject_count_n = "%d change requests"
|
|||
pulls.waiting_count_1 = "%d waiting review"
|
||||
pulls.waiting_count_n = "%d waiting reviews"
|
||||
pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
|
||||
pulls.blocked_by_user = You cannot create a pull request on this repository because you are blocked by the repository owner.
|
||||
|
||||
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
|
||||
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
|
||||
|
@ -2516,6 +2531,7 @@ team_access_desc = Repository access
|
|||
team_permission_desc = Permission
|
||||
team_unit_desc = Allow Access to Repository Sections
|
||||
team_unit_disabled = (Disabled)
|
||||
follow_blocked_user = You cannot follow this organisation because this organisation has blocked you.
|
||||
|
||||
form.name_reserved = The organization name "%s" is reserved.
|
||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name.
|
||||
|
|
|
@ -905,6 +905,12 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
Patch(bind(api.EditHookOption{}), user.EditHook).
|
||||
Delete(user.DeleteHook)
|
||||
}, reqWebhooksEnabled())
|
||||
|
||||
m.Group("", func() {
|
||||
m.Get("/list_blocked", user.ListBlockedUsers)
|
||||
m.Put("/block/{username}", user.BlockUser)
|
||||
m.Put("/unblock/{username}", user.UnblockUser)
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
|
@ -1321,6 +1327,12 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
Delete(org.DeleteHook)
|
||||
}, reqToken(), reqOrgOwnership(), reqWebhooksEnabled())
|
||||
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
|
||||
|
||||
m.Group("", func() {
|
||||
m.Get("/list_blocked", reqToken(), reqOrgOwnership(), org.ListBlockedUsers)
|
||||
m.Put("/block/{username}", reqToken(), reqOrgOwnership(), org.BlockUser)
|
||||
m.Put("/unblock/{username}", reqToken(), reqOrgOwnership(), org.UnblockUser)
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||
|
|
|
@ -437,3 +437,95 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
|||
|
||||
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
|
||||
}
|
||||
|
||||
// ListBlockedUsers list the organization's blocked users.
|
||||
func ListBlockedUsers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/list_blocked organization orgListBlockedUsers
|
||||
// ---
|
||||
// summary: List the organization's blocked users
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the org
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/BlockedUserList"
|
||||
|
||||
utils.ListUserBlockedUsers(ctx, ctx.ContextUser)
|
||||
}
|
||||
|
||||
// BlockUser blocks a user from the organization.
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/block/{username} organization orgBlockUser
|
||||
// ---
|
||||
// summary: Blocks a user from the organization
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the org
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
user := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
utils.BlockUser(ctx, ctx.Org.Organization.AsUser(), user)
|
||||
}
|
||||
|
||||
// UnblockUser unblocks a user from the organization.
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/unblock/{username} organization orgUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user from the organization
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the org
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
user := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
utils.UnblockUser(ctx, ctx.Org.Organization.AsUser(), user)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -652,7 +653,10 @@ func CreateIssue(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedByUser", err)
|
||||
return
|
||||
} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -364,7 +364,11 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a comment from an issue
|
||||
|
@ -196,9 +198,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
|||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment.Issue, comment, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
|
@ -406,9 +408,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
|||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
|
|
|
@ -418,7 +418,10 @@ func CreatePullRequest(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedByUser", err)
|
||||
return
|
||||
} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -414,3 +414,10 @@ type swaggerRepoNewIssuePinsAllowed struct {
|
|||
// in:body
|
||||
Body api.NewIssuePinsAllowed `json:"body"`
|
||||
}
|
||||
|
||||
// BlockedUserList
|
||||
// swagger:response BlockedUserList
|
||||
type swaggerBlockedUserList struct {
|
||||
// in:body
|
||||
Body []api.BlockedUser `json:"body"`
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -217,8 +218,14 @@ func Follow(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
if err := user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedByUser", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "FollowUser", err)
|
||||
return
|
||||
}
|
||||
|
@ -240,7 +247,7 @@ func Unfollow(ctx *context.APIContext) {
|
|||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
if err := user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UnfollowUser", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -202,3 +202,80 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
|||
|
||||
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
|
||||
}
|
||||
|
||||
// ListBlockedUsers list the authenticated user's blocked users.
|
||||
func ListBlockedUsers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/list_blocked user userListBlockedUsers
|
||||
// ---
|
||||
// summary: List the authenticated user's blocked users
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/BlockedUserList"
|
||||
|
||||
utils.ListUserBlockedUsers(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// BlockUser blocks a user from the doer.
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /user/block/{username} user userBlockUser
|
||||
// ---
|
||||
// summary: Blocks a user from the doer.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
utils.BlockUser(ctx, ctx.Doer, user)
|
||||
}
|
||||
|
||||
// UnblockUser unblocks a user from the doer.
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /user/unblock/{username} user userUnblockUser
|
||||
// ---
|
||||
// summary: Unblocks a user from the doer.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
utils.UnblockUser(ctx, ctx.Doer, user)
|
||||
}
|
||||
|
|
65
routers/api/v1/utils/block.go
Normal file
65
routers/api/v1/utils/block.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
// ListUserBlockedUsers lists the blocked users of the provided doer.
|
||||
func ListUserBlockedUsers(ctx *context.APIContext, doer *user_model.User) {
|
||||
count, err := user_model.CountBlockedUsers(ctx, doer.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
blockedUsers, err := user_model.ListBlockedUsers(ctx, doer.ID, GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiBlockedUsers := make([]*api.BlockedUser, len(blockedUsers))
|
||||
for i, blockedUser := range blockedUsers {
|
||||
apiBlockedUsers[i] = &api.BlockedUser{
|
||||
BlockID: blockedUser.ID,
|
||||
Created: blockedUser.CreatedUnix.AsTime(),
|
||||
}
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiBlockedUsers)
|
||||
}
|
||||
|
||||
// BlockUser blocks the blockUser from the doer.
|
||||
func BlockUser(ctx *context.APIContext, doer, blockUser *user_model.User) {
|
||||
err := user_service.BlockUser(ctx, doer.ID, blockUser.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UnblockUser unblocks the blockUser from the doer.
|
||||
func UnblockUser(ctx *context.APIContext, doer, blockUser *user_model.User) {
|
||||
err := user_model.UnblockUser(ctx, doer.ID, blockUser.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
62
routers/web/org/setting/blocked_users.go
Normal file
62
routers/web/org/setting/blocked_users.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
const tplBlockedUsers = "org/settings/blocked_users"
|
||||
|
||||
// BlockedUsers renders the blocked users page.
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
|
||||
ctx.Data["PageIsSettingsBlockedUsers"] = true
|
||||
|
||||
blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID, db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("ListBlockedUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["BlockedUsers"] = blockedUsers
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBlockedUsers)
|
||||
}
|
||||
|
||||
// BlockedUsersBlock blocks a particular user from the organization.
|
||||
func BlockedUsersBlock(ctx *context.Context) {
|
||||
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
|
||||
u, err := user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.BlockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
|
||||
ctx.ServerError("BlockUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.user_block_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
|
||||
}
|
||||
|
||||
// BlockedUsersUnblock unblocks a particular user from the organization.
|
||||
func BlockedUsersUnblock(ctx *context.Context) {
|
||||
if err := user_model.UnblockUser(ctx, ctx.Org.Organization.ID, ctx.FormInt64("user_id")); err != nil {
|
||||
ctx.ServerError("BlockUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
|
||||
}
|
|
@ -1162,7 +1162,10 @@ func NewIssuePost(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.issues.blocked_by_user"), tplIssueNew, form)
|
||||
return
|
||||
} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -3061,7 +3064,7 @@ func ChangeIssueReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Content)
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
|
@ -3163,7 +3166,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment.Issue, comment, form.Content)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
|
|
|
@ -1271,7 +1271,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
// instead of 500.
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.blocked_by_user"))
|
||||
ctx.Redirect(ctx.Link)
|
||||
return
|
||||
} else if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
return
|
||||
} else if git.IsErrPushRejected(err) {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -23,6 +24,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
// Profile render user's profile page
|
||||
|
@ -58,8 +60,10 @@ func Profile(ctx *context.Context) {
|
|||
}
|
||||
|
||||
var isFollowing bool
|
||||
var isBlocked bool
|
||||
if ctx.Doer != nil {
|
||||
isFollowing = user_model.IsFollowing(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
isBlocked = user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.ContextUser.DisplayName()
|
||||
|
@ -67,6 +71,7 @@ func Profile(ctx *context.Context) {
|
|||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
ctx.Data["OpenIDs"] = openIDs
|
||||
ctx.Data["IsFollowing"] = isFollowing
|
||||
ctx.Data["IsBlocked"] = isBlocked
|
||||
|
||||
if setting.Service.EnableUserHeatmap {
|
||||
data, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
|
||||
|
@ -351,17 +356,39 @@ func Profile(ctx *context.Context) {
|
|||
// Action response for follow/unfollow user request
|
||||
func Action(ctx *context.Context) {
|
||||
var err error
|
||||
var redirectViaJSON bool
|
||||
switch ctx.FormString("action") {
|
||||
case "follow":
|
||||
err = user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
case "unfollow":
|
||||
err = user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
case "block":
|
||||
err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
redirectViaJSON = true
|
||||
case "unblock":
|
||||
err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
|
||||
if !errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"))
|
||||
}
|
||||
}
|
||||
|
||||
if redirectViaJSON {
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": ctx.ContextUser.HomeLink(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: We should check this URL and make sure that it's a valid Gitea URL
|
||||
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.ContextUser.HomeLink())
|
||||
}
|
||||
|
|
46
routers/web/user/setting/blocked_users.go
Normal file
46
routers/web/user/setting/blocked_users.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
|
||||
)
|
||||
|
||||
// BlockedUsers render the blocked users list page.
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
|
||||
ctx.Data["PageIsBlockedUsers"] = true
|
||||
ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/blocked_users"
|
||||
ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/blocked_users"
|
||||
|
||||
blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Doer.ID, db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("ListBlockedUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["BlockedUsers"] = blockedUsers
|
||||
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
|
||||
}
|
||||
|
||||
// UnblockUser unblocks a particular user for the doer.
|
||||
func UnblockUser(ctx *context.Context) {
|
||||
if err := user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.FormInt64("user_id")); err != nil {
|
||||
ctx.ServerError("UnblockUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
|
||||
}
|
|
@ -518,6 +518,11 @@ func registerRoutes(m *web.Route) {
|
|||
})
|
||||
addWebhookEditRoutes()
|
||||
}, webhooksEnabled)
|
||||
|
||||
m.Group("/blocked_users", func() {
|
||||
m.Get("", user_setting.BlockedUsers)
|
||||
m.Post("/unblock", user_setting.UnblockUser)
|
||||
})
|
||||
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
|
||||
|
||||
m.Group("/user", func() {
|
||||
|
@ -770,6 +775,12 @@ func registerRoutes(m *web.Route) {
|
|||
addSettingsSecretsRoutes()
|
||||
}, actions.MustEnableActions)
|
||||
|
||||
m.Group("/blocked_users", func() {
|
||||
m.Get("", org_setting.BlockedUsers)
|
||||
m.Post("/block", org_setting.BlockedUsersBlock)
|
||||
m.Post("/unblock", org_setting.BlockedUsersUnblock)
|
||||
})
|
||||
|
||||
m.RouteMethods("/delete", "GET,POST", org.SettingsDelete)
|
||||
|
||||
m.Group("/packages", func() {
|
||||
|
|
|
@ -66,6 +66,11 @@ func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue
|
|||
|
||||
// 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) {
|
||||
// Check if doer is blocked by the poster of the issue.
|
||||
if user_model.IsBlocked(ctx, issue.PosterID, doer.ID) {
|
||||
return nil, user_model.ErrBlockedByUser
|
||||
}
|
||||
|
||||
comment, err := CreateComment(ctx, &issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: doer,
|
||||
|
|
|
@ -22,6 +22,11 @@ import (
|
|||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
|
||||
// Check if the user is not blocked by the repo's owner.
|
||||
if user_model.IsBlocked(ctx, repo.OwnerID, issue.PosterID) {
|
||||
return user_model.ErrBlockedByUser
|
||||
}
|
||||
|
||||
if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
47
services/issue/reaction.go
Normal file
47
services/issue/reaction.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// CreateIssueReaction creates a reaction on issue.
|
||||
func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the doer is blocked by the issue's poster or repository owner.
|
||||
if user_model.IsBlockedMultiple(ctx, []int64{issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
|
||||
return nil, user_model.ErrBlockedByUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on comment.
|
||||
func CreateCommentReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the doer is blocked by the issue's poster, the comment's poster or repository owner.
|
||||
if user_model.IsBlockedMultiple(ctx, []int64{comment.PosterID, issue.PosterID, issue.Repo.OwnerID}, doer.ID) {
|
||||
return nil, user_model.ErrBlockedByUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
}
|
|
@ -36,6 +36,11 @@ var pullWorkingPool = sync.NewExclusivePool()
|
|||
|
||||
// NewPullRequest creates new pull request with labels for repository.
|
||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
|
||||
// Check if the doer is not blocked by the repository's owner.
|
||||
if user_model.IsBlocked(ctx, repo.OwnerID, pull.PosterID) {
|
||||
return user_model.ErrBlockedByUser
|
||||
}
|
||||
|
||||
if err := TestPatch(pr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
58
services/user/block.go
Normal file
58
services/user/block.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// BlockUser adds a blocked user entry for userID to block blockID.
|
||||
// TODO: Figure out if instance admins should be immune to blocking.
|
||||
// TODO: Add more mechanism like removing blocked user as collaborator on
|
||||
// repositories where the user is an owner.
|
||||
func BlockUser(ctx context.Context, userID, blockID int64) error {
|
||||
if userID == blockID || user_model.IsBlocked(ctx, userID, blockID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Add the blocked user entry.
|
||||
_, err = db.GetEngine(ctx).Insert(&user_model.BlockedUser{UserID: userID, BlockID: blockID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unfollow the user from the block's perspective.
|
||||
err = user_model.UnfollowUser(ctx, blockID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unfollow the user from the doer's perspective.
|
||||
err = user_model.UnfollowUser(ctx, userID, blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Blocked user unwatch all repository owned by the doer.
|
||||
repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(ctx, blockID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = repo_model.UnwatchRepos(ctx, blockID, repoIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
41
services/user/block_test.go
Normal file
41
services/user/block_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestBlockUser will ensure that when you block a user, certain actions have
|
||||
// been taken, like unfollowing each other etc.
|
||||
func TestBlockUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
// Follow each other.
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, doer.ID, blockedUser.ID))
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, blockedUser.ID, doer.ID))
|
||||
|
||||
// Blocked user watch repository of doer.
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: doer.ID})
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, blockedUser.ID, repo.ID, true))
|
||||
|
||||
assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
|
||||
|
||||
// Ensure they aren't following each other anymore.
|
||||
assert.False(t, user_model.IsFollowing(doer.ID, blockedUser.ID))
|
||||
assert.False(t, user_model.IsFollowing(blockedUser.ID, doer.ID))
|
||||
|
||||
// Ensure blocked user isn't following doer's repository.
|
||||
assert.False(t, repo_model.IsWatching(blockedUser.ID, repo.ID))
|
||||
}
|
|
@ -90,6 +90,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
|
|||
&pull_model.AutoMerge{DoerID: u.ID},
|
||||
&pull_model.ReviewState{UserID: u.ID},
|
||||
&user_model.Redirect{RedirectUserID: u.ID},
|
||||
&user_model.BlockedUser{BlockID: u.ID},
|
||||
&user_model.BlockedUser{UserID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization profile">
|
||||
{{if .Flash}}
|
||||
<div class="ui container gt-mb-5">
|
||||
{{template "base/alert" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="ui container gt-df">
|
||||
{{avatar $.Context .Org 140 "org-avatar"}}
|
||||
<div id="org-info">
|
||||
|
|
40
templates/org/settings/blocked_users.tmpl
Normal file
40
templates/org/settings/blocked_users.tmpl
Normal file
|
@ -0,0 +1,40 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}}
|
||||
<div class="org-setting-content">
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form ignore-dirty" id="block-user-form" action="{{$.Link}}/block" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="uid" value="">
|
||||
<div class="inline field ui left">
|
||||
<div id="search-user-box" class="ui search">
|
||||
<div class="ui input">
|
||||
<input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="ui red button">{{.locale.Tr "user.block"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui bottom attached table segment blocked-users">
|
||||
{{range .BlockedUsers}}
|
||||
<div class="item gt-df gt-ac gt-fw">
|
||||
{{avatar $.Context . 48 "gt-mr-3 gt-mb-0"}}
|
||||
<div class="gt-df gt-fc">
|
||||
<a href="{{.HomeLink}}">{{.Name}}</a>
|
||||
<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
|
||||
</div>
|
||||
<div class="gt-ml-auto content">
|
||||
<form action="{{$.Link}}/unblock" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="user_id" value="{{.ID}}">
|
||||
<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item">
|
||||
<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
|
@ -35,6 +35,9 @@
|
|||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
|
||||
{{.locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
|
||||
{{.locale.Tr "org.settings.delete"}}
|
||||
</a>
|
||||
|
|
228
templates/swagger/v1_json.tmpl
generated
228
templates/swagger/v1_json.tmpl
generated
|
@ -1595,6 +1595,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/block/{username}": {
|
||||
"put": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"organization"
|
||||
],
|
||||
"summary": "Blocks a user from the organization",
|
||||
"operationId": "orgBlockUser",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the org",
|
||||
"name": "org",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/hooks": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -1959,6 +1995,44 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/list_blocked": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"organization"
|
||||
],
|
||||
"summary": "List the organization's blocked users",
|
||||
"operationId": "orgListBlockedUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the org",
|
||||
"name": "org",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results to return (1-based)",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page size of results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/BlockedUserList"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/members": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -2423,6 +2497,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/unblock/{username}": {
|
||||
"put": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"organization"
|
||||
],
|
||||
"summary": "Unblock a user from the organization",
|
||||
"operationId": "orgUnblockUser",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the org",
|
||||
"name": "org",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/packages/{owner}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -13790,6 +13900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/block/{username}": {
|
||||
"put": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Blocks a user from the doer.",
|
||||
"operationId": "userBlockUser",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/emails": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -13966,6 +14105,9 @@
|
|||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -14436,6 +14578,37 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/list_blocked": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "List the authenticated user's blocked users",
|
||||
"operationId": "userListBlockedUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results to return (1-based)",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page size of results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/BlockedUserList"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/orgs": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -14837,6 +15010,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/unblock/{username}": {
|
||||
"put": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Unblocks a user from the doer.",
|
||||
"operationId": "userUnblockUser",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/search": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -15767,6 +15969,23 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"BlockedUser": {
|
||||
"type": "object",
|
||||
"title": "BlockedUser represents a blocked user.",
|
||||
"properties": {
|
||||
"block_id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "BlockID"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Created"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"Branch": {
|
||||
"description": "Branch represents a repository branch",
|
||||
"type": "object",
|
||||
|
@ -21951,6 +22170,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"BlockedUserList": {
|
||||
"description": "BlockedUserList",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/BlockedUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Branch": {
|
||||
"description": "Branch",
|
||||
"schema": {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content user profile">
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui four wide column">
|
||||
<div class="ui card">
|
||||
|
@ -115,6 +116,21 @@
|
|||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
<li class="block">
|
||||
{{if $.IsBlocked}}
|
||||
<form method="post" action="{{.Link}}?action=unblock&redirect_to={{$.Link}}">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button type="submit" class="ui basic red button">{{svg "octicon-blocked"}} {{.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form>
|
||||
<button type="submit" class="ui basic orange button delete-button"
|
||||
data-modal-id="block-user" data-url="{{.Link}}?action=block">
|
||||
{{svg "octicon-blocked"}} {{.locale.Tr "user.block"}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -156,4 +172,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small basic delete modal" id="block-user">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-blocked" 16 "blocked inside"}}
|
||||
{{$.locale.Tr "user.block_user"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{$.locale.Tr "user.block_user.detail"}}</p>
|
||||
<ul>
|
||||
<li>{{$.locale.Tr "user.block_user.detail_1"}}</li>
|
||||
<li>{{$.locale.Tr "user.block_user.detail_2"}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
||||
{{template "base/footer" .}}
|
||||
|
|
31
templates/user/settings/blocked_users.tmpl
Normal file
31
templates/user/settings/blocked_users.tmpl
Normal file
|
@ -0,0 +1,31 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
|
||||
<div class="user-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{.locale.Tr "settings.blocked_users"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="ui blocked-user list gt-mt-0">
|
||||
{{range .BlockedUsers}}
|
||||
<div class="item gt-df gt-ac">
|
||||
{{avatar $.Context . 28 "gt-mr-3"}}
|
||||
<div class="gt-df gt-fc">
|
||||
<a href="{{.HomeLink}}">{{.Name}}</a>
|
||||
<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
|
||||
</div>
|
||||
<div class="gt-ml-auto content">
|
||||
<form action="{{$.Link}}/unblock" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="user_id" value="{{.ID}}">
|
||||
<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item">
|
||||
<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
|
@ -48,5 +48,8 @@
|
|||
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
||||
{{.locale.Tr "settings.repos"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
|
||||
{{.locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
101
tests/integration/api_block_test.go
Normal file
101
tests/integration/api_block_test.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIUserBlock(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := "user4"
|
||||
session := loginUser(t, user)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
t.Run("BlockUser", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/block/user2?token=%s", token))
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2})
|
||||
})
|
||||
|
||||
t.Run("ListBlocked", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/list_blocked?token=%s", token))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// One user just got blocked and the other one is defined in the fixtures.
|
||||
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
var blockedUsers []api.BlockedUser
|
||||
DecodeJSON(t, resp, &blockedUsers)
|
||||
assert.Len(t, blockedUsers, 2)
|
||||
assert.EqualValues(t, 1, blockedUsers[0].BlockID)
|
||||
assert.EqualValues(t, 2, blockedUsers[1].BlockID)
|
||||
})
|
||||
|
||||
t.Run("UnblockUser", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/unblock/user2?token=%s", token))
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIOrgBlock(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := "user5"
|
||||
org := "user6"
|
||||
session := loginUser(t, user)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
|
||||
|
||||
t.Run("BlockUser", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/user2?token=%s", org, token))
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2})
|
||||
})
|
||||
|
||||
t.Run("ListBlocked", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/list_blocked?token=%s", org, token))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
var blockedUsers []api.BlockedUser
|
||||
DecodeJSON(t, resp, &blockedUsers)
|
||||
assert.Len(t, blockedUsers, 1)
|
||||
assert.EqualValues(t, 2, blockedUsers[0].BlockID)
|
||||
})
|
||||
|
||||
t.Run("UnblockUser", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/unblock/user2?token=%s", org, token))
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2})
|
||||
})
|
||||
}
|
|
@ -19,7 +19,7 @@ func TestAPIFollow(t *testing.T) {
|
|||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user1 := "user4"
|
||||
user2 := "user1"
|
||||
user2 := "user10"
|
||||
|
||||
session1 := loginUser(t, user1)
|
||||
token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser)
|
||||
|
|
212
tests/integration/block_test.go
Normal file
212
tests/integration/block_test.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BlockUser(t *testing.T, doer, blockedUser *user_model.User) {
|
||||
t.Helper()
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
|
||||
|
||||
session := loginUser(t, doer.Name)
|
||||
req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
||||
"action": "block",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
type redirect struct {
|
||||
Redirect string `json:"redirect"`
|
||||
}
|
||||
|
||||
var respBody redirect
|
||||
DecodeJSON(t, resp, &respBody)
|
||||
assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect)
|
||||
assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
|
||||
}
|
||||
|
||||
func TestBlockUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
BlockUser(t, doer, blockedUser)
|
||||
|
||||
// Unblock user.
|
||||
session := loginUser(t, doer.Name)
|
||||
req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
||||
"action": "unblock",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
loc := resp.Header().Get("Location")
|
||||
assert.EqualValues(t, "/"+blockedUser.Name, loc)
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})
|
||||
}
|
||||
|
||||
func TestBlockIssueCreation(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: doer.ID})
|
||||
BlockUser(t, doer, blockedUser)
|
||||
|
||||
session := loginUser(t, blockedUser.Name)
|
||||
req := NewRequest(t, "GET", "/"+repo.OwnerName+"/"+repo.Name+"/issues/new")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
assert.True(t, exists)
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"title": "Title",
|
||||
"content": "Hello!",
|
||||
})
|
||||
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
assert.Contains(t,
|
||||
htmlDoc.doc.Find(".ui.negative.message").Text(),
|
||||
translation.NewLocale("en-US").Tr("repo.issues.blocked_by_user"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestBlockIssueReaction(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 4, PosterID: doer.ID, RepoID: repo.ID})
|
||||
issueURL := fmt.Sprintf("/%s/%s/issues/%d", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), issue.Index)
|
||||
|
||||
BlockUser(t, doer, blockedUser)
|
||||
|
||||
session := loginUser(t, blockedUser.Name)
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"content": "eyes",
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
type reactionResponse struct {
|
||||
Empty bool `json:"empty"`
|
||||
}
|
||||
|
||||
var respBody reactionResponse
|
||||
DecodeJSON(t, resp, &respBody)
|
||||
|
||||
assert.EqualValues(t, true, respBody.Empty)
|
||||
}
|
||||
|
||||
func TestBlockCommentReaction(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 1, RepoID: repo.ID})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{ID: 3, PosterID: doer.ID, IssueID: issue.ID})
|
||||
_ = comment.LoadIssue(db.DefaultContext)
|
||||
issueURL := fmt.Sprintf("/%s/%s/issues/%d", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), issue.Index)
|
||||
|
||||
BlockUser(t, doer, blockedUser)
|
||||
|
||||
session := loginUser(t, blockedUser.Name)
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", path.Join(repo.Link(), "/comments/", strconv.FormatInt(comment.ID, 10), "/reactions/react"), map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"content": "eyes",
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
type reactionResponse struct {
|
||||
Empty bool `json:"empty"`
|
||||
}
|
||||
|
||||
var respBody reactionResponse
|
||||
DecodeJSON(t, resp, &respBody)
|
||||
|
||||
assert.EqualValues(t, true, respBody.Empty)
|
||||
}
|
||||
|
||||
// TestBlockFollow ensures that the doer and blocked user cannot follow each other.
|
||||
func TestBlockFollow(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
BlockUser(t, doer, blockedUser)
|
||||
|
||||
// Doer cannot follow blocked user.
|
||||
session := loginUser(t, doer.Name)
|
||||
req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
||||
"action": "follow",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
|
||||
|
||||
// Blocked user cannot follow doer.
|
||||
session = loginUser(t, blockedUser.Name)
|
||||
req = NewRequestWithValues(t, "POST", "/"+doer.Name, map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/"+doer.Name),
|
||||
"action": "follow",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
|
||||
}
|
||||
|
||||
// TestBlockUserFromOrganization ensures that an organisation can block and unblock an user.
|
||||
func TestBlockUserFromOrganization(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
|
||||
blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17, Type: user_model.UserTypeOrganization})
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
|
||||
|
||||
session := loginUser(t, doer.Name)
|
||||
req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/block", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
|
||||
"uname": blockedUser.Name,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID}))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/unblock", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
|
||||
"user_id": strconv.FormatInt(blockedUser.ID, 10),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
|
||||
}
|
|
@ -191,30 +191,35 @@
|
|||
}
|
||||
|
||||
.organization.teams .repositories .item,
|
||||
.organization.teams .members .item {
|
||||
.organization.teams .members .item,
|
||||
.organization.settings .blocked-users .item {
|
||||
padding: 10px 19px;
|
||||
}
|
||||
|
||||
.organization.teams .repositories .item:not(:last-child),
|
||||
.organization.teams .members .item:not(:last-child) {
|
||||
.organization.teams .members .item:not(:last-child),
|
||||
.organization.settings .blocked-users .item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.organization.teams .repositories .item .button,
|
||||
.organization.teams .members .item .button {
|
||||
.organization.teams .members .item .button,
|
||||
.organization.settings .blocked-users .item button {
|
||||
padding: 9px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.organization.teams #add-repo-form input,
|
||||
.organization.teams #repo-multiple-form input,
|
||||
.organization.teams #add-member-form input {
|
||||
.organization.teams #add-member-form input,
|
||||
.organization.settings #block-user-form input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.organization.teams #add-repo-form .ui.button,
|
||||
.organization.teams #repo-multiple-form .ui.button,
|
||||
.organization.teams #add-member-form .ui.button {
|
||||
.organization.teams #add-member-form .ui.button,
|
||||
.organization.settings #block-user-form .ui.button {
|
||||
margin-left: 5px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,11 @@
|
|||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.user.profile .ui.card .extra.content > ul > li.follow .ui.button {
|
||||
.user.profile .ui.card .extra.content > ul > li.follow .ui.button,
|
||||
.user.profile .ui.card .extra.content > ul > li.block .ui.button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue