Spaces:
Sleeping
Sleeping
| 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/public/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 | |
| } | |
| func NewAPI( | |
| app *app.App, | |
| singleUserToken string, | |
| authService string, | |
| permissions permissions.PermissionsService, | |
| logger mlog.LoggerIFace, | |
| audit *audit.Audit, | |
| ) *API { | |
| return &API{ | |
| app: app, | |
| singleUserToken: singleUserToken, | |
| authService: authService, | |
| permissions: permissions, | |
| logger: logger, | |
| audit: audit, | |
| } | |
| } | |
| 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.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) | |
| a.registerComplianceRoutes(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) { //nolint:unparam | |
| setResponseHeader(w, "Content-Type", "application/json") | |
| w.WriteHeader(code) | |
| fmt.Fprint(w, message) | |
| } | |
| func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam | |
| setResponseHeader(w, "Content-Type", "application/json") | |
| w.WriteHeader(code) | |
| _, _ = w.Write(json) | |
| } | |
| func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam | |
| header := w.Header() | |
| if header == nil { | |
| return | |
| } | |
| header.Set(key, value) | |
| } | |