From 79ff020f182327986dcfd874bc49d4fe32efc29a Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 9 Jun 2023 08:07:03 +0000 Subject: [PATCH] [MODERATION] organization blocking a user (#802) - Resolves #476 - Follow up for: #540 - Ensure that the doer and blocked person cannot follow each other. - Ensure that the block person cannot watch doer's repositories. - Add unblock button to the blocked user list. - Add blocked since information to the blocked user list. - Add extra testing to moderation code. - Blocked user will unwatch doer's owned repository upon blocking. - Add flash messages to let the user know the block/unblock action was successful. - Add "You haven't blocked any users" message. - Add organization blocking a user. Co-authored-by: Gusted Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/802 (cherry picked from commit 0505a1042197bd9136b58bc70ec7400a23471585) (cherry picked from commit 37b4e6ef9b85e97d651cf350c9f3ea272ee8d76a) (cherry picked from commit 217475385a815298dcbd8029e0cc8cb2c5877bae) (cherry picked from commit f2c38ce5c2f6cf4008aa1929539063715b50562c) (cherry picked from commit 1edfb68137d8c322a7a9a7c7196fc8f01ff1a889) (cherry picked from commit 2cbc12dc740e6fefc196b7fea6ac8a0ffbbfbeef) --- models/fixtures/repository.yml | 2 +- models/fixtures/watch.yml | 8 ++- models/repo/user_repo.go | 13 +++++ models/repo/user_repo_test.go | 13 +++++ models/repo/watch.go | 6 +++ models/repo/watch_test.go | 13 +++++ models/user/block.go | 4 +- models/user/follow.go | 15 ++++-- models/user/user_test.go | 6 +++ options/locale/locale_en-US.ini | 7 +++ routers/api/v1/user/follower.go | 7 +++ routers/web/org/setting/blocked_users.go | 61 ++++++++++++++++++++++ routers/web/user/profile.go | 13 ++++- routers/web/user/setting/blocked_users.go | 11 ++++ routers/web/web.go | 11 +++- services/user/block.go | 20 ++++++- services/user/block_test.go | 41 +++++++++++++++ templates/org/home.tmpl | 5 ++ templates/org/settings/blocked_users.tmpl | 40 ++++++++++++++ templates/org/settings/navbar.tmpl | 3 ++ templates/swagger/v1_json.tmpl | 3 ++ templates/user/profile.tmpl | 1 + templates/user/settings/blocked_users.tmpl | 17 +++++- tests/integration/api_user_follow_test.go | 2 +- tests/integration/block_test.go | 56 +++++++++++++++++++- web_src/css/org.css | 15 ++++-- 26 files changed, 375 insertions(+), 18 deletions(-) create mode 100644 routers/web/org/setting/blocked_users.go create mode 100644 services/user/block_test.go create mode 100644 templates/org/settings/blocked_users.tmpl diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 050a9e2d06..d50c89838d 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -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 diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index c29f6bb65a..c6c9726cc8 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -26,4 +26,10 @@ id: 5 user_id: 11 repo_id: 1 - mode: 3 # auto + mode: 3 # auto + +- + id: 6 + user_id: 4 + repo_id: 2 + mode: 1 # normal diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index dd2ef62201..5d6e24e2a5 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -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 +} diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index 7816b0262a..ad794beb9b 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -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]) +} diff --git a/models/repo/watch.go b/models/repo/watch.go index 6ff3a3f7b3..53b35c0e51 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -201,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 +} diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index b6ae2a0ef5..02d0d3b0dd 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -155,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}) +} diff --git a/models/user/block.go b/models/user/block.go index 64dd93ed38..838bc7431e 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -53,10 +53,12 @@ func UnblockUser(ctx context.Context, userID, blockID int64) error { } // 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) ([]*User, error) { users := make([]*User, 0, 8) err := db.GetEngine(ctx). - Select("`user`.*"). + 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). Find(&users) diff --git a/models/user/follow.go b/models/user/follow.go index 936efbc164..5b3ff489ca 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -24,16 +24,25 @@ 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(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || IsFollowing(userID, followID) { + if userID == followID || IsFollowingCtx(ctx, userID, followID) { return nil } + if IsBlocked(ctx, userID, followID) || IsBlocked(ctx, followID, userID) { + return ErrBlockedByUser + } + ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -56,7 +65,7 @@ func FollowUser(ctx context.Context, userID, followID int64) (err error) { // UnfollowUser unmarks someone as another's follower. func UnfollowUser(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || !IsFollowing(userID, followID) { + if userID == followID || !IsFollowingCtx(ctx, userID, followID) { return nil } diff --git a/models/user/user_test.go b/models/user/user_test.go index d5f0e80510..e21e9ad52e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -457,6 +457,12 @@ func TestFollowUser(t *testing.T) { 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{}) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2868170add..aa9a166991 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -601,6 +601,7 @@ 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. @@ -892,6 +893,7 @@ hooks.desc = Add webhooks which will be triggered for all repositoriesCANNOT be undone. @@ -914,6 +916,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? Migrate repository. owner = Owner @@ -2523,6 +2529,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. diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 364507711d..bc1e6724f7 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -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, 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 } diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go new file mode 100644 index 0000000000..eae6f81fa0 --- /dev/null +++ b/routers/web/org/setting/blocked_users.go @@ -0,0 +1,61 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + "strings" + + 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) + 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") +} diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index d91bf23f91..d103ed4ae2 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -5,6 +5,7 @@ package user import ( + "errors" "fmt" "net/http" "strings" @@ -369,8 +370,16 @@ func Action(ctx *context.Context) { } if err != nil { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err) - return + 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 { diff --git a/routers/web/user/setting/blocked_users.go b/routers/web/user/setting/blocked_users.go index ea6ccf74d9..134becf969 100644 --- a/routers/web/user/setting/blocked_users.go +++ b/routers/web/user/setting/blocked_users.go @@ -32,3 +32,14 @@ func BlockedUsers(ctx *context.Context) { 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") +} diff --git a/routers/web/web.go b/routers/web/web.go index 46f4080d7d..8e64be56c3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -517,7 +517,10 @@ func registerRoutes(m *web.Route) { addWebhookEditRoutes() }, webhooksEnabled) - m.Get("/blocked_users", user_setting.BlockedUsers) + 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 +773,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() { diff --git a/services/user/block.go b/services/user/block.go index eff3242784..05f9a376a7 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -6,6 +6,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" ) @@ -30,11 +31,28 @@ func BlockUser(ctx context.Context, userID, blockID int64) error { return err } - // Unfollow the user from block's perspective. + // 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() } diff --git a/services/user/block_test.go b/services/user/block_test.go new file mode 100644 index 0000000000..8a0a3c4739 --- /dev/null +++ b/services/user/block_test.go @@ -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)) +} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index d540f80352..2e53385f36 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -1,5 +1,10 @@ {{template "base/head" .}}
+ {{if .Flash}} +
+ {{template "base/alert" .}} +
+ {{end}}
{{avatar $.Context .Org 140 "org-avatar"}}
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl new file mode 100644 index 0000000000..e551ee5d67 --- /dev/null +++ b/templates/org/settings/blocked_users.tmpl @@ -0,0 +1,40 @@ +{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}} +
+
+
+ {{.CsrfTokenHtml}} + +
+ +
+ +
+
+
+ {{range .BlockedUsers}} +
+ {{avatar $.Context . 48 "gt-mr-3 gt-mb-0"}} +
+ {{.Name}} + {{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}} +
+
+
+ {{$.CsrfTokenHtml}} + + +
+
+
+ {{else}} +
+ {{$.locale.Tr "settings.blocked_users_none"}} +
+ {{end}} +
+
+{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index 6bea9f5f60..20c3279b7b 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -35,6 +35,9 @@
{{end}} + + {{.locale.Tr "settings.blocked_users"}} + {{.locale.Tr "org.settings.delete"}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e7d1cc1fe5..d1f21b3cf9 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13966,6 +13966,9 @@ "responses": { "204": { "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" } } }, diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 58aab7167c..ddce4bbdab 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -1,6 +1,7 @@ {{template "base/head" .}}
+ {{template "base/alert" .}}
diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl index fd0cb07883..dc90970ec2 100644 --- a/templates/user/settings/blocked_users.tmpl +++ b/templates/user/settings/blocked_users.tmpl @@ -6,8 +6,23 @@
{{range .BlockedUsers}} +
+ {{avatar $.Context . 28 "gt-mr-3"}} +
+ {{.Name}} + {{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}} +
+
+
+ {{$.CsrfTokenHtml}} + + +
+
+
+ {{else}}
- {{avatar $.Context . 28 "gt-mr-3"}}{{.Name}} + {{$.locale.Tr "settings.blocked_users_none"}}
{{end}}
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go index 62717af90e..bf6560b103 100644 --- a/tests/integration/api_user_follow_test.go +++ b/tests/integration/api_user_follow_test.go @@ -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) diff --git a/tests/integration/block_test.go b/tests/integration/block_test.go index 03a5b14712..b8001f4968 100644 --- a/tests/integration/block_test.go +++ b/tests/integration/block_test.go @@ -41,7 +41,7 @@ func BlockUser(t *testing.T, doer, blockedUser *user_model.User) { var respBody redirect DecodeJSON(t, resp, &respBody) assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect) - assert.EqualValues(t, true, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})) + assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})) } func TestBlockUser(t *testing.T) { @@ -156,3 +156,57 @@ func TestBlockCommentReaction(t *testing.T) { 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}) +} diff --git a/web_src/css/org.css b/web_src/css/org.css index 9711ed25ad..30322a2c99 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -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; }