focalboard/server/api/api.go

1707 lines
45 KiB
Go
Raw Normal View History

package api
2020-10-16 11:41:56 +02:00
import (
"encoding/json"
"fmt"
"io"
2020-10-16 11:41:56 +02:00
"io/ioutil"
"net/http"
"path/filepath"
"runtime/debug"
2020-11-12 19:16:59 +01:00
"strconv"
2020-10-16 11:41:56 +02:00
"strings"
2020-12-07 20:40:16 +01:00
"time"
2020-10-16 11:41:56 +02:00
"github.com/gorilla/mux"
2021-01-26 23:13:46 +01:00
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
2021-03-26 19:01:54 +01:00
"github.com/mattermost/focalboard/server/services/store"
2021-01-26 23:13:46 +01:00
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
2020-10-16 11:41:56 +02:00
)
const (
HeaderRequestedWith = "X-Requested-With"
HeaderRequestedWithXML = "XMLHttpRequest"
UploadFormFileKey = "file"
)
2021-03-26 19:01:54 +01:00
const (
ErrorNoWorkspaceCode = 1000
ErrorNoWorkspaceMessage = "No workspace"
2021-03-26 19:01:54 +01:00
)
type PermissionError struct {
msg string
}
func (pe PermissionError) Error() string {
return pe.msg
}
2020-10-16 11:41:56 +02:00
// ----------------------------------------------------------------------------------------------------
// REST APIs
2020-10-16 16:21:42 +02:00
type API struct {
app *app.App
authService string
singleUserToken string
MattermostAuth bool
logger *mlog.Logger
audit *audit.Audit
2020-10-16 16:21:42 +02:00
}
func NewAPI(app *app.App, singleUserToken string, authService string, logger *mlog.Logger, audit *audit.Audit) *API {
2021-02-09 21:27:34 +01:00
return &API{
app: app,
2021-02-09 21:27:34 +01:00
singleUserToken: singleUserToken,
2021-03-26 19:01:54 +01:00
authService: authService,
logger: logger,
audit: audit,
2021-02-09 21:27:34 +01:00
}
2020-10-16 16:21:42 +02:00
}
2020-10-16 11:41:56 +02:00
func (a *API) RegisterRoutes(r *mux.Router) {
2021-02-05 19:28:52 +01:00
apiv1 := r.PathPrefix("/api/v1").Subrouter()
apiv1.Use(a.panicHandler)
2021-02-05 19:28:52 +01:00
apiv1.Use(a.requireCSRFToken)
2020-12-07 20:40:16 +01:00
2021-03-26 19:01:54 +01:00
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH")
2021-03-26 19:01:54 +01:00
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
2021-03-26 19:01:54 +01:00
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/sharing/{rootID}", a.sessionRequired(a.handleGetSharing)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}", a.sessionRequired(a.handleGetWorkspace)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/users", a.sessionRequired(a.getWorkspaceUsers)).Methods("GET")
2020-10-16 11:41:56 +02:00
2021-03-26 19:01:54 +01:00
// User APIs
2021-02-05 19:28:52 +01:00
apiv1.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
apiv1.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
apiv1.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST")
2020-11-06 16:46:35 +01:00
2021-02-05 19:28:52 +01:00
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
2021-02-05 19:28:52 +01:00
apiv1.HandleFunc("/register", a.handleRegister).Methods("POST")
apiv1.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET")
2020-10-16 11:41:56 +02:00
apiv1.HandleFunc("/workspaces/{workspaceID}/{rootID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
2021-02-05 19:45:28 +01:00
apiv1.HandleFunc("/workspaces", a.sessionRequired(a.handleGetUserWorkspaces)).Methods("GET")
2021-02-05 19:45:28 +01:00
// Get Files API
2021-02-05 19:28:52 +01:00
2021-02-05 20:22:56 +01:00
files := r.PathPrefix("/files").Subrouter()
files.HandleFunc("/workspaces/{workspaceID}/{rootID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
// Subscriptions
apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE")
apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET")
// archives
apiv1.HandleFunc("/workspaces/{workspaceID}/archive/export", a.sessionRequired(a.handleArchiveExport)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
2020-10-16 11:41:56 +02:00
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {
2021-01-22 23:14:12 +01:00
r.HandleFunc("/api/v1/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
}
func (a *API) panicHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if p := recover(); p != nil {
a.logger.Error("Http handler panic",
mlog.Any("panic", p),
mlog.String("stack", string(debug.Stack())),
mlog.String("uri", r.URL.Path),
)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", nil)
}
}()
next.ServeHTTP(w, r)
})
}
2021-02-05 19:28:52 +01:00
func (a *API) requireCSRFToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !a.checkCSRFToken(r) {
a.logger.Error("checkCSRFToken FAILED")
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "checkCSRFToken FAILED", nil)
return
}
2021-02-05 19:28:52 +01:00
next.ServeHTTP(w, r)
})
}
func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) {
clientConfig := a.app.GetClientConfig()
configData, err := json.Marshal(clientConfig)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, configData)
}
func (a *API) checkCSRFToken(r *http.Request) bool {
token := r.Header.Get(HeaderRequestedWith)
return token == HeaderRequestedWithXML
}
func (a *API) hasValidReadTokenForBlock(r *http.Request, container store.Container, blockID string) bool {
query := r.URL.Query()
readToken := query.Get("read_token")
if len(readToken) < 1 {
return false
}
isValid, err := a.app.IsValidReadToken(container, blockID, readToken)
if err != nil {
a.logger.Error("IsValidReadToken ERROR", mlog.Err(err))
return false
}
return isValid
}
2021-03-29 19:41:27 +02:00
func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID string) (*store.Container, error) {
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
if a.MattermostAuth {
// Workspace auth
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
2021-03-26 19:01:54 +01:00
container := store.Container{
WorkspaceID: workspaceID,
2021-03-26 19:01:54 +01:00
}
if workspaceID == "0" {
return &container, nil
}
// Has session and access to workspace
if session != nil && a.app.DoesUserHaveWorkspaceAccess(session.UserID, container.WorkspaceID) {
return &container, nil
}
// No session, but has valid read token (read-only mode)
if len(blockID) > 0 &&
a.hasValidReadTokenForBlock(r, container, blockID) &&
a.app.GetClientConfig().EnablePublicSharedBoards {
return &container, nil
}
return nil, PermissionError{"access denied to workspace"}
2021-03-26 19:01:54 +01:00
}
// Native auth: always use root workspace
2021-03-26 19:01:54 +01:00
container := store.Container{
WorkspaceID: "0",
2021-03-26 19:01:54 +01:00
}
// Has session
if session != nil {
return &container, nil
}
2021-03-29 19:41:27 +02:00
// No session, but has valid read token (read-only mode)
if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) {
return &container, nil
2021-03-29 19:41:27 +02:00
}
return nil, PermissionError{"access denied to workspace"}
2021-03-26 19:01:54 +01:00
}
2021-03-29 19:41:27 +02:00
func (a *API) getContainer(r *http.Request) (*store.Container, error) {
return a.getContainerAllowingReadTokenForBlock(r, "")
}
2020-10-16 11:41:56 +02:00
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks getBlocks
2021-02-17 20:29:20 +01:00
//
// Returns blocks
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: parent_id
// in: query
// description: ID of parent block, omit to specify all blocks
// required: false
// type: string
// - name: type
// in: query
// description: Type of blocks to return, omit to specify all types
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2020-10-16 11:41:56 +02:00
query := r.URL.Query()
parentID := query.Get("parent_id")
blockType := query.Get("type")
Migrate webapp global state to redux (#737) * Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
2021-08-02 17:46:00 +02:00
all := query.Get("all")
2021-10-22 18:13:31 +02:00
blockID := query.Get("block_id")
container, err := a.getContainerAllowingReadTokenForBlock(r, blockID)
2021-03-26 19:01:54 +01:00
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
2020-10-16 11:41:56 +02:00
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("parentID", parentID)
auditRec.AddMeta("blockType", blockType)
Migrate webapp global state to redux (#737) * Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
2021-08-02 17:46:00 +02:00
auditRec.AddMeta("all", all)
2021-10-22 18:13:31 +02:00
auditRec.AddMeta("blockID", blockID)
Migrate webapp global state to redux (#737) * Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
2021-08-02 17:46:00 +02:00
var blocks []model.Block
2021-10-22 18:13:31 +02:00
var block *model.Block
switch {
case all != "":
Migrate webapp global state to redux (#737) * Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
2021-08-02 17:46:00 +02:00
blocks, err = a.app.GetAllBlocks(*container)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
2021-10-22 18:13:31 +02:00
case blockID != "":
block, err = a.app.GetBlockByID(*container, blockID)
2021-10-22 18:13:31 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if block != nil {
blocks = append(blocks, *block)
}
default:
Migrate webapp global state to redux (#737) * Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
2021-08-02 17:46:00 +02:00
blocks, err = a.app.GetBlocks(*container, parentID, blockType)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
2020-10-16 11:41:56 +02:00
}
a.logger.Debug("GetBlocks",
mlog.String("parentID", parentID),
mlog.String("blockType", blockType),
2021-10-22 18:13:31 +02:00
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
2020-10-16 11:41:56 +02:00
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 11:41:56 +02:00
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
2020-10-16 11:41:56 +02:00
}
func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *audit.Record) {
2021-01-12 03:53:08 +01:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-12 03:53:08 +01:00
userID := session.UserID
if userID == model.SingleUser {
2021-01-12 03:53:08 +01:00
userID = ""
}
now := utils.GetMillis()
2021-01-12 03:53:08 +01:00
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])
}
2021-01-12 03:53:08 +01:00
}
}
2020-10-16 11:41:56 +02:00
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
2021-02-17 20:29:20 +01:00
//
// Insert blocks. The specified IDs will only be used to link
// blocks with existing ones, the rest will be replaced by server
// generated IDs
2021-02-17 20:29:20 +01:00
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: Body
// in: body
// description: array of blocks to insert or update
// required: true
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// items:
// $ref: '#/definitions/Block'
// type: array
2021-02-17 20:29:20 +01:00
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-03-26 19:01:54 +01:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
2020-10-16 11:41:56 +02:00
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 11:41:56 +02:00
return
}
var blocks []model.Block
err = json.Unmarshal(requestBody, &blocks)
2020-10-16 11:41:56 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 11:41:56 +02:00
return
}
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
2021-02-17 20:29:20 +01:00
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
2020-10-16 11:41:56 +02:00
return
}
2020-10-16 11:41:56 +02:00
if block.CreateAt < 1 {
2021-02-17 20:29:20 +01:00
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
2020-10-16 11:41:56 +02:00
return
}
2020-10-16 11:41:56 +02:00
if block.UpdateAt < 1 {
2021-02-17 20:29:20 +01:00
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
2020-10-16 11:41:56 +02:00
return
}
}
blocks = model.GenerateBlockIDs(blocks, a.logger)
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
stampModificationMetadata(r, blocks, auditRec)
2021-01-12 03:53:08 +01:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
// this query param exists when creating template from board, or board from template
sourceBoardID := r.URL.Query().Get("sourceBoardID")
if sourceBoardID != "" {
if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, container.WorkspaceID, blocks); updateFileIDsErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", updateFileIDsErr)
return
}
}
newBlocks, err := a.app.InsertBlocks(*container, blocks, session.UserID, true)
2020-10-16 16:21:42 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 16:21:42 +02:00
return
}
2020-10-16 11:41:56 +02:00
a.logger.Debug("POST Blocks", mlog.Int("block_count", len(blocks)))
json, err := json.Marshal(newBlocks)
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()
2020-10-16 11:41:56 +02:00
}
2020-12-07 20:40:16 +01:00
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
2021-02-17 20:29:20 +01:00
// swagger:operation GET /api/v1/users/{userID} getUser
//
// Returns a user
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2020-12-07 20:40:16 +01:00
vars := mux.Vars(r)
userID := vars["userID"]
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("userID", userID)
user, err := a.app.GetUser(userID)
2020-12-07 20:40:16 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 20:40:16 +01:00
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 20:40:16 +01:00
return
}
2021-02-17 20:29:20 +01:00
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.Success()
2020-12-07 20:40:16 +01:00
}
func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
2021-02-17 20:29:20 +01:00
// swagger:operation GET /api/v1/users/me getMe
//
// Returns the currently logged-in user
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2020-12-07 20:40:16 +01:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2020-12-07 20:40:16 +01:00
var user *model.User
var err error
auditRec := a.makeAuditRecord(r, "getMe", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
if session.UserID == model.SingleUser {
now := utils.GetMillis()
2020-12-07 20:40:16 +01:00
user = &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
2020-12-07 20:40:16 +01:00
CreateAt: now,
UpdateAt: now,
}
} else {
user, err = a.app.GetUser(session.UserID)
2020-12-07 20:40:16 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 20:40:16 +01:00
return
}
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 20:40:16 +01:00
return
}
2021-02-17 20:29:20 +01:00
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID)
auditRec.Success()
2020-12-07 20:40:16 +01:00
}
2020-10-16 11:41:56 +02:00
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation DELETE /api/v1/workspaces/{workspaceID}/blocks/{blockID} deleteBlock
2021-02-17 20:29:20 +01:00
//
// Deletes a block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: blockID
// in: path
// description: ID of block to delete
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-01-12 20:16:25 +01:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-12 20:16:25 +01:00
userID := session.UserID
2020-10-16 11:41:56 +02:00
vars := mux.Vars(r)
blockID := vars["blockID"]
2021-03-26 19:01:54 +01:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
err = a.app.DeleteBlock(*container, blockID, userID)
2020-10-16 16:21:42 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 16:21:42 +02:00
return
2020-10-16 11:41:56 +02:00
}
a.logger.Debug("DELETE Block", mlog.String("blockID", blockID))
2020-10-16 11:41:56 +02:00
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
2020-10-16 11:41:56 +02:00
}
func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /api/v1/workspaces/{workspaceID}/blocks/{blockID} patchBlock
//
// Partially updates a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to patch
// required: true
// type: string
// - name: Body
// in: body
// description: block patch to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
blockID := vars["blockID"]
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var patch *model.BlockPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
err = a.app.PatchBlock(*container, blockID, patch, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("PATCH Block", mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /api/v1/workspaces/{workspaceID}/blocks/ patchBlocks
//
// Partially updates batch of blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: Body
// in: body
// description: block Ids and block patches to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatchBatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var patches *model.BlockPatchBatch
err = json.Unmarshal(requestBody, &patches)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
for i := range patches.BlockIDs {
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i])
}
err = a.app.PatchBlocks(*container, patches, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("PATCH Blocks", mlog.String("patches", strconv.Itoa(len(patches.BlockIDs))))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
2020-10-16 11:41:56 +02:00
func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks/{blockID}/subtree getSubTree
2021-02-17 20:29:20 +01:00
//
// Returns the blocks of a subtree
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - 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"
2020-10-16 11:41:56 +02:00
vars := mux.Vars(r)
blockID := vars["blockID"]
2021-03-29 19:41:27 +02:00
container, err := a.getContainerAllowingReadTokenForBlock(r, blockID)
2021-03-26 19:01:54 +01:00
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
2020-11-12 19:16:59 +01:00
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)
2020-11-12 19:16:59 +01:00
return
}
auditRec := a.makeAuditRecord(r, "getSubTree", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("blockID", blockID)
blocks, err := a.app.GetSubTree(*container, blockID, int(levels))
2020-10-16 16:21:42 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 16:21:42 +02:00
return
}
2020-10-16 11:41:56 +02:00
a.logger.Debug("GetSubTree",
mlog.Int64("levels", levels),
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
2020-10-16 11:41:56 +02:00
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 11:41:56 +02:00
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
2020-10-16 11:41:56 +02:00
}
2021-01-13 00:35:30 +01:00
// Sharing
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/sharing/{rootID} getSharing
2021-02-17 20:29:20 +01:00
//
// Returns sharing information for a root block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Sharing"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-01-13 00:35:30 +01:00
vars := mux.Vars(r)
rootID := vars["rootID"]
2021-03-26 19:01:54 +01:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("rootID", rootID)
sharing, err := a.app.GetSharing(*container, rootID)
2021-01-13 00:35:30 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 00:35:30 +01:00
return
}
sharingData, err := json.Marshal(sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 00:35:30 +01:00
return
}
2021-02-17 20:29:20 +01:00
jsonBytesResponse(w, http.StatusOK, sharingData)
if sharing == nil {
sharing = &model.Sharing{}
}
a.logger.Debug("GET sharing",
mlog.String("rootID", rootID),
mlog.String("shareID", sharing.ID),
mlog.Bool("enabled", sharing.Enabled),
)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
auditRec.Success()
2021-01-13 00:35:30 +01:00
}
func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/sharing/{rootID} postSharing
2021-02-17 20:29:20 +01:00
//
// Sets sharing information for a root block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 19:01:54 +01:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
// - name: Body
// in: body
// description: sharing information for a root block
// required: true
// schema:
// "$ref": "#/definitions/Sharing"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-03-26 19:01:54 +01:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 19:01:54 +01:00
return
}
2021-01-13 00:35:30 +01:00
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 00:35:30 +01:00
return
}
var sharing model.Sharing
err = json.Unmarshal(requestBody, &sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 00:35:30 +01:00
return
}
auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
2021-01-13 00:35:30 +01:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-13 00:35:30 +01:00
userID := session.UserID
if userID == model.SingleUser {
2021-01-13 00:35:30 +01:00
userID = ""
}
if !a.app.GetClientConfig().EnablePublicSharedBoards {
a.logger.Info(
"Attempt to turn on sharing for board via API failed, sharing off in configuration.",
mlog.String("boardID", sharing.ID),
mlog.String("userID", userID))
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "Turning on sharing for board failed, see log for details.", nil)
return
}
2021-01-13 00:35:30 +01:00
sharing.ModifiedBy = userID
err = a.app.UpsertSharing(*container, sharing)
2021-01-13 00:35:30 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 00:35:30 +01:00
return
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID))
auditRec.Success()
2021-01-13 00:35:30 +01:00
}
2021-01-14 01:56:01 +01:00
// Workspace
func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation GET /api/v1/workspaces/{workspaceID} getWorkspace
2021-02-17 20:29:20 +01:00
//
// Returns information of the root workspace
//
// ---
// produces:
// - application/json
2021-03-26 19:01:54 +01:00
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Workspace"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-03-26 19:01:54 +01:00
var workspace *model.Workspace
var err error
if a.MattermostAuth {
2021-03-26 19:01:54 +01:00
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
if !a.app.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "user does not have workspace access", nil)
2021-03-26 19:01:54 +01:00
return
}
2021-03-31 00:25:16 +02:00
workspace, err = a.app.GetWorkspace(workspaceID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
2021-03-31 00:25:16 +02:00
if workspace == nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid workspace", nil)
2021-03-31 00:25:16 +02:00
return
2021-03-26 19:01:54 +01:00
}
} else {
workspace, err = a.app.GetRootWorkspace()
2021-03-26 19:01:54 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-03-26 19:01:54 +01:00
return
}
2021-01-14 01:56:01 +01:00
}
auditRec := a.makeAuditRecord(r, "getWorkspace", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("resultWorkspaceID", workspace.ID)
2021-01-14 01:56:01 +01:00
workspaceData, err := json.Marshal(workspace)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 01:56:01 +01:00
return
}
2021-02-17 20:29:20 +01:00
jsonBytesResponse(w, http.StatusOK, workspaceData)
auditRec.Success()
2021-01-14 01:56:01 +01:00
}
func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/regenerate_signup_token regenerateSignupToken
2021-02-17 20:29:20 +01:00
//
// Regenerates the signup token for the root workspace
//
// ---
// produces:
// - application/json
2021-03-26 19:01:54 +01:00
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
workspace, err := a.app.GetRootWorkspace()
2021-01-14 01:56:01 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 01:56:01 +01:00
return
}
auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
workspace.SignupToken = utils.NewID(utils.IDTypeToken)
2021-01-14 01:56:01 +01:00
err = a.app.UpsertWorkspaceSignupToken(*workspace)
2021-01-14 01:56:01 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 01:56:01 +01:00
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
2021-01-14 01:56:01 +01:00
}
2020-10-16 11:41:56 +02:00
// File upload
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /workspaces/{workspaceID}/{rootID}/{fileID} getFile
2021-02-17 20:29:20 +01:00
//
// Returns the contents of an uploaded file
//
// ---
// produces:
// - application/json
// - image/jpg
// - image/png
// - image/gif
2021-02-17 20:29:20 +01:00
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: fileID
// in: path
// description: ID of the file
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2020-10-16 11:41:56 +02:00
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
2020-10-16 11:41:56 +02:00
filename := vars["filename"]
// Caller must have access to the root block's container
_, err := a.getContainerAllowingReadTokenForBlock(r, rootID)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("rootID", rootID)
auditRec.AddMeta("filename", filename)
2020-10-16 11:41:56 +02:00
contentType := "image/jpg"
2020-10-16 11:41:56 +02:00
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == "png" {
contentType = "image/png"
}
if fileExtension == "gif" {
contentType = "image/gif"
}
2020-10-16 11:41:56 +02:00
w.Header().Set("Content-Type", contentType)
fileReader, err := a.app.GetFileReader(workspaceID, rootID, filename)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
defer fileReader.Close()
http.ServeContent(w, r, filename, time.Now(), fileReader)
auditRec.Success()
2020-10-16 11:41:56 +02:00
}
2021-02-17 20:29:20 +01:00
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
// The FileID to retrieve the uploaded file
2021-02-17 20:29:20 +01:00
// required: true
FileID string `json:"fileId"`
2021-02-17 20:29:20 +01:00
}
func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) {
var fileUploadResponse FileUploadResponse
if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil {
return nil, err
}
return &fileUploadResponse, nil
}
2020-10-16 11:41:56 +02:00
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/{rootID}/files uploadFile
2021-02-17 20:29:20 +01:00
//
// Upload a binary file, attached to a root block
2021-02-17 20:29:20 +01:00
//
// ---
// consumes:
// - multipart/form-data
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
2021-02-17 20:29:20 +01:00
// - name: uploaded file
// in: formData
// type: file
// description: The file to upload
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/FileUploadResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
// Caller must have access to the root block's container
_, err := a.getContainerAllowingReadTokenForBlock(r, rootID)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
file, handle, err := r.FormFile(UploadFormFileKey)
2020-10-16 11:41:56 +02:00
if err != nil {
fmt.Fprintf(w, "%v", err)
return
}
defer file.Close()
auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("rootID", rootID)
auditRec.AddMeta("filename", handle.Filename)
fileID, err := a.app.SaveFile(file, workspaceID, rootID, handle.Filename)
2020-10-16 11:41:56 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-02-17 20:29:20 +01:00
return
}
a.logger.Debug("uploadFile",
mlog.String("filename", handle.Filename),
mlog.String("fileID", fileID),
)
data, err := json.Marshal(FileUploadResponse{FileID: fileID})
2021-02-17 20:29:20 +01:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-10-16 11:41:56 +02:00
return
}
2021-02-17 20:29:20 +01:00
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("fileID", fileID)
auditRec.Success()
2020-10-16 11:41:56 +02:00
}
func (a *API) getWorkspaceUsers(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/users getWorkspaceUsers
//
// Returns workspace users
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
if !a.app.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to workspace", PermissionError{"access denied to workspace"})
return
}
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
users, err := a.app.GetWorkspaceUsers(workspaceID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
data, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("userCount", len(users))
auditRec.Success()
}
// subscriptions
func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/subscriptions createSubscription
//
// Creates a subscription to a block for a user. The user will receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: Body
// in: body
// description: subscription definition
// required: true
// schema:
// "$ref": "#/definitions/Subscription"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var sub model.Subscription
err = json.Unmarshal(requestBody, &sub)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if err = sub.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "createSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("subscriber_id", sub.SubscriberID)
auditRec.AddMeta("block_id", sub.BlockID)
// User can only create subscriptions for themselves (for now)
if session.UserID != sub.SubscriberID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
return
}
// check for valid block
block, err := a.app.GetBlockByID(*container, sub.BlockID)
if err != nil || block == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid blockID", err)
return
}
subNew, err := a.app.CreateSubscription(*container, &sub)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("CREATE subscription",
mlog.String("subscriber_id", subNew.SubscriberID),
mlog.String("block_id", subNew.BlockID),
)
json, err := json.Marshal(subNew)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.Success()
}
func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /api/v1/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID} deleteSubscription
//
// Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
blockID := vars["blockID"]
subscriberID := vars["subscriberID"]
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("block_id", blockID)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only delete subscriptions for themselves
if session.UserID != subscriberID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
return
}
_, err = a.app.DeleteSubscription(*container, blockID, subscriberID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("DELETE subscription",
mlog.String("blockID", blockID),
mlog.String("subscriberID", subscriberID),
)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/subscriptions/{subscriberID} getSubscriptions
//
// Gets subscriptions for a user.
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
subscriberID := vars["subscriberID"]
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only get subscriptions for themselves (for now)
if session.UserID != subscriberID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
return
}
subs, err := a.app.GetSubscriptions(*container, subscriberID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("GET subscriptions",
mlog.String("subscriberID", subscriberID),
mlog.Int("count", len(subs)),
)
json, err := json.Marshal(subs)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("subscription_count", len(subs))
auditRec.Success()
}
2020-10-16 11:41:56 +02:00
// Response helpers
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
a.logger.Error("API ERROR",
mlog.Int("code", code),
mlog.Err(sourceError),
mlog.String("msg", message),
mlog.String("api", api),
)
2020-11-17 15:43:56 +01:00
w.Header().Set("Content-Type", "application/json")
2021-03-26 19:01:54 +01:00
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code})
if err != nil {
data = []byte("{}")
}
2020-10-16 11:41:56 +02:00
w.WriteHeader(code)
_, _ = w.Write(data)
2020-10-16 11:41:56 +02:00
}
2020-10-16 16:21:42 +02:00
func (a *API) errorResponseWithCode(w http.ResponseWriter, api string, statusCode int, errorCode int, message string, sourceError error) {
a.logger.Error("API ERROR",
mlog.Int("status", statusCode),
mlog.Int("code", errorCode),
mlog.Err(sourceError),
mlog.String("msg", message),
mlog.String("api", api),
)
2021-03-26 19:01:54 +01:00
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: errorCode})
if err != nil {
data = []byte("{}")
}
w.WriteHeader(statusCode)
_, _ = w.Write(data)
2021-03-26 19:01:54 +01:00
}
func (a *API) noContainerErrorResponse(w http.ResponseWriter, api string, sourceError error) {
a.errorResponseWithCode(w, api, http.StatusBadRequest, ErrorNoWorkspaceCode, ErrorNoWorkspaceMessage, sourceError)
}
func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write(json)
2020-10-16 16:21:42 +02:00
}
func (a *API) handleGetUserWorkspaces(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userWorkspaces, err := a.app.GetUserWorkspaces(session.UserID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
data, err := json.Marshal(userWorkspaces)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}