focalboard/server/api/api.go
Harshil Sharma ab3bf6312c
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 12:28:16 +01:00

1866 lines
49 KiB
Go

package api
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const (
HeaderRequestedWith = "X-Requested-With"
HeaderRequestedWithXML = "XMLHttpRequest"
UploadFormFileKey = "file"
)
const (
ErrorNoWorkspaceCode = 1000
ErrorNoWorkspaceMessage = "No workspace"
)
type PermissionError struct {
msg string
}
func (pe PermissionError) Error() string {
return pe.msg
}
// ----------------------------------------------------------------------------------------------------
// REST APIs
type API struct {
app *app.App
authService string
singleUserToken string
MattermostAuth bool
logger *mlog.Logger
audit *audit.Audit
}
func NewAPI(app *app.App, singleUserToken string, authService string, logger *mlog.Logger, audit *audit.Audit) *API {
return &API{
app: app,
singleUserToken: singleUserToken,
authService: authService,
logger: logger,
audit: audit,
}
}
func (a *API) RegisterRoutes(r *mux.Router) {
apiv1 := r.PathPrefix("/api/v1").Subrouter()
apiv1.Use(a.panicHandler)
apiv1.Use(a.requireCSRFToken)
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")
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")
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")
// User APIs
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")
apiv1.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
apiv1.HandleFunc("/register", a.handleRegister).Methods("POST")
apiv1.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/{rootID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
apiv1.HandleFunc("/workspaces", a.sessionRequired(a.handleGetUserWorkspaces)).Methods("GET")
// Get Files API
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")
// 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")
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {
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)
})
}
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
}
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
}
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"]
container := store.Container{
WorkspaceID: workspaceID,
}
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"}
}
// Native auth: always use root workspace
container := store.Container{
WorkspaceID: "0",
}
// Has session
if session != nil {
return &container, nil
}
// No session, but has valid read token (read-only mode)
if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) {
return &container, nil
}
return nil, PermissionError{"access denied to workspace"}
}
func (a *API) getContainer(r *http.Request) (*store.Container, error) {
return a.getContainerAllowingReadTokenForBlock(r, "")
}
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks getBlocks
//
// Returns blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - 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"
query := r.URL.Query()
parentID := query.Get("parent_id")
blockType := query.Get("type")
all := query.Get("all")
blockID := query.Get("block_id")
container, err := a.getContainerAllowingReadTokenForBlock(r, blockID)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("parentID", parentID)
auditRec.AddMeta("blockType", blockType)
auditRec.AddMeta("all", all)
auditRec.AddMeta("blockID", blockID)
var blocks []model.Block
var block *model.Block
switch {
case all != "":
blocks, err = a.app.GetAllBlocks(*container)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
case blockID != "":
block, err = a.app.GetBlockByID(*container, blockID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if block != nil {
blocks = append(blocks, *block)
}
default:
blocks, err = a.app.GetBlocks(*container, parentID, blockType)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
}
a.logger.Debug("GetBlocks",
mlog.String("parentID", parentID),
mlog.String("blockType", blockType),
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
//
// Insert blocks. The specified IDs will only be used to link
// blocks with existing ones, the rest will be replaced by server
// generated IDs
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - 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
// 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 blocks []model.Block
err = json.Unmarshal(requestBody, &blocks)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
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)
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)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
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()
}
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()
}
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
// 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"
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)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.Success()
}
func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
// 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"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
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()
user = &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: now,
UpdateAt: now,
}
} else {
user, err = a.app.GetUser(session.UserID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID)
auditRec.Success()
}
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /api/v1/workspaces/{workspaceID}/blocks/{blockID} deleteBlock
//
// Deletes 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 delete
// 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, "deleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
err = a.app.DeleteBlock(*container, blockID, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("DELETE Block", mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
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()
}
func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/blocks/{blockID}/subtree getSubTree
//
// Returns the blocks of a subtree
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: blockID
// in: path
// description: The ID of the root block of the subtree
// required: true
// type: string
// - name: l
// in: query
// description: The number of levels to return. 2 or 3. Defaults to 2.
// required: false
// type: integer
// minimum: 2
// maximum: 3
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
blockID := vars["blockID"]
container, err := a.getContainerAllowingReadTokenForBlock(r, blockID)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
query := r.URL.Query()
levels, err := strconv.ParseInt(query.Get("l"), 10, 32)
if err != nil {
levels = 2
}
if levels != 2 && levels != 3 {
a.logger.Error("Invalid levels", mlog.Int64("levels", levels))
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid levels", nil)
return
}
auditRec := a.makeAuditRecord(r, "getSubTree", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("blockID", blockID)
blocks, err := a.app.GetSubTree(*container, blockID, int(levels))
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("GetSubTree",
mlog.Int64("levels", levels),
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
// Sharing
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/sharing/{rootID} getSharing
//
// Returns sharing information for a root block
//
// ---
// 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
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Sharing"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
rootID := vars["rootID"]
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
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)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
sharingData, err := json.Marshal(sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
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()
}
func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/sharing/{rootID} postSharing
//
// Sets sharing information for a root block
//
// ---
// 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
// - 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"
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 sharing model.Sharing
err = json.Unmarshal(requestBody, &sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
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)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if userID == model.SingleUser {
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
}
sharing.ModifiedBy = userID
err = a.app.UpsertSharing(*container, sharing)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID))
auditRec.Success()
}
// Workspace
func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID} getWorkspace
//
// Returns information of the root workspace
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Workspace"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
var workspace *model.Workspace
var err error
if a.MattermostAuth {
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)
return
}
workspace, err = a.app.GetWorkspace(workspaceID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
if workspace == nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid workspace", nil)
return
}
} else {
workspace, err = a.app.GetRootWorkspace()
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
}
auditRec := a.makeAuditRecord(r, "getWorkspace", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("resultWorkspaceID", workspace.ID)
workspaceData, err := json.Marshal(workspace)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, workspaceData)
auditRec.Success()
}
func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/regenerate_signup_token regenerateSignupToken
//
// Regenerates the signup token for the root workspace
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
workspace, err := a.app.GetRootWorkspace()
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
workspace.SignupToken = utils.NewID(utils.IDTypeToken)
err = a.app.UpsertWorkspaceSignupToken(*workspace)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// File upload
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /workspaces/{workspaceID}/{rootID}/{fileID} getFile
//
// Returns the contents of an uploaded file
//
// ---
// produces:
// - application/json
// - image/jpg
// - image/png
// - image/gif
// 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
// - 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"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
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)
contentType := "image/jpg"
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == "png" {
contentType = "image/png"
}
if fileExtension == "gif" {
contentType = "image/gif"
}
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()
}
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
// The FileID to retrieve the uploaded file
// required: true
FileID string `json:"fileId"`
}
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
}
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/{rootID}/files uploadFile
//
// Upload a binary file, attached to a root block
//
// ---
// 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
// - 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)
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)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
a.logger.Debug("uploadFile",
mlog.String("filename", handle.Filename),
mlog.String("fileID", fileID),
)
data, err := json.Marshal(FileUploadResponse{FileID: fileID})
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("fileID", fileID)
auditRec.Success()
}
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()
}
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)
}
// 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),
)
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code})
if err != nil {
data = []byte("{}")
}
w.WriteHeader(code)
_, _ = w.Write(data)
}
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),
)
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)
}
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)
}
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)
}