diff options
author | KN4CK3R <admin@oldschoolhack.me> | 2024-03-04 09:16:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-04 08:16:03 +0000 |
commit | c337ff0ec70618ef2ead7850f90ab2a8458db192 (patch) | |
tree | cf4618cf7dc258018d5f9ec827b0fda4a9ebd196 /routers/api | |
parent | 8e12ba34bab7e728ac93ccfaecbe91e053ef1c89 (diff) | |
download | gitea-c337ff0ec70618ef2ead7850f90ab2a8458db192.tar.gz gitea-c337ff0ec70618ef2ead7850f90ab2a8458db192.zip |
Add user blocking (#29028)
Fixes #17453
This PR adds the abbility to block a user from a personal account or
organization to restrict how the blocked user can interact with the
blocker. The docs explain what's the consequence of blocking a user.
Screenshots:



---------
Co-authored-by: Lauris BH <lauris@nix.lv>
Diffstat (limited to 'routers/api')
-rw-r--r-- | routers/api/v1/api.go | 20 | ||||
-rw-r--r-- | routers/api/v1/org/block.go | 116 | ||||
-rw-r--r-- | routers/api/v1/org/member.go | 2 | ||||
-rw-r--r-- | routers/api/v1/org/team.go | 13 | ||||
-rw-r--r-- | routers/api/v1/repo/collaborators.go | 24 | ||||
-rw-r--r-- | routers/api/v1/repo/fork.go | 2 | ||||
-rw-r--r-- | routers/api/v1/repo/issue.go | 14 | ||||
-rw-r--r-- | routers/api/v1/repo/issue_comment.go | 14 | ||||
-rw-r--r-- | routers/api/v1/repo/issue_comment_attachment.go | 10 | ||||
-rw-r--r-- | routers/api/v1/repo/issue_reaction.go | 12 | ||||
-rw-r--r-- | routers/api/v1/repo/pull.go | 10 | ||||
-rw-r--r-- | routers/api/v1/repo/transfer.go | 7 | ||||
-rw-r--r-- | routers/api/v1/shared/block.go | 98 | ||||
-rw-r--r-- | routers/api/v1/user/block.go | 96 | ||||
-rw-r--r-- | routers/api/v1/user/follower.go | 11 | ||||
-rw-r--r-- | routers/api/v1/user/star.go | 27 | ||||
-rw-r--r-- | routers/api/v1/user/watch.go | 27 |
17 files changed, 452 insertions, 51 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1587d413f5..c65650c388 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1027,7 +1027,16 @@ func Routes() *web.Route { m.Group("/avatar", func() { m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) m.Delete("", user.DeleteAvatar) - }, reqToken()) + }) + + m.Group("/blocks", func() { + m.Get("", user.ListBlocks) + m.Group("/{username}", func() { + m.Get("", user.CheckUserBlock) + m.Put("", user.BlockUser) + m.Delete("", user.UnblockUser) + }, context.UserAssignmentAPI()) + }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1477,6 +1486,15 @@ func Routes() *web.Route { m.Delete("", org.DeleteAvatar) }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) + + m.Group("/blocks", func() { + m.Get("", org.ListBlocks) + m.Group("/{username}", func() { + m.Get("", org.CheckUserBlock) + m.Put("", org.BlockUser) + m.Delete("", org.UnblockUser) + }) + }, reqToken(), reqOrgOwnership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go new file mode 100644 index 0000000000..69a5222a20 --- /dev/null +++ b/routers/api/v1/org/block.go @@ -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()) +} diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index fb66d4c3f5..9db9ad964b 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) { if ctx.Written() { return } - if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil { + if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil { ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err) } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index b62a386fd7..015af774e3 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -15,6 +15,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" @@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) { if ctx.Written() { return } - if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "AddMember", err) + if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddTeamMember", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddTeamMember", err) + } return } ctx.Status(http.StatusNoContent) @@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) { return } - if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil { + if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil { ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err) return } diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 7d48d71516..4ce14f7d01 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -8,7 +8,6 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -54,16 +53,11 @@ func ListCollaborators(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{ - RepoID: ctx.Repo.Repository.ID, + collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{ + ListOptions: utils.GetListOptions(ctx), + RepoID: ctx.Repo.Repository.ID, }) if err != nil { - ctx.InternalServerError(err) - return - } - - collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) - if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return } @@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) { users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer) } - ctx.SetTotalCountHeader(count) + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, users) } @@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": @@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) { } if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil { - ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddCollaborator", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) + } return } @@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) { return } - if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil { + if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 212cc7a93b..a1e3c9804b 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) { if err != nil { if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) { ctx.Error(http.StatusConflict, "ForkRepository", err) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "ForkRepository", err) } else { ctx.Error(http.StatusInternalServerError, "ForkRepository", err) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 1b2ecd474b..d43711e362 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "fmt" "net/http" "strconv" @@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) { // "$ref": "#/responses/validationError" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.CreateIssueOption) var deadlineUnix timeutil.TimeStamp if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) { @@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) - return + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "NewIssue", err) + } else { + ctx.Error(http.StatusInternalServerError, "NewIssue", err) } - ctx.Error(http.StatusInternalServerError, "NewIssue", err) return } @@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateAssignees", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + } return } } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 6209e960af..21aabadf3d 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.CreateIssueCommentOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { @@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) { comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) if err != nil { - ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "CreateIssueComment", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) + } return } @@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.EditIssueCommentOption) editIssueComment(ctx, *form) } @@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) oldContent := comment.Content comment.Content = form.Body if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateComment", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateComment", err) + } return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index e7436db798..4096cbf07b 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -4,10 +4,12 @@ package repo import ( + "errors" "net/http" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { // "$ref": "#/responses/Attachment" // "400": // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/error" // "423": @@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { } if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil { - ctx.ServerError("UpdateComment", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateComment", err) + } else { + ctx.ServerError("UpdateComment", err) + } return } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 799c687812..3ff3d19f13 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -8,11 +8,13 @@ import ( "net/http" issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" 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/context" "code.gitea.io/gitea/services/convert" + issue_service "code.gitea.io/gitea/services/issue" ) // GetIssueCommentReactions list reactions of a comment from an issue @@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp if isCreateType { // PostIssueCommentReaction part - reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) + reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.Error(http.StatusForbidden, err.Error(), err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ @@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i if isCreateType { // PostIssueReaction part - reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction) + reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction) if err != nil { - if issues_model.IsErrForbiddenIssueReaction(err) { + if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { ctx.Error(http.StatusForbidden, err.Error(), err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ @@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i Created: reaction.CreatedUnix.AsTime(), }) } else { - ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err) + ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err) } return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 8f9848f71d..4cb94b11a2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/PullRequest" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "409": @@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) { if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) - return + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) } - ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) return } @@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) { if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "UpdateAssignees", err) } else { ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) } diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 4f05c0df51..776b336761 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" @@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) { return } - ctx.InternalServerError(err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.InternalServerError(err) + } return } diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go new file mode 100644 index 0000000000..a1e65625ed --- /dev/null +++ b/routers/api/v1/shared/block.go @@ -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) +} diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go new file mode 100644 index 0000000000..7231e9add7 --- /dev/null +++ b/routers/api/v1/user/block.go @@ -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) +} diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 398c6b2567..6abb70de19 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" @@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "FollowUser", err) + if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "FollowUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "FollowUser", err) + } return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index e624884db3..ad9ed9548d 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -5,10 +5,9 @@ package user import ( - std_context "context" + "errors" "net/http" - "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -20,8 +19,12 @@ import ( // getStarredRepos returns the repos that the user with the specified userID has // starred -func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { - starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) +func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) { + starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{ + ListOptions: utils.GetListOptions(ctx), + StarrerID: user.ID, + IncludePrivate: private, + }) if err != nil { return nil, err } @@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" private := ctx.ContextUser.ID == ctx.Doer.ID - repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + repos, err := getStarredRepos(ctx, ctx.ContextUser, private) if err != nil { ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) return @@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + repos, err := getStarredRepos(ctx, ctx.Doer, true) if err != nil { ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) } @@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "StarRepo", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + } return } ctx.Status(http.StatusNoContent) @@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 706f4cc66b..2cc23ae476 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -4,10 +4,9 @@ package user import ( - std_context "context" + "errors" "net/http" - "code.gitea.io/gitea/models/db" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -18,8 +17,12 @@ import ( ) // getWatchedRepos returns the repos that the user with the specified userID is watching -func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) { - watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions) +func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) { + watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{ + ListOptions: utils.GetListOptions(ctx), + WatcherID: user.ID, + IncludePrivate: private, + }) if err != nil { return nil, 0, err } @@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" private := ctx.ContextUser.ID == ctx.Doer.ID - repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } @@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + repos, total, err := getWatchedRepos(ctx, ctx.Doer, true) if err != nil { ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) } @@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/WatchInfo" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "BlockedUser", err) + } else { + ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + } return } ctx.JSON(http.StatusOK, api.WatchInfo{ @@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err) return |