f915a20c64
* Working in the new content block editor * Moving blocksEditor content block into its own component * Initial integration with quick development flow * More WIP * Adding drag and drop support with server side help * Some extra work around the styles * Adding image support * Adding video and attachments, and fixing edit * Putting everything behind a feature flag * Adding support for download attachments * Fixing compilation error * Fixing linter errors * Fixing javascript tests * Fixing a typescript error * Moving the move block to an action with undo support * Fixing ci * Fixing post merge errors * Moving to more specific content-blocks api * Apply suggestions from code review Co-authored-by: Doug Lauder <wiggin77@warpmail.net> * Fixing the behavior of certain blocks * Fixing linter error * Fixing javascript linter errors * Adding permission testing for the new move content block api * Adding some unit tests * Improving a bit the tests * Adding more unit tests to the backend * Fixed PR suggestion * Adding h1, h2 and h3 tests * Adding image tests * Adding video tests * Adding attachment tests * Adding quote block tests * Adding divider tests * Adding checkbox tests * Adding list item block tests * Adding text block tests * Reorganizing a bit the code to support deveditor eagain * Fixing dark theme on editor view * Fixing linter errors * Fixing tests and removing unneeded data-testid * Adding root input tests * Fixing some merge problems * Fixing text/text.test.tsx test * Adding more unit tests to the blocks editor * Fix linter error * Adding blocksEditor tests * Fixing linter errors * Adding tests for blockContent * Update webapp/src/components/blocksEditor/blockContent.test.tsx Fix linter warning * Update webapp/src/components/blocksEditor/blockContent.test.tsx Fix linter warning * Update webapp/src/components/blocksEditor/blockContent.test.tsx Fix linter error * Fixing test * Removing unneeded TODO Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
241 lines
6.1 KiB
Go
241 lines
6.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"runtime/debug"
|
|
|
|
"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/permissions"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
)
|
|
|
|
const (
|
|
HeaderRequestedWith = "X-Requested-With"
|
|
HeaderRequestedWithXML = "XMLHttpRequest"
|
|
UploadFormFileKey = "file"
|
|
True = "true"
|
|
|
|
ErrorNoTeamCode = 1000
|
|
ErrorNoTeamMessage = "No team"
|
|
)
|
|
|
|
var (
|
|
ErrHandlerPanic = errors.New("http handler panic")
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
// REST APIs
|
|
|
|
type API struct {
|
|
app *app.App
|
|
authService string
|
|
permissions permissions.PermissionsService
|
|
singleUserToken string
|
|
MattermostAuth bool
|
|
logger mlog.LoggerIFace
|
|
audit *audit.Audit
|
|
isPlugin bool
|
|
}
|
|
|
|
func NewAPI(
|
|
app *app.App,
|
|
singleUserToken string,
|
|
authService string,
|
|
permissions permissions.PermissionsService,
|
|
logger mlog.LoggerIFace,
|
|
audit *audit.Audit,
|
|
isPlugin bool,
|
|
) *API {
|
|
return &API{
|
|
app: app,
|
|
singleUserToken: singleUserToken,
|
|
authService: authService,
|
|
permissions: permissions,
|
|
logger: logger,
|
|
audit: audit,
|
|
isPlugin: isPlugin,
|
|
}
|
|
}
|
|
|
|
func (a *API) RegisterRoutes(r *mux.Router) {
|
|
apiv2 := r.PathPrefix("/api/v2").Subrouter()
|
|
apiv2.Use(a.panicHandler)
|
|
apiv2.Use(a.requireCSRFToken)
|
|
|
|
/* ToDo:
|
|
apiv3 := r.PathPrefix("/api/v3").Subrouter()
|
|
apiv3.Use(a.panicHandler)
|
|
apiv3.Use(a.requireCSRFToken)
|
|
*/
|
|
|
|
// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
|
|
a.registerUsersRoutes(apiv2)
|
|
a.registerAuthRoutes(apiv2)
|
|
a.registerMembersRoutes(apiv2)
|
|
a.registerCategoriesRoutes(apiv2)
|
|
a.registerSharingRoutes(apiv2)
|
|
a.registerTeamsRoutes(apiv2)
|
|
a.registerAchivesRoutes(apiv2)
|
|
a.registerSubscriptionsRoutes(apiv2)
|
|
a.registerFilesRoutes(apiv2)
|
|
a.registerLimitsRoutes(apiv2)
|
|
a.registerInsightsRoutes(apiv2)
|
|
a.registerOnboardingRoutes(apiv2)
|
|
a.registerSearchRoutes(apiv2)
|
|
a.registerConfigRoutes(apiv2)
|
|
a.registerBoardsAndBlocksRoutes(apiv2)
|
|
a.registerChannelsRoutes(apiv2)
|
|
a.registerTemplatesRoutes(apiv2)
|
|
a.registerBoardsRoutes(apiv2)
|
|
a.registerBlocksRoutes(apiv2)
|
|
a.registerContentBlocksRoutes(apiv2)
|
|
a.registerStatisticsRoutes(apiv2)
|
|
|
|
// V3 routes
|
|
a.registerCardsRoutes(apiv2)
|
|
|
|
// System routes are outside the /api/v2 path
|
|
a.registerSystemRoutes(r)
|
|
}
|
|
|
|
func (a *API) RegisterAdminRoutes(r *mux.Router) {
|
|
r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
|
|
}
|
|
|
|
func getUserID(r *http.Request) string {
|
|
ctx := r.Context()
|
|
session, ok := ctx.Value(sessionContextKey).(*model.Session)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return session.UserID
|
|
}
|
|
|
|
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, ErrHandlerPanic)
|
|
}
|
|
}()
|
|
|
|
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, model.NewErrBadRequest("checkCSRFToken FAILED"))
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (a *API) checkCSRFToken(r *http.Request) bool {
|
|
token := r.Header.Get(HeaderRequestedWith)
|
|
return token == HeaderRequestedWithXML
|
|
}
|
|
|
|
func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
|
|
query := r.URL.Query()
|
|
readToken := query.Get("read_token")
|
|
|
|
if len(readToken) < 1 {
|
|
return false
|
|
}
|
|
|
|
isValid, err := a.app.IsValidReadToken(boardID, readToken)
|
|
if err != nil {
|
|
a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
|
|
return false
|
|
}
|
|
|
|
return isValid
|
|
}
|
|
|
|
func (a *API) userIsGuest(userID string) (bool, error) {
|
|
if a.singleUserToken != "" {
|
|
return false, nil
|
|
}
|
|
return a.app.UserIsGuest(userID)
|
|
}
|
|
|
|
// Response helpers
|
|
|
|
func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
a.logger.Error(err.Error())
|
|
errorResponse := model.ErrorResponse{Error: err.Error()}
|
|
|
|
switch {
|
|
case model.IsErrBadRequest(err):
|
|
errorResponse.ErrorCode = http.StatusBadRequest
|
|
case model.IsErrUnauthorized(err):
|
|
errorResponse.ErrorCode = http.StatusUnauthorized
|
|
case model.IsErrForbidden(err):
|
|
errorResponse.ErrorCode = http.StatusForbidden
|
|
case model.IsErrNotFound(err):
|
|
errorResponse.ErrorCode = http.StatusNotFound
|
|
case model.IsErrRequestEntityTooLarge(err):
|
|
errorResponse.ErrorCode = http.StatusRequestEntityTooLarge
|
|
case model.IsErrNotImplemented(err):
|
|
errorResponse.ErrorCode = http.StatusNotImplemented
|
|
default:
|
|
a.logger.Error("API ERROR",
|
|
mlog.Int("code", http.StatusInternalServerError),
|
|
mlog.Err(err),
|
|
mlog.String("api", r.URL.Path),
|
|
)
|
|
errorResponse.Error = "internal server error"
|
|
errorResponse.ErrorCode = http.StatusInternalServerError
|
|
}
|
|
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
|
data, err := json.Marshal(errorResponse)
|
|
if err != nil {
|
|
data = []byte("{}")
|
|
}
|
|
|
|
w.WriteHeader(errorResponse.ErrorCode)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func stringResponse(w http.ResponseWriter, message string) {
|
|
setResponseHeader(w, "Content-Type", "text/plain")
|
|
_, _ = fmt.Fprint(w, message)
|
|
}
|
|
|
|
func jsonStringResponse(w http.ResponseWriter, code int, message string) {
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
fmt.Fprint(w, message)
|
|
}
|
|
|
|
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_, _ = w.Write(json)
|
|
}
|
|
|
|
func setResponseHeader(w http.ResponseWriter, key string, value string) {
|
|
header := w.Header()
|
|
if header == nil {
|
|
return
|
|
}
|
|
header.Set(key, value)
|
|
}
|