|
|
|
@@ -87,14 +87,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
|
|
|
|
|
|
|
|
|
// Import&Export APIs
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST")
|
|
|
|
|
|
|
|
|
|
// Member APIs
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET")
|
|
|
|
|
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
|
|
|
|
@@ -363,23 +358,6 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
auditRec.Success()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *audit.Record) {
|
|
|
|
|
userID := getUserID(r)
|
|
|
|
|
if userID == model.SingleUser {
|
|
|
|
|
userID = ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := utils.GetMillis()
|
|
|
|
|
for i := range blocks {
|
|
|
|
|
blocks[i].ModifiedBy = userID
|
|
|
|
|
blocks[i].UpdateAt = now
|
|
|
|
|
|
|
|
|
|
if auditRec != nil {
|
|
|
|
|
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
requestBody, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -1219,287 +1197,6 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
auditRec.Success()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// swagger:operation GET /api/v1/boards/{boardID}/blocks/{blockID}/subtree getSubTree
|
|
|
|
|
//
|
|
|
|
|
// Returns the blocks of a subtree
|
|
|
|
|
//
|
|
|
|
|
// ---
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: boardID
|
|
|
|
|
// in: path
|
|
|
|
|
// description: Board ID
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: blockID
|
|
|
|
|
// in: path
|
|
|
|
|
// description: The ID of the root block of the subtree
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: l
|
|
|
|
|
// in: query
|
|
|
|
|
// description: The number of levels to return. 2 or 3. Defaults to 2.
|
|
|
|
|
// required: false
|
|
|
|
|
// type: integer
|
|
|
|
|
// minimum: 2
|
|
|
|
|
// maximum: 3
|
|
|
|
|
// security:
|
|
|
|
|
// - BearerAuth: []
|
|
|
|
|
// responses:
|
|
|
|
|
// '200':
|
|
|
|
|
// description: success
|
|
|
|
|
// schema:
|
|
|
|
|
// type: array
|
|
|
|
|
// items:
|
|
|
|
|
// "$ref": "#/definitions/Block"
|
|
|
|
|
// default:
|
|
|
|
|
// description: internal error
|
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/ErrorResponse"
|
|
|
|
|
|
|
|
|
|
userID := getUserID(r)
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
boardID := vars["boardID"]
|
|
|
|
|
blockID := vars["blockID"]
|
|
|
|
|
|
|
|
|
|
if !a.hasValidReadTokenForBoard(r, boardID) && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := r.URL.Query()
|
|
|
|
|
levels, err := strconv.ParseInt(query.Get("l"), 10, 32)
|
|
|
|
|
if err != nil {
|
|
|
|
|
levels = 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if levels != 2 && levels != 3 {
|
|
|
|
|
a.logger.Error("Invalid levels", mlog.Int64("levels", levels))
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid levels", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auditRec := a.makeAuditRecord(r, "getSubTree", audit.Fail)
|
|
|
|
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
|
|
|
|
auditRec.AddMeta("boardID", boardID)
|
|
|
|
|
auditRec.AddMeta("blockID", blockID)
|
|
|
|
|
|
|
|
|
|
blocks, err := a.app.GetSubTree(boardID, blockID, int(levels), model.QuerySubtreeOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.logger.Debug("GetSubTree",
|
|
|
|
|
mlog.Int64("levels", levels),
|
|
|
|
|
mlog.String("boardID", boardID),
|
|
|
|
|
mlog.String("blockID", blockID),
|
|
|
|
|
mlog.Int("block_count", len(blocks)),
|
|
|
|
|
)
|
|
|
|
|
json, err := json.Marshal(blocks)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonBytesResponse(w, http.StatusOK, json)
|
|
|
|
|
|
|
|
|
|
auditRec.AddMeta("blockCount", len(blocks))
|
|
|
|
|
auditRec.Success()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *API) handleExport(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// swagger:operation GET /api/v1/boards/{boardID}/blocks/export exportBlocks
|
|
|
|
|
//
|
|
|
|
|
// Returns all blocks 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:
|
|
|
|
|
// type: array
|
|
|
|
|
// items:
|
|
|
|
|
// "$ref": "#/definitions/Block"
|
|
|
|
|
// default:
|
|
|
|
|
// description: internal error
|
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/ErrorResponse"
|
|
|
|
|
|
|
|
|
|
userID := getUserID(r)
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
boardID := vars["boardID"]
|
|
|
|
|
|
|
|
|
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := r.URL.Query()
|
|
|
|
|
rootID := query.Get("root_id")
|
|
|
|
|
|
|
|
|
|
auditRec := a.makeAuditRecord(r, "export", audit.Fail)
|
|
|
|
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
|
|
|
|
auditRec.AddMeta("boardID", boardID)
|
|
|
|
|
auditRec.AddMeta("rootID", rootID)
|
|
|
|
|
|
|
|
|
|
var blocks []model.Block
|
|
|
|
|
var err error
|
|
|
|
|
if rootID == "" {
|
|
|
|
|
blocks, err = a.app.GetBlocksForBoard(boardID)
|
|
|
|
|
} else {
|
|
|
|
|
blocks, err = a.app.GetBlocksWithBoardID(boardID)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.logger.Debug("raw blocks", mlog.Int("block_count", len(blocks)))
|
|
|
|
|
auditRec.AddMeta("rawCount", len(blocks))
|
|
|
|
|
|
|
|
|
|
blocks = filterOrphanBlocks(blocks)
|
|
|
|
|
|
|
|
|
|
a.logger.Debug("EXPORT filtered blocks", mlog.Int("block_count", len(blocks)))
|
|
|
|
|
auditRec.AddMeta("filteredCount", len(blocks))
|
|
|
|
|
|
|
|
|
|
json, err := json.Marshal(blocks)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonBytesResponse(w, http.StatusOK, json)
|
|
|
|
|
|
|
|
|
|
auditRec.Success()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func filterOrphanBlocks(blocks []model.Block) (ret []model.Block) {
|
|
|
|
|
queue := make([]model.Block, 0)
|
|
|
|
|
childrenOfBlockWithID := make(map[string]*[]model.Block)
|
|
|
|
|
|
|
|
|
|
// Build the trees from nodes
|
|
|
|
|
for _, block := range blocks {
|
|
|
|
|
if len(block.ParentID) == 0 {
|
|
|
|
|
// Queue root blocks to process first
|
|
|
|
|
queue = append(queue, block)
|
|
|
|
|
} else {
|
|
|
|
|
siblings := childrenOfBlockWithID[block.ParentID]
|
|
|
|
|
if siblings != nil {
|
|
|
|
|
*siblings = append(*siblings, block)
|
|
|
|
|
} else {
|
|
|
|
|
siblings := []model.Block{block}
|
|
|
|
|
childrenOfBlockWithID[block.ParentID] = &siblings
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map the trees to an array, which skips orphaned nodes
|
|
|
|
|
blocks = make([]model.Block, 0)
|
|
|
|
|
for len(queue) > 0 {
|
|
|
|
|
block := queue[0]
|
|
|
|
|
queue = queue[1:] // dequeue
|
|
|
|
|
blocks = append(blocks, block)
|
|
|
|
|
children := childrenOfBlockWithID[block.ID]
|
|
|
|
|
if children != nil {
|
|
|
|
|
queue = append(queue, *children...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return blocks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// swagger:operation POST /api/v1/boards/{boardID}/blocks/import importBlocks
|
|
|
|
|
//
|
|
|
|
|
// Import blocks on a given board
|
|
|
|
|
//
|
|
|
|
|
// ---
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: boardID
|
|
|
|
|
// in: path
|
|
|
|
|
// description: Board ID
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: Body
|
|
|
|
|
// in: body
|
|
|
|
|
// description: array of blocks to import
|
|
|
|
|
// required: true
|
|
|
|
|
// schema:
|
|
|
|
|
// type: array
|
|
|
|
|
// items:
|
|
|
|
|
// "$ref": "#/definitions/Block"
|
|
|
|
|
// security:
|
|
|
|
|
// - BearerAuth: []
|
|
|
|
|
// responses:
|
|
|
|
|
// '200':
|
|
|
|
|
// description: success
|
|
|
|
|
// default:
|
|
|
|
|
// description: internal error
|
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/ErrorResponse"
|
|
|
|
|
|
|
|
|
|
userID := getUserID(r)
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
boardID := vars["boardID"]
|
|
|
|
|
|
|
|
|
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestBody, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var blocks []model.Block
|
|
|
|
|
|
|
|
|
|
err = json.Unmarshal(requestBody, &blocks)
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auditRec := a.makeAuditRecord(r, "import", audit.Fail)
|
|
|
|
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
|
|
|
|
auditRec.AddMeta("boardID", boardID)
|
|
|
|
|
|
|
|
|
|
// all blocks should now be part of the board that they're being
|
|
|
|
|
// imported onto
|
|
|
|
|
for i := range blocks {
|
|
|
|
|
blocks[i].BoardID = boardID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stampModificationMetadata(r, blocks, auditRec)
|
|
|
|
|
|
|
|
|
|
if _, err = a.app.InsertBlocks(model.GenerateBlockIDs(blocks, a.logger), userID, false); err != nil {
|
|
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonStringResponse(w, http.StatusOK, "{}")
|
|
|
|
|
|
|
|
|
|
a.logger.Debug("IMPORT BlockIDs", mlog.Int("block_count", len(blocks)))
|
|
|
|
|
auditRec.AddMeta("blockCount", len(blocks))
|
|
|
|
|
auditRec.Success()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sharing
|
|
|
|
|
|
|
|
|
|
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|