mirror of https://github.com/go-gitea/gitea
Add user blocking (#29028)
Fixes #17453 This PR adds the abbility to block a user from a personal account or organization to restrict how the blocked user can interact with the blocker. The docs explain what's the consequence of blocking a user. Screenshots: ![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b) --------- Co-authored-by: Lauris BH <lauris@nix.lv>pull/28894/head^2
parent
8e12ba34ba
commit
c337ff0ec7
@ -0,0 +1,56 @@ |
|||||||
|
--- |
||||||
|
date: "2024-01-31T00:00:00+00:00" |
||||||
|
title: "Blocking a user" |
||||||
|
slug: "blocking-user" |
||||||
|
sidebar_position: 25 |
||||||
|
toc: false |
||||||
|
draft: false |
||||||
|
aliases: |
||||||
|
- /en-us/webhooks |
||||||
|
menu: |
||||||
|
sidebar: |
||||||
|
parent: "usage" |
||||||
|
name: "Blocking a user" |
||||||
|
sidebar_position: 30 |
||||||
|
identifier: "blocking-user" |
||||||
|
--- |
||||||
|
|
||||||
|
# Blocking a user |
||||||
|
|
||||||
|
Gitea supports blocking of users to restrict how they can interact with you and your content. |
||||||
|
|
||||||
|
You can block a user in your account settings, from the user's profile or from comments created by the user. |
||||||
|
The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you. |
||||||
|
Organization owners can block anyone who is not a member of the organization too. |
||||||
|
If a blocked user has admin permissions, they can still perform all actions even if blocked. |
||||||
|
|
||||||
|
### When you block a user |
||||||
|
|
||||||
|
- the user stops following you |
||||||
|
- you stop following the user |
||||||
|
- the user's stars are removed from your repositories |
||||||
|
- your stars are removed from their repositories |
||||||
|
- the user stops watching your repositories |
||||||
|
- you stop watching their repositories |
||||||
|
- the user's issue assignments are removed from your repositories |
||||||
|
- your issue assignments are removed from their repositories |
||||||
|
- the user is removed as a collaborator on your repositories |
||||||
|
- you are removed as a collaborator on their repositories |
||||||
|
- any pending repository transfers to or from the blocked user are canceled |
||||||
|
|
||||||
|
### When you block a user, the user cannot |
||||||
|
|
||||||
|
- follow you |
||||||
|
- watch your repositories |
||||||
|
- star your repositories |
||||||
|
- fork your repositories |
||||||
|
- transfer repositories to you |
||||||
|
- open issues or pull requests on your repositories |
||||||
|
- comment on issues or pull requests you've created |
||||||
|
- comment on issues or pull requests on your repositories |
||||||
|
- react to your comments on issues or pull requests |
||||||
|
- react to comments on issues or pull requests on your repositories |
||||||
|
- assign you to issues or pull requests |
||||||
|
- add you as a collaborator on their repositories |
||||||
|
- send you notifications by @mentioning your username |
||||||
|
- be added as team member (if blocked by an organization) |
@ -0,0 +1,19 @@ |
|||||||
|
- |
||||||
|
id: 1 |
||||||
|
blocker_id: 2 |
||||||
|
blockee_id: 29 |
||||||
|
|
||||||
|
- |
||||||
|
id: 2 |
||||||
|
blocker_id: 17 |
||||||
|
blockee_id: 28 |
||||||
|
|
||||||
|
- |
||||||
|
id: 3 |
||||||
|
blocker_id: 2 |
||||||
|
blockee_id: 34 |
||||||
|
|
||||||
|
- |
||||||
|
id: 4 |
||||||
|
blocker_id: 50 |
||||||
|
blockee_id: 34 |
@ -0,0 +1,26 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_22 //nolint
|
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
|
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
type Blocking struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
BlockerID int64 `xorm:"UNIQUE(block)"` |
||||||
|
BlockeeID int64 `xorm:"UNIQUE(block)"` |
||||||
|
Note string |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
||||||
|
} |
||||||
|
|
||||||
|
func (*Blocking) TableName() string { |
||||||
|
return "user_blocking" |
||||||
|
} |
||||||
|
|
||||||
|
func AddUserBlockingTable(x *xorm.Engine) error { |
||||||
|
return x.Sync(&Blocking{}) |
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/modules/container" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
|
||||||
|
"xorm.io/builder" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization") |
||||||
|
ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user") |
||||||
|
ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user") |
||||||
|
ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked") |
||||||
|
) |
||||||
|
|
||||||
|
type Blocking struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
BlockerID int64 `xorm:"UNIQUE(block)"` |
||||||
|
Blocker *User `xorm:"-"` |
||||||
|
BlockeeID int64 `xorm:"UNIQUE(block)"` |
||||||
|
Blockee *User `xorm:"-"` |
||||||
|
Note string |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
||||||
|
} |
||||||
|
|
||||||
|
func (*Blocking) TableName() string { |
||||||
|
return "user_blocking" |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(Blocking)) |
||||||
|
} |
||||||
|
|
||||||
|
func UpdateBlockingNote(ctx context.Context, id int64, note string) error { |
||||||
|
_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note}) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool { |
||||||
|
if len(blockerIDs) == 0 { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if blockee.IsAdmin { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}. |
||||||
|
And(builder.In("user_blocking.blocker_id", blockerIDs)) |
||||||
|
|
||||||
|
has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{}) |
||||||
|
return has |
||||||
|
} |
||||||
|
|
||||||
|
type FindBlockingOptions struct { |
||||||
|
db.ListOptions |
||||||
|
BlockerID int64 |
||||||
|
BlockeeID int64 |
||||||
|
} |
||||||
|
|
||||||
|
func (opts *FindBlockingOptions) ToConds() builder.Cond { |
||||||
|
cond := builder.NewCond() |
||||||
|
if opts.BlockerID != 0 { |
||||||
|
cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID}) |
||||||
|
} |
||||||
|
if opts.BlockeeID != 0 { |
||||||
|
cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID}) |
||||||
|
} |
||||||
|
return cond |
||||||
|
} |
||||||
|
|
||||||
|
func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) { |
||||||
|
return db.FindAndCount[Blocking](ctx, opts) |
||||||
|
} |
||||||
|
|
||||||
|
func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) { |
||||||
|
blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{ |
||||||
|
BlockerID: blockerID, |
||||||
|
BlockeeID: blockeeID, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(blocks) == 0 { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
return blocks[0], nil |
||||||
|
} |
||||||
|
|
||||||
|
type BlockingList []*Blocking |
||||||
|
|
||||||
|
func (blocks BlockingList) LoadAttributes(ctx context.Context) error { |
||||||
|
ids := make(container.Set[int64], len(blocks)*2) |
||||||
|
for _, b := range blocks { |
||||||
|
ids.Add(b.BlockerID) |
||||||
|
ids.Add(b.BlockeeID) |
||||||
|
} |
||||||
|
|
||||||
|
userList, err := GetUsersByIDs(ctx, ids.Values()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
userMap := make(map[int64]*User, len(userList)) |
||||||
|
for _, u := range userList { |
||||||
|
userMap[u.ID] = u |
||||||
|
} |
||||||
|
|
||||||
|
for _, b := range blocks { |
||||||
|
b.Blocker = userMap[b.BlockerID] |
||||||
|
b.Blockee = userMap[b.BlockeeID] |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,116 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package org |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/routers/api/v1/shared" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
) |
||||||
|
|
||||||
|
func ListBlocks(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
|
||||||
|
// ---
|
||||||
|
// summary: List users blocked by the organization
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/UserList"
|
||||||
|
|
||||||
|
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser()) |
||||||
|
} |
||||||
|
|
||||||
|
func CheckUserBlock(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
|
||||||
|
// ---
|
||||||
|
// summary: Check if a user is blocked by the organization
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to check
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser()) |
||||||
|
} |
||||||
|
|
||||||
|
func BlockUser(ctx *context.APIContext) { |
||||||
|
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
|
||||||
|
// ---
|
||||||
|
// summary: Block a user
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to block
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: note
|
||||||
|
// in: query
|
||||||
|
// description: optional note for the block
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.BlockUser(ctx, ctx.Org.Organization.AsUser()) |
||||||
|
} |
||||||
|
|
||||||
|
func UnblockUser(ctx *context.APIContext) { |
||||||
|
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
|
||||||
|
// ---
|
||||||
|
// summary: Unblock a user
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to unblock
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser()) |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package shared |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
api "code.gitea.io/gitea/modules/structs" |
||||||
|
"code.gitea.io/gitea/routers/api/v1/utils" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
"code.gitea.io/gitea/services/convert" |
||||||
|
user_service "code.gitea.io/gitea/services/user" |
||||||
|
) |
||||||
|
|
||||||
|
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { |
||||||
|
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{ |
||||||
|
ListOptions: utils.GetListOptions(ctx), |
||||||
|
BlockerID: blocker.ID, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
ctx.Error(http.StatusInternalServerError, "FindBlockings", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil { |
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
users := make([]*api.User, 0, len(blocks)) |
||||||
|
for _, b := range blocks { |
||||||
|
users = append(users, convert.ToUser(ctx, b.Blockee, blocker)) |
||||||
|
} |
||||||
|
|
||||||
|
ctx.SetTotalCountHeader(total) |
||||||
|
ctx.JSON(http.StatusOK, &users) |
||||||
|
} |
||||||
|
|
||||||
|
func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { |
||||||
|
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) |
||||||
|
if err != nil { |
||||||
|
ctx.NotFound("GetUserByName", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
status := http.StatusNotFound |
||||||
|
blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) |
||||||
|
if err != nil { |
||||||
|
ctx.Error(http.StatusInternalServerError, "GetBlocking", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if blocking != nil { |
||||||
|
status = http.StatusNoContent |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Status(status) |
||||||
|
} |
||||||
|
|
||||||
|
func BlockUser(ctx *context.APIContext, blocker *user_model.User) { |
||||||
|
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) |
||||||
|
if err != nil { |
||||||
|
ctx.NotFound("GetUserByName", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil { |
||||||
|
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { |
||||||
|
ctx.Error(http.StatusBadRequest, "BlockUser", err) |
||||||
|
} else { |
||||||
|
ctx.Error(http.StatusInternalServerError, "BlockUser", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent) |
||||||
|
} |
||||||
|
|
||||||
|
func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { |
||||||
|
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username")) |
||||||
|
if err != nil { |
||||||
|
ctx.NotFound("GetUserByName", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil { |
||||||
|
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { |
||||||
|
ctx.Error(http.StatusBadRequest, "UnblockUser", err) |
||||||
|
} else { |
||||||
|
ctx.Error(http.StatusInternalServerError, "UnblockUser", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent) |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/routers/api/v1/shared" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
) |
||||||
|
|
||||||
|
func ListBlocks(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /user/blocks user userListBlocks
|
||||||
|
// ---
|
||||||
|
// summary: List users blocked by the authenticated user
|
||||||
|
// parameters:
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/UserList"
|
||||||
|
|
||||||
|
shared.ListBlocks(ctx, ctx.Doer) |
||||||
|
} |
||||||
|
|
||||||
|
func CheckUserBlock(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
|
||||||
|
// ---
|
||||||
|
// summary: Check if a user is blocked by the authenticated user
|
||||||
|
// parameters:
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to check
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.CheckUserBlock(ctx, ctx.Doer) |
||||||
|
} |
||||||
|
|
||||||
|
func BlockUser(ctx *context.APIContext) { |
||||||
|
// swagger:operation PUT /user/blocks/{username} user userBlockUser
|
||||||
|
// ---
|
||||||
|
// summary: Block a user
|
||||||
|
// parameters:
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to block
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: note
|
||||||
|
// in: query
|
||||||
|
// description: optional note for the block
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.BlockUser(ctx, ctx.Doer) |
||||||
|
} |
||||||
|
|
||||||
|
func UnblockUser(ctx *context.APIContext) { |
||||||
|
// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
|
||||||
|
// ---
|
||||||
|
// summary: Unblock a user
|
||||||
|
// parameters:
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: user to unblock
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.UnblockUser(ctx, ctx.Doer, ctx.Doer) |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package org |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
shared_user "code.gitea.io/gitea/routers/web/shared/user" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users" |
||||||
|
) |
||||||
|
|
||||||
|
func BlockedUsers(ctx *context.Context) { |
||||||
|
ctx.Data["Title"] = ctx.Tr("user.block.list") |
||||||
|
ctx.Data["PageIsOrgSettings"] = true |
||||||
|
ctx.Data["PageIsSettingsBlockedUsers"] = true |
||||||
|
|
||||||
|
shared_user.BlockedUsers(ctx, ctx.ContextUser) |
||||||
|
if ctx.Written() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers) |
||||||
|
} |
||||||
|
|
||||||
|
func BlockedUsersPost(ctx *context.Context) { |
||||||
|
shared_user.BlockedUsersPost(ctx, ctx.ContextUser) |
||||||
|
if ctx.Written() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users") |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
|
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
"code.gitea.io/gitea/services/forms" |
||||||
|
user_service "code.gitea.io/gitea/services/user" |
||||||
|
) |
||||||
|
|
||||||
|
func BlockedUsers(ctx *context.Context, blocker *user_model.User) { |
||||||
|
blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{ |
||||||
|
BlockerID: blocker.ID, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("FindBlockings", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil { |
||||||
|
ctx.ServerError("LoadAttributes", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["UserBlocks"] = blocks |
||||||
|
} |
||||||
|
|
||||||
|
func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) { |
||||||
|
form := web.GetForm(ctx).(*forms.BlockUserForm) |
||||||
|
if ctx.HasError() { |
||||||
|
ctx.ServerError("FormValidation", nil) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
blockee, err := user_model.GetUserByName(ctx, form.Blockee) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetUserByName", nil) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch form.Action { |
||||||
|
case "block": |
||||||
|
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil { |
||||||
|
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { |
||||||
|
ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error())) |
||||||
|
} else { |
||||||
|
ctx.ServerError("BlockUser", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
case "unblock": |
||||||
|
if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil { |
||||||
|
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { |
||||||
|
ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error())) |
||||||
|
} else { |
||||||
|
ctx.ServerError("UnblockUser", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
case "note": |
||||||
|
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetBlocking", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if block != nil { |
||||||
|
if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil { |
||||||
|
ctx.ServerError("UpdateBlockingNote", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
shared_user "code.gitea.io/gitea/routers/web/shared/user" |
||||||
|
"code.gitea.io/gitea/services/context" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users" |
||||||
|
) |
||||||
|
|
||||||
|
func BlockedUsers(ctx *context.Context) { |
||||||
|
ctx.Data["Title"] = ctx.Tr("user.block.list") |
||||||
|
ctx.Data["PageIsSettingsBlockedUsers"] = true |
||||||
|
|
||||||
|
shared_user.BlockedUsers(ctx, ctx.Doer) |
||||||
|
if ctx.Written() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers) |
||||||
|
} |
||||||
|
|
||||||
|
func BlockedUsersPost(ctx *context.Context) { |
||||||
|
shared_user.BlockedUsersPost(ctx, ctx.Doer) |
||||||
|
if ctx.Written() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users") |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
) |
||||||
|
|
||||||
|
// CreateIssueReaction creates a reaction on an issue.
|
||||||
|
func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) { |
||||||
|
if err := issue.LoadRepo(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) { |
||||||
|
return nil, user_model.ErrBlockedUser |
||||||
|
} |
||||||
|
|
||||||
|
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{ |
||||||
|
Type: content, |
||||||
|
DoerID: doer.ID, |
||||||
|
IssueID: issue.ID, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// CreateCommentReaction creates a reaction on a comment.
|
||||||
|
func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) { |
||||||
|
if err := comment.LoadIssue(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := comment.Issue.LoadRepo(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) { |
||||||
|
return nil, user_model.ErrBlockedUser |
||||||
|
} |
||||||
|
|
||||||
|
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{ |
||||||
|
Type: content, |
||||||
|
DoerID: doer.ID, |
||||||
|
IssueID: comment.Issue.ID, |
||||||
|
CommentID: comment.ID, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,308 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
issues_model "code.gitea.io/gitea/models/issues" |
||||||
|
org_model "code.gitea.io/gitea/models/organization" |
||||||
|
repo_model "code.gitea.io/gitea/models/repo" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
repo_service "code.gitea.io/gitea/services/repository" |
||||||
|
) |
||||||
|
|
||||||
|
func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool { |
||||||
|
if blocker.ID == blockee.ID { |
||||||
|
return false |
||||||
|
} |
||||||
|
if doer.ID == blockee.ID { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if blockee.IsOrganization() { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if blocker.IsOrganization() { |
||||||
|
org := org_model.OrgFromUser(blocker) |
||||||
|
if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember { |
||||||
|
return false |
||||||
|
} |
||||||
|
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin { |
||||||
|
return false |
||||||
|
} |
||||||
|
} else if !doer.IsAdmin && doer.ID != blocker.ID { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool { |
||||||
|
if doer.ID == blockee.ID { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if blocker.IsOrganization() { |
||||||
|
org := org_model.OrgFromUser(blocker) |
||||||
|
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin { |
||||||
|
return false |
||||||
|
} |
||||||
|
} else if !doer.IsAdmin && doer.ID != blocker.ID { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error { |
||||||
|
if blockee.IsOrganization() { |
||||||
|
return user_model.ErrBlockOrganization |
||||||
|
} |
||||||
|
|
||||||
|
if !CanBlockUser(ctx, doer, blocker, blockee) { |
||||||
|
return user_model.ErrCanNotBlock |
||||||
|
} |
||||||
|
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error { |
||||||
|
// unfollow each other
|
||||||
|
if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// unstar each other
|
||||||
|
if err := unstarRepos(ctx, blocker, blockee); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := unstarRepos(ctx, blockee, blocker); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// unwatch each others repositories
|
||||||
|
if err := unwatchRepos(ctx, blocker, blockee); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := unwatchRepos(ctx, blockee, blocker); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// unassign each other from issues
|
||||||
|
if err := unassignIssues(ctx, blocker, blockee); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := unassignIssues(ctx, blockee, blocker); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// remove each other from repository collaborations
|
||||||
|
if err := removeCollaborations(ctx, blocker, blockee); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := removeCollaborations(ctx, blockee, blocker); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// cancel each other repository transfers
|
||||||
|
if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return db.Insert(ctx, &user_model.Blocking{ |
||||||
|
BlockerID: blocker.ID, |
||||||
|
BlockeeID: blockee.ID, |
||||||
|
Note: note, |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error { |
||||||
|
opts := &repo_model.StarredReposOptions{ |
||||||
|
ListOptions: db.ListOptions{ |
||||||
|
Page: 1, |
||||||
|
PageSize: 25, |
||||||
|
}, |
||||||
|
StarrerID: starrer.ID, |
||||||
|
RepoOwnerID: repoOwner.ID, |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
repos, err := repo_model.GetStarredRepos(ctx, opts) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(repos) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, repo := range repos { |
||||||
|
if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
opts.Page++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error { |
||||||
|
opts := &repo_model.WatchedReposOptions{ |
||||||
|
ListOptions: db.ListOptions{ |
||||||
|
Page: 1, |
||||||
|
PageSize: 25, |
||||||
|
}, |
||||||
|
WatcherID: watcher.ID, |
||||||
|
RepoOwnerID: repoOwner.ID, |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
repos, _, err := repo_model.GetWatchedRepos(ctx, opts) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(repos) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, repo := range repos { |
||||||
|
if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
opts.Page++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { |
||||||
|
transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{ |
||||||
|
SenderID: sender.ID, |
||||||
|
RecipientID: recipient.ID, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
for _, transfer := range transfers { |
||||||
|
repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error { |
||||||
|
opts := &issues_model.AssignedIssuesOptions{ |
||||||
|
ListOptions: db.ListOptions{ |
||||||
|
Page: 1, |
||||||
|
PageSize: 25, |
||||||
|
}, |
||||||
|
AssigneeID: assignee.ID, |
||||||
|
RepoOwnerID: repoOwner.ID, |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
issues, _, err := issues_model.GetAssignedIssues(ctx, opts) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(issues) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, issue := range issues { |
||||||
|
if err := issue.LoadAssignees(ctx); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
opts.Page++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error { |
||||||
|
opts := &repo_model.FindCollaborationOptions{ |
||||||
|
ListOptions: db.ListOptions{ |
||||||
|
Page: 1, |
||||||
|
PageSize: 25, |
||||||
|
}, |
||||||
|
CollaboratorID: collaborator.ID, |
||||||
|
RepoOwnerID: repoOwner.ID, |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
collaborations, _, err := repo_model.GetCollaborators(ctx, opts) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(collaborations) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, collaboration := range collaborations { |
||||||
|
repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
opts.Page++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error { |
||||||
|
if blockee.IsOrganization() { |
||||||
|
return user_model.ErrBlockOrganization |
||||||
|
} |
||||||
|
|
||||||
|
if !CanUnblockUser(ctx, doer, blocker, blockee) { |
||||||
|
return user_model.ErrCanNotUnblock |
||||||
|
} |
||||||
|
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error { |
||||||
|
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if block != nil { |
||||||
|
_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID) |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestCanBlockUser(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||||
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) |
||||||
|
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) |
||||||
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) |
||||||
|
|
||||||
|
// Doer can't self block
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1)) |
||||||
|
// Blocker can't be blockee
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2)) |
||||||
|
// Can't block already blocked user
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29)) |
||||||
|
// Blockee can't be an organization
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3)) |
||||||
|
// Doer must be blocker or admin
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29)) |
||||||
|
// Organization can't block a member
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4)) |
||||||
|
// Doer must be organization owner or admin if blocker is an organization
|
||||||
|
assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2)) |
||||||
|
|
||||||
|
assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4)) |
||||||
|
assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4)) |
||||||
|
assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29)) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCanUnblockUser(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) |
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||||
|
user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) |
||||||
|
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) |
||||||
|
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) |
||||||
|
|
||||||
|
// Doer can't self unblock
|
||||||
|
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1)) |
||||||
|
// Can't unblock not blocked user
|
||||||
|
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28)) |
||||||
|
// Doer must be blocker or admin
|
||||||
|
assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29)) |
||||||
|
// Doer must be organization owner or admin if blocker is an organization
|
||||||
|
assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28)) |
||||||
|
|
||||||
|
assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29)) |
||||||
|
assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29)) |
||||||
|
assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28)) |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}} |
||||||
|
<div class="org-setting-content"> |
||||||
|
{{template "shared/user/blocked_users" .}} |
||||||
|
</div> |
||||||
|
{{template "org/settings/layout_footer" .}} |
@ -0,0 +1,23 @@ |
|||||||
|
<div class="ui small modal" id="block-user-modal"> |
||||||
|
<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div> |
||||||
|
<div class="content"> |
||||||
|
<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div> |
||||||
|
<form class="ui form modal-form" method="post"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="action" value="block" /> |
||||||
|
<input type="hidden" name="blockee" class="modal-blockee" /> |
||||||
|
<div class="field"> |
||||||
|
<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label> |
||||||
|
<input id="block-note" name="note"> |
||||||
|
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> |
||||||
|
</div> |
||||||
|
<div class="text right actions"> |
||||||
|
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button> |
||||||
|
<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,83 @@ |
|||||||
|
<h4 class="ui top attached header"> |
||||||
|
{{ctx.Locale.Tr "user.block.title"}} |
||||||
|
</h4> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<p>{{ctx.Locale.Tr "user.block.info_1"}}</p> |
||||||
|
<ul> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_2"}}</li> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_3"}}</li> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_4"}}</li> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_5"}}</li> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_6"}}</li> |
||||||
|
<li>{{ctx.Locale.Tr "user.block.info_7"}}</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div class="ui segment"> |
||||||
|
<form class="ui form ignore-dirty" action="{{$.Link}}" method="post"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="action" value="block" /> |
||||||
|
<div id="search-user-box" class="field ui fluid search input"> |
||||||
|
<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required> |
||||||
|
<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label> |
||||||
|
<input name="note"> |
||||||
|
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
<h4 class="ui top attached header"> |
||||||
|
{{ctx.Locale.Tr "user.block.list"}} |
||||||
|
</h4> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<div class="flex-list"> |
||||||
|
{{range .UserBlocks}} |
||||||
|
<div class="flex-item"> |
||||||
|
<div class="flex-item-leading"> |
||||||
|
{{ctx.AvatarUtils.Avatar .Blockee}} |
||||||
|
</div> |
||||||
|
<div class="flex-item-main"> |
||||||
|
<div class="flex-item-title"> |
||||||
|
<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a> |
||||||
|
</div> |
||||||
|
{{if .Note}} |
||||||
|
<div class="flex-item-body"> |
||||||
|
<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
<div class="flex-item-trailing"> |
||||||
|
<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button> |
||||||
|
<form action="{{$.Link}}" method="post"> |
||||||
|
{{$.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="action" value="unblock" /> |
||||||
|
<input type="hidden" name="blockee" value="{{.Blockee.Name}}" /> |
||||||
|
<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="ui small modal" id="block-user-note-modal"> |
||||||
|
<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div> |
||||||
|
<div class="content"> |
||||||
|
<form class="ui form" action="{{$.Link}}" method="post"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="action" value="note" /> |
||||||
|
<input type="hidden" name="blockee" class="modal-blockee" /> |
||||||
|
<div class="field"> |
||||||
|
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label> |
||||||
|
<input name="note" class="modal-note" /> |
||||||
|
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> |
||||||
|
</div> |
||||||
|
<div class="text right actions"> |
||||||
|
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button> |
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,5 @@ |
|||||||
|
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}} |
||||||
|
<div class="user-setting-content"> |
||||||
|
{{template "shared/user/blocked_users" .}} |
||||||
|
</div> |
||||||
|
{{template "user/settings/layout_footer" .}} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue