1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00
focalboard/server/api/api.go

1867 lines
49 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 20:16:59 +02:00
"strconv"
2020-10-16 11:41:56 +02:00
"strings"
2020-12-07 21:40:16 +02:00
"time"
2020-10-16 11:41:56 +02:00
"github.com/gorilla/mux"
2021-01-27 00:13:46 +02:00
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
2021-03-26 20:01:54 +02:00
"github.com/mattermost/focalboard/server/services/store"
2021-01-27 00:13:46 +02: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 20:01:54 +02:00
const (
ErrorNoWorkspaceCode = 1000
ErrorNoWorkspaceMessage = "No workspace"
2021-03-26 20:01:54 +02: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 22:27:34 +02:00
return &API{
app: app,
2021-02-09 22:27:34 +02:00
singleUserToken: singleUserToken,
2021-03-26 20:01:54 +02:00
authService: authService,
logger: logger,
audit: audit,
2021-02-09 22:27:34 +02: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 20:28:52 +02:00
apiv1 := r.PathPrefix("/api/v1").Subrouter()
apiv1.Use(a.panicHandler)
2021-02-05 20:28:52 +02:00
apiv1.Use(a.requireCSRFToken)
2020-12-07 21:40:16 +02:00
2021-03-26 20:01:54 +02: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 20:01:54 +02:00
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
2021-03-26 20:01:54 +02: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 20:01:54 +02:00
// User APIs
2021-02-05 20:28:52 +02: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")
Merge Onboarding feature branch into main (#2406) * Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
2022-02-28 13:28:16 +02:00
apiv1.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
2020-11-06 17:46:35 +02:00
2021-02-05 20:28:52 +02:00
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
2021-02-05 20:28:52 +02: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 20:45:28 +02:00
apiv1.HandleFunc("/workspaces", a.sessionRequired(a.handleGetUserWorkspaces)).Methods("GET")
2021-02-05 20:45:28 +02:00
// Get Files API
2021-02-05 20:28:52 +02:00
2021-02-05 21:22:56 +02: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")
Merge Onboarding feature branch into main (#2406) * Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
2022-02-28 13:28:16 +02:00
// onboarding tour endpoints
apiv1.HandleFunc("/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost)
// 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-23 00:14:12 +02: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 20:28:52 +02: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 20:28:52 +02: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 20:01:54 +02:00
container := store.Container{
WorkspaceID: workspaceID,
2021-03-26 20:01:54 +02: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 20:01:54 +02:00
}
// Native auth: always use root workspace
2021-03-26 20:01:54 +02:00
container := store.Container{
WorkspaceID: "0",
2021-03-26 20:01:54 +02: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 20:01:54 +02: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 20:01:54 +02:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks getBlocks
2021-02-17 21:29:20 +02:00
//
// Returns blocks
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 20:01:54 +02:00
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02: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 (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
2021-02-17 21:29:20 +02: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 21:29:20 +02:00
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 21:29:20 +02:00
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-03-26 20:01:54 +02:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02: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 21:29:20 +02: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 21:29:20 +02: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 21:29:20 +02: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)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
Merge Onboarding feature branch into main (#2406) * Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
2022-02-28 13:28:16 +02:00
userID := session.UserID
model.StampModificationMetadata(userID, blocks, auditRec)
// 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
}
Merge Onboarding feature branch into main (#2406) * Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
2022-02-28 13:28:16 +02:00
func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /api/v1/users/{userID}/config updateUserConfig
//
// Updates user config
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: Body
// in: body
// description: User config patch to apply
// required: true
// schema:
// "$ref": "#/definitions/UserPropPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var patch *model.UserPropPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
vars := mux.Vars(r)
userID := vars["userID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
// a user can update only own config
if userID != session.UserID {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
updatedConfig, err := a.app.UpdateUserConfig(userID, *patch)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
data, err := json.Marshal(updatedConfig)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
2020-12-07 21:40:16 +02:00
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
2021-02-17 21:29:20 +02: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 21:40:16 +02: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 21:40:16 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 21:40:16 +02:00
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 21:40:16 +02:00
return
}
2021-02-17 21:29:20 +02:00
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.Success()
2020-12-07 21:40:16 +02:00
}
func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
2021-02-17 21:29:20 +02: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 21:40:16 +02:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2020-12-07 21:40:16 +02: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 21:40:16 +02:00
user = &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
2020-12-07 21:40:16 +02:00
CreateAt: now,
UpdateAt: now,
}
} else {
user, err = a.app.GetUser(session.UserID)
2020-12-07 21:40:16 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 21:40:16 +02:00
return
}
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2020-12-07 21:40:16 +02:00
return
}
2021-02-17 21:29:20 +02:00
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID)
auditRec.Success()
2020-12-07 21:40:16 +02:00
}
2020-10-16 11:41:56 +02:00
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation DELETE /api/v1/workspaces/{workspaceID}/blocks/{blockID} deleteBlock
2021-02-17 21:29:20 +02:00
//
// Deletes a block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 21:16:25 +02:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-12 21:16:25 +02:00
userID := session.UserID
2020-10-16 11:41:56 +02:00
vars := mux.Vars(r)
blockID := vars["blockID"]
2021-03-26 20:01:54 +02:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02: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) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete undeleteBlock
//
// Undeletes 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 undelete
// 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)
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
}
auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
err = a.app.UndeleteBlock(*container, blockID, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
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 20:01:54 +02:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks/{blockID}/subtree getSubTree
2021-02-17 21:29:20 +02:00
//
// Returns the blocks of a subtree
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 20:01:54 +02:00
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02:00
return
}
2020-11-12 20:16:59 +02: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 20:16:59 +02: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 01:35:30 +02:00
// Sharing
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation GET /api/v1/workspaces/{workspaceID}/sharing/{rootID} getSharing
2021-02-17 21:29:20 +02:00
//
// Returns sharing information for a root block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 01:35:30 +02:00
vars := mux.Vars(r)
rootID := vars["rootID"]
2021-03-26 20:01:54 +02:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02: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 01:35:30 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 01:35:30 +02:00
return
}
sharingData, err := json.Marshal(sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 01:35:30 +02:00
return
}
2021-02-17 21:29:20 +02: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 01:35:30 +02:00
}
func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/sharing/{rootID} postSharing
2021-02-17 21:29:20 +02:00
//
// Sets sharing information for a root block
//
// ---
// produces:
// - application/json
// parameters:
2021-03-26 20:01:54 +02:00
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02: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 20:01:54 +02:00
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
2021-03-26 20:01:54 +02:00
return
}
2021-01-13 01:35:30 +02:00
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 01:35:30 +02: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 01:35:30 +02: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 01:35:30 +02:00
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
2021-01-13 01:35:30 +02:00
userID := session.UserID
if userID == model.SingleUser {
2021-01-13 01:35:30 +02: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 01:35:30 +02:00
sharing.ModifiedBy = userID
err = a.app.UpsertSharing(*container, sharing)
2021-01-13 01:35:30 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-13 01:35:30 +02:00
return
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID))
auditRec.Success()
2021-01-13 01:35:30 +02:00
}
2021-01-14 02:56:01 +02:00
// Workspace
func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation GET /api/v1/workspaces/{workspaceID} getWorkspace
2021-02-17 21:29:20 +02:00
//
// Returns information of the root workspace
//
// ---
// produces:
// - application/json
2021-03-26 20:01:54 +02:00
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02:00
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Workspace"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
2021-03-26 20:01:54 +02:00
var workspace *model.Workspace
var err error
if a.MattermostAuth {
2021-03-26 20:01:54 +02: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 20:01:54 +02: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 20:01:54 +02:00
}
} else {
workspace, err = a.app.GetRootWorkspace()
2021-03-26 20:01:54 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-03-26 20:01:54 +02:00
return
}
2021-01-14 02:56:01 +02:00
}
auditRec := a.makeAuditRecord(r, "getWorkspace", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("resultWorkspaceID", workspace.ID)
2021-01-14 02:56:01 +02:00
workspaceData, err := json.Marshal(workspace)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 02:56:01 +02:00
return
}
2021-02-17 21:29:20 +02:00
jsonBytesResponse(w, http.StatusOK, workspaceData)
auditRec.Success()
2021-01-14 02:56:01 +02:00
}
func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
2021-03-26 20:01:54 +02:00
// swagger:operation POST /api/v1/workspaces/{workspaceID}/regenerate_signup_token regenerateSignupToken
2021-02-17 21:29:20 +02:00
//
// Regenerates the signup token for the root workspace
//
// ---
// produces:
// - application/json
2021-03-26 20:01:54 +02:00
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
2021-02-17 21:29:20 +02:00
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
workspace, err := a.app.GetRootWorkspace()
2021-01-14 02:56:01 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 02:56:01 +02: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 02:56:01 +02:00
err = a.app.UpsertWorkspaceSignupToken(*workspace)
2021-01-14 02:56:01 +02:00
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
2021-01-14 02:56:01 +02:00
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
2021-01-14 02:56:01 +02: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 21:29:20 +02:00
//
// Returns the contents of an uploaded file
//
// ---
// produces:
// - application/json
// - image/jpg
// - image/png
// - image/gif
2021-02-17 21:29:20 +02: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 21:29:20 +02: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 21:29:20 +02:00
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
// The FileID to retrieve the uploaded file
2021-02-17 21:29:20 +02:00
// required: true
FileID string `json:"fileId"`
2021-02-17 21:29:20 +02: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 21:29:20 +02:00
//
// Upload a binary file, attached to a root block
2021-02-17 21:29:20 +02: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 21:29:20 +02: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 21:29:20 +02: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 21:29:20 +02: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 21:29:20 +02: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()
}
Merge Onboarding feature branch into main (#2406) * Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
2022-02-28 13:28:16 +02:00
func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/onboard onboard
//
// Onboards a user on Boards.
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/OnboardingResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
workspaceID, boardID, err := a.app.PrepareOnboardingTour(session.UserID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
response := map[string]string{
"workspaceID": workspaceID,
"boardID": boardID,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
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 16:43:56 +02:00
w.Header().Set("Content-Type", "application/json")
2021-03-26 20:01:54 +02: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 20:01:54 +02: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 20:01:54 +02: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)
}