joinBoard API (#2660)

This commit is contained in:
Chen-I Lim 2022-03-29 01:14:33 -07:00 committed by GitHub
parent a9d4a7457c
commit efb8caf88f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 489 additions and 241 deletions

View file

@ -99,6 +99,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
apiv1.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
// Sharing APIs
apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
@ -3214,6 +3215,98 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
auditRec.Success()
}
func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/join joinBoard
//
// Become a member of a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// '404':
// description: board not found
// '503':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
if userID == "" {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
boardID := mux.Vars(r)["boardID"]
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return
}
if board.Type != model.BoardTypeOpen {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
return
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
return
}
// currently all memberships are created as editors by default
// TODO: Support different public roles
newBoardMember := &model.BoardMember{
UserID: userID,
BoardID: boardID,
SchemeEditor: true,
}
auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", userID)
member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("AddMember",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", userID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
//

View file

@ -176,6 +176,10 @@ func (c *Client) GetBoardRoute(boardID string) string {
return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID)
}
func (c *Client) GetJoinBoardRoute(boardID string) string {
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
}
func (c *Client) GetBlocksRoute(boardID string) string {
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
}
@ -512,6 +516,16 @@ func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) JoinBoard(boardID string) (*model.BoardMember, *Response) {
r, err := c.DoAPIPost(c.GetJoinBoardRoute(boardID), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
if err != nil {

View file

@ -1,6 +1,7 @@
package integrationtests
import (
"encoding/json"
"testing"
"github.com/mattermost/focalboard/server/client"
@ -1268,3 +1269,78 @@ func TestDeleteMember(t *testing.T) {
require.True(t, members[0].SchemeAdmin)
})
}
func TestJoinBoard(t *testing.T) {
t.Run("create and join public board", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
me := th.GetUser1()
title := "Public board"
teamID := testTeamID
newBoard := &model.Board{
Title: title,
Type: model.BoardTypeOpen,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, board)
require.NotNil(t, board.ID)
require.Equal(t, title, board.Title)
require.Equal(t, model.BoardTypeOpen, board.Type)
require.Equal(t, teamID, board.TeamID)
require.Equal(t, me.ID, board.CreatedBy)
require.Equal(t, me.ID, board.ModifiedBy)
member, resp := th.Client2.JoinBoard(board.ID)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, member)
require.Equal(t, board.ID, member.BoardID)
require.Equal(t, th.GetUser2().ID, member.UserID)
s, _ := json.MarshalIndent(member, "", "\t")
t.Log(string(s))
})
t.Run("create and join private board (should not succeed)", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
me := th.GetUser1()
title := "Private board"
teamID := testTeamID
newBoard := &model.Board{
Title: title,
Type: model.BoardTypePrivate,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, board)
require.NotNil(t, board.ID)
require.Equal(t, title, board.Title)
require.Equal(t, model.BoardTypePrivate, board.Type)
require.Equal(t, teamID, board.TeamID)
require.Equal(t, me.ID, board.CreatedBy)
require.Equal(t, me.ID, board.ModifiedBy)
member, resp := th.Client2.JoinBoard(board.ID)
th.CheckForbidden(resp)
require.Nil(t, member)
})
t.Run("join invalid board", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
member, resp := th.Client2.JoinBoard("nonexistent-board-ID")
th.CheckNotFound(resp)
require.Nil(t, member)
})
}

View file

@ -1 +1 @@
5.3.0
5.4.0

View file

@ -1840,6 +1840,25 @@ paths:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/users/me/memberships:
get:
description: Returns the currently users board memberships
operationId: getMyMemberships
produces:
- application/json
responses:
"200":
description: success
schema:
items:
$ref: '#/definitions/BoardMember'
type: array
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete:
post:
description: Undeletes a block
@ -1900,6 +1919,33 @@ paths:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/boards/{boardID}/join:
post:
description: Become a member of a board
operationId: joinBoard
parameters:
- description: Board ID
in: path
name: boardID
required: true
type: string
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/BoardMember'
"404":
description: board not found
"503":
description: access denied
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/boards/{boardID}/members:
post:
description: Adds a new member to a board

482
webapp/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -435,6 +435,21 @@ class OctoClient {
return this.getJson<BoardMember>(response, {} as BoardMember)
}
async joinBoard(boardId: string): Promise<BoardMember|undefined> {
Utils.log(`joinBoard: board ${boardId}`)
const response = await fetch(this.getBaseURL() + `/api/v1/boards/${boardId}/join`, {
method: 'POST',
headers: this.headers()
})
if (response.status !== 200) {
return undefined
}
return this.getJson<BoardMember>(response, {} as BoardMember)
}
async updateBoardMember(member: BoardMember): Promise<Response> {
Utils.log(`udpateBoardMember: user ${member.userId} and board ${member.boardId}`)

View file

@ -84,7 +84,7 @@ const BoardPage = (props: Props): JSX.Element => {
// and fetch its data
const result: any = await dispatch(loadBoardData(boardId))
if (result.payload.blocks.length === 0 && userId) {
const member = await octoClient.createBoardMember({userId, boardId})
const member = await octoClient.joinBoard(boardId)
if (!member) {
UserSettings.setLastBoardID(boardTeamId, null)
UserSettings.setLastViewId(boardId, null)