focalboard/server/api/api.go

1515 lines
39 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"
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"
SingleUser = "single-user"
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.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/{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}/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST")
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")
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")
}
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.GetBlockWithID(*container, blockID)
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 == 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)
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)
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 == SingleUser {
now := utils.GetMillis()
2020-12-07 20:40:16 +01:00
user = &model.User{
ID: SingleUser,
Username: SingleUser,
Email: 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()
}
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
}
func (a *API) handleExport(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks/export exportBlocks
2021-02-17 20:29:20 +01:00
//
// Returns all blocks
//
// ---
// 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:
// type: array
// items:
// "$ref": "#/definitions/Block"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
rootID := query.Get("root_id")
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, "export", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("rootID", rootID)
var blocks []model.Block
if rootID == "" {
blocks, err = a.app.GetAllBlocks(*container)
} else {
blocks, err = a.app.GetBlocksWithRootID(*container, rootID)
2020-10-16 16:21:42 +02:00
}
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
2020-10-16 11:41:56 +02:00
a.logger.Debug("raw blocks", mlog.Int("block_count", len(blocks)))
auditRec.AddMeta("rawCount", len(blocks))
2020-12-10 21:39:09 +01:00
blocks = filterOrphanBlocks(blocks)
a.logger.Debug("EXPORT filtered blocks", mlog.Int("block_count", len(blocks)))
auditRec.AddMeta("filteredCount", 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.Success()
2020-10-16 11:41:56 +02:00
}
2020-12-10 21:39:09 +01:00
func filterOrphanBlocks(blocks []model.Block) (ret []model.Block) {
2020-12-14 20:24:38 +01:00
queue := make([]model.Block, 0)
2021-03-21 09:28:26 +01:00
childrenOfBlockWithID := make(map[string]*[]model.Block)
2020-12-14 20:24:38 +01:00
// Build the trees from nodes
2020-12-10 21:39:09 +01:00
for _, block := range blocks {
2020-12-14 20:24:38 +01:00
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
}
2020-12-10 21:39:09 +01:00
}
}
2020-12-14 20:24:38 +01:00
// 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
2020-12-10 21:39:09 +01:00
}
2020-10-16 11:41:56 +02:00
func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
2021-03-26 19:01:54 +01:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks/import importBlocks
2021-02-17 20:29:20 +01:00
//
// Import 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: 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"
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
}
auditRec := a.makeAuditRecord(r, "import", 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)
_, err = a.app.InsertBlocks(*container, model.GenerateBlockIDs(blocks), session.UserID, false)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
2020-10-16 11:41:56 +02:00
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("IMPORT Blocks", mlog.Int("block_count", len(blocks)))
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
// Stamp ModifiedBy
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-13 00:35:30 +01:00
userID := session.UserID
if userID == SingleUser {
2021-01-13 00:35:30 +01:00
userID = ""
}
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
// 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"
}
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()
}
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)
}