1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-07-03 23:30:29 +02:00

Refactor error usage from the store level up and add API helpers (#3792)

* Refactor error usage from the store level up and add API helpers

* Complete API tests

* Fix merge errorResponse calls

* Remove ensure helpers to allow for custom messages on permission errors

* Fix bad import and call

* Remove bad user check on auth that was added as part of the main merge

* Fix empty list test

* Replace deprecated proxy calls to ioutil.ReadAll with io.ReadAll

* Add information to the NotFound errors

* Add context to all remaining errors and address review comments

* Fix linter

* Adapt the new card API endpoints to the error refactor

* Remove almost all customErrorResponse calls

* Add request entity too large to errorResponse and remove customErrorResponse

* Fix linter
This commit is contained in:
Miguel de la Cruz
2022-09-13 12:18:40 +02:00
committed by GitHub
parent ed655ac996
commit 08c0b7a2fd
68 changed files with 1349 additions and 922 deletions

View File

@ -4,7 +4,7 @@
package boards package boards
import ( import (
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -113,7 +113,7 @@ func TestServeHTTP(t *testing.T) {
result := w.Result() result := w.Result()
assert.NotNil(result) assert.NotNil(result)
defer result.Body.Close() defer result.Body.Close()
bodyBytes, err := ioutil.ReadAll(result.Body) bodyBytes, err := io.ReadAll(result.Body)
assert.Nil(err) assert.Nil(err)
bodyString := string(bodyBytes) bodyString := string(bodyBytes)

View File

@ -2,11 +2,12 @@ package api
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -20,16 +21,16 @@ func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
username := vars["username"] username := vars["username"]
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var requestData AdminSetPasswordData var requestData AdminSetPasswordData
err = json.Unmarshal(requestBody, &requestData) err = json.Unmarshal(requestBody, &requestData)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -38,13 +39,13 @@ func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("username", username) auditRec.AddMeta("username", username)
if !strings.Contains(requestData.Password, "") { if !strings.Contains(requestData.Password, "") {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "password is required", err) a.errorResponse(w, r, model.NewErrBadRequest("password is required"))
return return
} }
err = a.app.UpdateUserPassword(username, requestData.Password) err = a.app.UpdateUserPassword(username, requestData.Password)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -21,22 +21,14 @@ const (
HeaderRequestedWithXML = "XMLHttpRequest" HeaderRequestedWithXML = "XMLHttpRequest"
UploadFormFileKey = "file" UploadFormFileKey = "file"
True = "true" True = "true"
)
const (
ErrorNoTeamCode = 1000 ErrorNoTeamCode = 1000
ErrorNoTeamMessage = "No team" ErrorNoTeamMessage = "No team"
) )
var errAPINotSupportedInStandaloneMode = errors.New("API not supported in standalone mode") var (
ErrHandlerPanic = errors.New("http handler panic")
type PermissionError struct { )
msg string
}
func (pe PermissionError) Error() string {
return pe.msg
}
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// REST APIs // REST APIs
@ -133,7 +125,7 @@ func (a *API) panicHandler(next http.Handler) http.Handler {
mlog.String("stack", string(debug.Stack())), mlog.String("stack", string(debug.Stack())),
mlog.String("uri", r.URL.Path), mlog.String("uri", r.URL.Path),
) )
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", nil) a.errorResponse(w, r, ErrHandlerPanic)
} }
}() }()
@ -145,7 +137,7 @@ func (a *API) requireCSRFToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !a.checkCSRFToken(r) { if !a.checkCSRFToken(r) {
a.logger.Error("checkCSRFToken FAILED") a.logger.Error("checkCSRFToken FAILED")
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "checkCSRFToken FAILED", nil) a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED"))
return return
} }
@ -184,34 +176,39 @@ func (a *API) userIsGuest(userID string) (bool, error) {
// Response helpers // Response helpers
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) { func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
if code == http.StatusUnauthorized || code == http.StatusForbidden { errorResponse := model.ErrorResponse{Error: err.Error()}
a.logger.Debug("API DEBUG",
mlog.Int("code", code), switch {
mlog.Err(sourceError), case model.IsErrBadRequest(err):
mlog.String("msg", message), errorResponse.ErrorCode = http.StatusBadRequest
mlog.String("api", api), case model.IsErrUnauthorized(err):
) errorResponse.ErrorCode = http.StatusUnauthorized
} else { 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", a.logger.Error("API ERROR",
mlog.Int("code", code), mlog.Int("code", http.StatusInternalServerError),
mlog.Err(sourceError), mlog.Err(err),
mlog.String("msg", message), mlog.String("api", r.URL.Path),
mlog.String("api", api),
) )
errorResponse.Error = "internal server error"
errorResponse.ErrorCode = http.StatusInternalServerError
} }
setResponseHeader(w, "Content-Type", "application/json") setResponseHeader(w, "Content-Type", "application/json")
data, err := json.Marshal(errorResponse)
if sourceError != nil && message != sourceError.Error() {
message += "; " + sourceError.Error()
}
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code})
if err != nil { if err != nil {
data = []byte("{}") data = []byte("{}")
} }
w.WriteHeader(code)
w.WriteHeader(errorResponse.ErrorCode)
_, _ = w.Write(data) _, _ = w.Write(data)
} }

78
server/api/api_test.go Normal file
View File

@ -0,0 +1,78 @@
package api
import (
"database/sql"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/stretchr/testify/require"
pluginapi "github.com/mattermost/mattermost-plugin-api"
)
func TestErrorResponse(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
testCases := []struct {
Name string
Error error
ResponseCode int
ResponseBody string
}{
// bad request
{"ErrBadRequest", model.NewErrBadRequest("bad field"), http.StatusBadRequest, "bad field"},
{"ErrViewsLimitReached", model.ErrViewsLimitReached, http.StatusBadRequest, "limit reached"},
{"ErrAuthParam", model.NewErrAuthParam("password is required"), http.StatusBadRequest, "password is required"},
{"ErrInvalidCategory", model.NewErrInvalidCategory("open"), http.StatusBadRequest, "open"},
{"ErrBoardMemberIsLastAdmin", model.ErrBoardMemberIsLastAdmin, http.StatusBadRequest, "no admins"},
{"ErrBoardIDMismatch", model.ErrBoardIDMismatch, http.StatusBadRequest, "Board IDs do not match"},
// unauthorized
{"ErrUnauthorized", model.NewErrUnauthorized("not enough permissions"), http.StatusUnauthorized, "not enough permissions"},
// forbidden
{"ErrForbidden", model.NewErrForbidden("not enough permissions"), http.StatusForbidden, "not enough permissions"},
{"ErrPermission", model.NewErrPermission("not enough permissions"), http.StatusForbidden, "not enough permissions"},
{"ErrPatchUpdatesLimitedCards", model.ErrPatchUpdatesLimitedCards, http.StatusForbidden, "cards that are limited"},
{"ErrCategoryPermissionDenied", model.ErrCategoryPermissionDenied, http.StatusForbidden, "doesn't belong to user"},
// not found
{"ErrNotFound", model.NewErrNotFound("board"), http.StatusNotFound, "board"},
{"ErrNotAllFound", model.NewErrNotAllFound("block", []string{"1", "2"}), http.StatusNotFound, "not all instances of {block} in {1, 2} found"},
{"sql.ErrNoRows", sql.ErrNoRows, http.StatusNotFound, "rows"},
{"mattermost-plugin-api/ErrNotFound", pluginapi.ErrNotFound, http.StatusNotFound, "not found"},
{"ErrNotFound", model.ErrCategoryDeleted, http.StatusNotFound, "category is deleted"},
// request entity too large
{"ErrRequestEntityTooLarge", model.ErrRequestEntityTooLarge, http.StatusRequestEntityTooLarge, "entity too large"},
// not implemented
{"ErrNotFound", model.ErrInsufficientLicense, http.StatusNotImplemented, "appropriate license required"},
{"ErrNotImplemented", model.NewErrNotImplemented("not implemented in plugin mode"), http.StatusNotImplemented, "plugin mode"},
// internal server error
{"Any other error", ErrHandlerPanic, http.StatusInternalServerError, "internal server error"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s should be a %d code", tc.Name, tc.ResponseCode), func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
testAPI.errorResponse(w, r, tc.Error)
res := w.Result()
require.Equal(t, tc.ResponseCode, res.StatusCode)
require.Equal(t, "application/json", res.Header.Get("Content-Type"))
b, rErr := io.ReadAll(res.Body)
require.NoError(t, rErr)
res.Body.Close()
require.Contains(t, string(b), tc.ResponseBody)
})
}
}

View File

@ -56,7 +56,7 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
@ -66,11 +66,7 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
@ -85,7 +81,7 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Transfer-Encoding", "binary") w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil { if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
} }
auditRec.Success() auditRec.Success()
@ -130,17 +126,17 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
teamID := vars["teamID"] teamID := vars["teamID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
@ -166,7 +162,7 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
mlog.String("team_id", teamID), mlog.String("team_id", teamID),
mlog.Err(err), mlog.Err(err),
) )
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -202,7 +198,8 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
@ -218,13 +215,13 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest) boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
ids := []string{} ids := []string{}
@ -243,7 +240,7 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Transfer-Encoding", "binary") w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil { if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
} }
auditRec.Success() auditRec.Success()

View File

@ -3,7 +3,7 @@ package api
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"strings" "strings"
@ -57,30 +57,26 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
}
if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
return return
} }
if len(a.singleUserToken) > 0 { if len(a.singleUserToken) > 0 {
// Not permitted in single-user mode // Not permitted in single-user mode
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil) a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return return
} }
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var loginData model.LoginRequest var loginData model.LoginRequest
err = json.Unmarshal(requestBody, &loginData) err = json.Unmarshal(requestBody, &loginData)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -92,12 +88,12 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
if loginData.Type == "normal" { if loginData.Type == "normal" {
token, err := a.app.Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken) token, err := a.app.Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "incorrect login", err) a.errorResponse(w, r, model.NewErrUnauthorized("incorrect login"))
return return
} }
json, err := json.Marshal(model.LoginResponse{Token: token}) json, err := json.Marshal(model.LoginResponse{Token: token})
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -106,7 +102,7 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid login type", nil) a.errorResponse(w, r, model.NewErrBadRequest("invalid login type"))
} }
func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) { func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
@ -127,17 +123,13 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
}
if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
return return
} }
if len(a.singleUserToken) > 0 { if len(a.singleUserToken) > 0 {
// Not permitted in single-user mode // Not permitted in single-user mode
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil) a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return return
} }
@ -150,7 +142,7 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("userID", session.UserID) auditRec.AddMeta("userID", session.UserID)
if err := a.app.Logout(session.ID); err != nil { if err := a.app.Logout(session.ID); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "incorrect login", err) a.errorResponse(w, r, model.NewErrUnauthorized("incorrect logout"))
return return
} }
@ -185,30 +177,26 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
}
if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
return return
} }
if len(a.singleUserToken) > 0 { if len(a.singleUserToken) > 0 {
// Not permitted in single-user mode // Not permitted in single-user mode
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil) a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return return
} }
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var registerData model.RegisterRequest var registerData model.RegisterRequest
err = json.Unmarshal(requestBody, &registerData) err = json.Unmarshal(requestBody, &registerData)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
registerData.Email = strings.TrimSpace(registerData.Email) registerData.Email = strings.TrimSpace(registerData.Email)
@ -218,29 +206,29 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
if len(registerData.Token) > 0 { if len(registerData.Token) > 0 {
team, err2 := a.app.GetRootTeam() team, err2 := a.app.GetRootTeam()
if err2 != nil { if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) a.errorResponse(w, r, err2)
return return
} }
if registerData.Token != team.SignupToken { if registerData.Token != team.SignupToken {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid token", nil) a.errorResponse(w, r, model.NewErrUnauthorized("invalid token"))
return return
} }
} else { } else {
// No signup token, check if no active users // No signup token, check if no active users
userCount, err2 := a.app.GetRegisteredUserCount() userCount, err2 := a.app.GetRegisteredUserCount()
if err2 != nil { if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) a.errorResponse(w, r, err2)
return return
} }
if userCount > 0 { if userCount > 0 {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "no sign-up token and user(s) already exist", nil) a.errorResponse(w, r, model.NewErrUnauthorized("no sign-up token and user(s) already exist"))
return return
} }
} }
if err = registerData.IsValid(); err != nil { if err = registerData.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
@ -250,7 +238,7 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
err = a.app.RegisterUser(registerData.Username, registerData.Email, registerData.Password) err = a.app.RegisterUser(registerData.Username, registerData.Email, registerData.Password)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -292,37 +280,33 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
}
if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
return return
} }
if len(a.singleUserToken) > 0 { if len(a.singleUserToken) > 0 {
// Not permitted in single-user mode // Not permitted in single-user mode
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil) a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
userID := vars["userID"] userID := vars["userID"]
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var requestData model.ChangePasswordRequest var requestData model.ChangePasswordRequest
if err = json.Unmarshal(requestBody, &requestData); err != nil { if err = json.Unmarshal(requestBody, &requestData); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if err = requestData.IsValid(); err != nil { if err = requestData.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
@ -330,7 +314,7 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelAuth, auditRec) defer a.audit.LogRecord(audit.LevelAuth, auditRec)
if err = a.app.ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil { if err = a.app.ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -349,7 +333,7 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
a.logger.Debug(`attachSession`, mlog.Bool("single_user", len(a.singleUserToken) > 0)) a.logger.Debug(`attachSession`, mlog.Bool("single_user", len(a.singleUserToken) > 0))
if len(a.singleUserToken) > 0 { if len(a.singleUserToken) > 0 {
if required && (token != a.singleUserToken) { if required && (token != a.singleUserToken) {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid single user token", nil) a.errorResponse(w, r, model.NewErrUnauthorized("invalid single user token"))
return return
} }
@ -389,7 +373,7 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
session, err := a.app.GetSession(token) session, err := a.app.GetSession(token)
if err != nil { if err != nil {
if required { if required {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", err) a.errorResponse(w, r, model.NewErrUnauthorized(err.Error()))
return return
} }
@ -404,7 +388,7 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
mlog.String("want", a.authService), mlog.String("want", a.authService),
mlog.String("got", authService), mlog.String("got", authService),
) )
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", err) a.errorResponse(w, r, model.NewErrUnauthorized(err.Error()))
return return
} }
@ -418,7 +402,7 @@ func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request)
// Currently, admin APIs require local unix connections // Currently, admin APIs require local unix connections
conn := GetContextConn(r) conn := GetContextConn(r)
if _, isUnix := conn.(*net.UnixConn); !isUnix { if _, isUnix := conn.(*net.UnixConn); !isUnix {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not a local unix connection", nil) a.errorResponse(w, r, model.NewErrUnauthorized("not a local unix connection"))
return return
} }

View File

@ -2,14 +2,12 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
@ -78,29 +76,25 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken { if userID == "" && !hasValidReadToken {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "Board not found", nil)
return return
} }
if !hasValidReadToken { if !hasValidReadToken {
if board.IsTemplate && board.Type == model.BoardTypeOpen { if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return return
} }
} else { } else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} }
@ -108,12 +102,12 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
var isGuest bool var isGuest bool
isGuest, err = a.userIsGuest(userID) isGuest, err = a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guest are not allowed to get board templates"}) a.errorResponse(w, r, model.NewErrPermission("guest are not allowed to get board templates"))
return return
} }
} }
@ -133,27 +127,26 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
case all != "": case all != "":
blocks, err = a.app.GetBlocksForBoard(boardID) blocks, err = a.app.GetBlocksForBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
case blockID != "": case blockID != "":
block, err = a.app.GetBlockByID(blockID) block, err = a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if block != nil {
if block.BoardID != boardID { if block.BoardID != boardID {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
blocks = append(blocks, *block) blocks = append(blocks, *block)
}
default: default:
blocks, err = a.app.GetBlocks(boardID, parentID, blockType) blocks, err = a.app.GetBlocks(boardID, parentID, blockType)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
} }
@ -169,13 +162,13 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
var bErr error var bErr error
blocks, bErr = a.app.ApplyCloudLimits(blocks) blocks, bErr = a.app.ApplyCloudLimits(blocks)
if bErr != nil { if bErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", bErr) a.errorResponse(w, r, err)
return return
} }
json, err := json.Marshal(blocks) json, err := json.Marshal(blocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -234,9 +227,9 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
val := r.URL.Query().Get("disable_notify") val := r.URL.Query().Get("disable_notify")
disableNotify := val == True disableNotify := val == True
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -244,7 +237,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(requestBody, &blocks) err = json.Unmarshal(requestBody, &blocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -254,7 +247,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// Error checking // Error checking
if len(block.Type) < 1 { if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID) message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
@ -266,32 +259,32 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
if block.CreateAt < 1 { if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID) message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if block.UpdateAt < 1 { if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if block.BoardID != boardID { if block.BoardID != boardID {
message := fmt.Sprintf("invalid BoardID for block id %s", block.ID) message := fmt.Sprintf("invalid BoardID for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
} }
if hasContents { if hasContents {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return return
} }
} }
if hasComments { if hasComments {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"}) a.errorResponse(w, r, model.NewErrPermission("access denied to post card comments"))
return return
} }
} }
@ -311,19 +304,14 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
sourceBoardID := r.URL.Query().Get("sourceBoardID") sourceBoardID := r.URL.Query().Get("sourceBoardID")
if sourceBoardID != "" { if sourceBoardID != "" {
if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil { if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", updateFileIDsErr) a.errorResponse(w, r, updateFileIDsErr)
return return
} }
} }
newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify) newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify)
if err != nil { if err != nil {
if errors.Is(err, app.ErrViewsLimitReached) { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
} else {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
return return
} }
@ -334,7 +322,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
json, err := json.Marshal(newBlocks) json, err := json.Marshal(newBlocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -389,17 +377,18 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
disableNotify := val == True disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return return
} }
block, err := a.app.GetBlockByID(blockID) block, err := a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if block == nil || block.BoardID != boardID { if block.BoardID != boardID {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
@ -410,7 +399,7 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify) err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -463,31 +452,24 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
block, err := a.app.GetLastBlockHistoryEntry(blockID) block, err := a.app.GetLastBlockHistoryEntry(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if board.ID != block.BoardID { if board.ID != block.BoardID {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return return
} }
@ -497,13 +479,13 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
undeletedBlock, err := a.app.UndeleteBlock(blockID, userID) undeletedBlock, err := a.app.UndeleteBlock(blockID, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
undeletedBlockData, err := json.Marshal(undeletedBlock) undeletedBlockData, err := json.Marshal(undeletedBlock)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -564,30 +546,31 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
disableNotify := val == True disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return return
} }
block, err := a.app.GetBlockByID(blockID) block, err := a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if block == nil || block.BoardID != boardID { if block.BoardID != boardID {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var patch *model.BlockPatch var patch *model.BlockPatch
err = json.Unmarshal(requestBody, &patch) err = json.Unmarshal(requestBody, &patch)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -596,13 +579,8 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID) auditRec.AddMeta("blockID", blockID)
_, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify) if _, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify); err != nil {
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
return
}
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return return
} }
@ -657,16 +635,16 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
val := r.URL.Query().Get("disable_notify") val := r.URL.Query().Get("disable_notify")
disableNotify := val == True disableNotify := val == True
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var patches *model.BlockPatchBatch var patches *model.BlockPatchBatch
err = json.Unmarshal(requestBody, &patches) err = json.Unmarshal(requestBody, &patches)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -680,22 +658,18 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
var block *model.Block var block *model.Block
block, err = a.app.GetBlockByID(blockID) block, err = a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrForbidden("access denied to make board changes"))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrPermission("access denied to make board changesa"))
return return
} }
} }
err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify) err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify)
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
return
}
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -748,41 +722,35 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
}
if userID == "" { if userID == "" {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return return
} }
block, err := a.app.GetBlockByID(blockID) block, err := a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if board.ID != block.BoardID { if board.ID != block.BoardID {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
if block.Type == model.TypeComment { if block.Type == model.TypeComment {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to comment on board cards"))
return return
} }
} else { } else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards"))
return return
} }
} }
@ -799,13 +767,13 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True) blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(blocks) data, err := json.Marshal(blocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,12 +2,10 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors" "io"
"io/ioutil"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
@ -57,7 +55,7 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -67,14 +65,14 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
// retrieve boards list // retrieve boards list
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest) boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -85,7 +83,7 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(boards) data, err := json.Marshal(boards)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -125,42 +123,42 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var newBoard *model.Board var newBoard *model.Board
if err = json.Unmarshal(requestBody, &newBoard); err != nil { if err = json.Unmarshal(requestBody, &newBoard); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if newBoard.Type == model.BoardTypeOpen { if newBoard.Type == model.BoardTypeOpen {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) { if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create public boards"))
return return
} }
} else { } else {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) { if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create private boards"))
return return
} }
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
if err = newBoard.IsValid(); err != nil { if err = newBoard.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -172,7 +170,7 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
// create board // create board
board, err := a.app.CreateBoard(newBoard, userID, true) board, err := a.app.CreateBoard(newBoard, userID, true)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -185,7 +183,7 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(board) data, err := json.Marshal(board)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -228,42 +226,38 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken { if userID == "" && !hasValidReadToken {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if !hasValidReadToken { if !hasValidReadToken {
if board.Type == model.BoardTypePrivate { if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} else { } else {
var isGuest bool var isGuest bool
isGuest, err = a.userIsGuest(userID) isGuest, err = a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} }
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} }
@ -279,7 +273,7 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(board) data, err := json.Marshal(board)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -324,49 +318,44 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"] boardID := mux.Vars(r)["boardID"]
board, err := a.app.GetBoard(boardID) if _, err := a.app.GetBoard(boardID); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var patch *model.BoardPatch var patch *model.BoardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil { if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if err = patch.IsValid(); err != nil { if err = patch.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return return
} }
if patch.Type != nil || patch.MinimumRole != nil { if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return return
} }
} }
if patch.ChannelID != nil { if patch.ChannelID != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board access"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board access"))
return return
} }
} }
@ -379,7 +368,7 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
// patch board // patch board
updatedBoard, err := a.app.PatchBoard(patch, boardID, userID) updatedBoard, err := a.app.PatchBoard(patch, boardID, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -390,7 +379,7 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(updatedBoard) data, err := json.Marshal(updatedBoard)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -430,18 +419,13 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
// Check if board exists // Check if board exists
board, err := a.app.GetBoard(boardID) if _, err := a.app.GetBoard(boardID); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return return
} }
@ -450,7 +434,7 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("boardID", boardID)
if err := a.app.DeleteBoard(boardID, userID); err != nil { if err := a.app.DeleteBoard(boardID, userID); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -495,49 +479,45 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
toTeam := query.Get("toTeam") toTeam := query.Get("toTeam")
if userID == "" { if userID == "" {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) { if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
if board.IsTemplate && board.Type == model.BoardTypeOpen { if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} else { } else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
@ -551,13 +531,13 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True) boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(boardsAndBlocks) data, err := json.Marshal(boardsAndBlocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -603,13 +583,13 @@ func (a *API) handleUndeleteBoard(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("boardID", boardID)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to undelete board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to undelete board"))
return return
} }
err := a.app.UndeleteBoard(boardID, userID) err := a.app.UndeleteBoard(boardID, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -653,27 +633,23 @@ func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
board, boardMetadata, err := a.app.GetBoardMetadata(boardID) board, boardMetadata, err := a.app.GetBoardMetadata(boardID)
if errors.Is(err, app.ErrInsufficientLicense) {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", err)
return
}
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if board == nil || boardMetadata == nil { if board == nil || boardMetadata == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) a.errorResponse(w, r, model.NewErrNotFound("board metadata BoardID="+boardID))
return return
} }
if board.Type == model.BoardTypePrivate { if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} else { } else {
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
} }
@ -684,7 +660,7 @@ func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(boardMetadata) data, err := json.Marshal(boardMetadata)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,13 +2,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
@ -51,22 +49,20 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var newBab *model.BoardsAndBlocks var newBab *model.BoardsAndBlocks
if err = json.Unmarshal(requestBody, &newBab); err != nil { if err = json.Unmarshal(requestBody, &newBab); err != nil {
// a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return return
} }
if len(newBab.Boards) == 0 { if len(newBab.Boards) == 0 {
message := "at least one board is required" a.errorResponse(w, r, model.NewErrBadRequest("at least one board is required"))
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return return
} }
@ -81,30 +77,28 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
} }
if teamID != board.TeamID { if teamID != board.TeamID {
message := "cannot create boards for multiple teams" a.errorResponse(w, r, model.NewErrBadRequest("cannot create boards for multiple teams"))
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return return
} }
if board.ID == "" { if board.ID == "" {
message := "boards need an ID to be referenced from the blocks" a.errorResponse(w, r, model.NewErrBadRequest("boards need an ID to be referenced from the blocks"))
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return return
} }
} }
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return return
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
@ -112,25 +106,25 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
// Error checking // Error checking
if len(block.Type) < 1 { if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID) message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if block.CreateAt < 1 { if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID) message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if block.UpdateAt < 1 { if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if !boardIDs[block.BoardID] { if !boardIDs[block.BoardID] {
message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID) message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
} }
@ -139,7 +133,7 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
// linked and then regenerated by the server // linked and then regenerated by the server
newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger) newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -153,7 +147,7 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
// create boards and blocks // create boards and blocks
bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true) bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
@ -166,7 +160,7 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
data, err := json.Marshal(bab) data, err := json.Marshal(bab)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
@ -205,20 +199,20 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var pbab *model.PatchBoardsAndBlocks var pbab *model.PatchBoardsAndBlocks
if err = json.Unmarshal(requestBody, &pbab); err != nil { if err = json.Unmarshal(requestBody, &pbab); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, err)
return return
} }
if err = pbab.IsValid(); err != nil { if err = pbab.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -229,29 +223,25 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
patch := pbab.BoardPatches[i] patch := pbab.BoardPatches[i]
if err = patch.IsValid(); err != nil { if err = patch.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return return
} }
if patch.Type != nil || patch.MinimumRole != nil { if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return return
} }
} }
board, err2 := a.app.GetBoard(boardID) board, err2 := a.app.GetBoard(boardID)
if err2 != nil { if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) a.errorResponse(w, r, err2)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return return
} }
@ -259,7 +249,7 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
teamID = board.TeamID teamID = board.TeamID
} }
if teamID != board.TeamID { if teamID != board.TeamID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("mismatched team ID"))
return return
} }
} }
@ -267,21 +257,17 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
for _, blockID := range pbab.BlockIDs { for _, blockID := range pbab.BlockIDs {
block, err2 := a.app.GetBlockByID(blockID) block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil { if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) a.errorResponse(w, r, err2)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return return
} }
if _, ok := boardIDMap[block.BoardID]; !ok { if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return return
} }
} }
@ -292,12 +278,8 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs)) auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID) bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
return
}
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -308,7 +290,7 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
data, err := json.Marshal(bab) data, err := json.Marshal(bab)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -345,15 +327,15 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var dbab *model.DeleteBoardsAndBlocks var dbab *model.DeleteBoardsAndBlocks
if err = json.Unmarshal(requestBody, &dbab); err != nil { if err = json.Unmarshal(requestBody, &dbab); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -366,24 +348,20 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
// all boards in the request should belong to the same team // all boards in the request should belong to the same team
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return return
} }
if teamID == "" { if teamID == "" {
teamID = board.TeamID teamID = board.TeamID
} }
if teamID != board.TeamID { if teamID != board.TeamID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("all boards should belong to the same team"))
return return
} }
// permission check // permission check
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return return
} }
} }
@ -391,27 +369,23 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
for _, blockID := range dbab.Blocks { for _, blockID := range dbab.Blocks {
block, err2 := a.app.GetBlockByID(blockID) block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil { if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) a.errorResponse(w, r, err2)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return return
} }
if _, ok := boardIDMap[block.BoardID]; !ok { if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return return
} }
} }
if err := dbab.IsValid(); err != nil { if err := dbab.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -421,7 +395,7 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
auditRec.AddMeta("blocksCount", len(dbab.Blocks)) auditRec.AddMeta("blocksCount", len(dbab.Blocks))
if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil { if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
@ -71,29 +72,29 @@ func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
requestBody, err := io.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "invalid request body", err) a.errorResponse(w, r, err)
return return
} }
var newCard *model.Card var newCard *model.Card
if err = json.Unmarshal(requestBody, &newCard); err != nil { if err = json.Unmarshal(requestBody, &newCard); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create card"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create card"))
return return
} }
if newCard.BoardID != "" && newCard.BoardID != boardID { if newCard.BoardID != "" && newCard.BoardID != boardID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", model.ErrBoardIDMismatch) a.errorResponse(w, r, model.ErrBoardIDMismatch)
return return
} }
newCard.PopulateWithBoardID(boardID) newCard.PopulateWithBoardID(boardID)
if err = newCard.CheckValid(); err != nil { if err = newCard.CheckValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -104,7 +105,7 @@ func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
// create card // create card
card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify) card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -116,7 +117,7 @@ func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(card) data, err := json.Marshal(card)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -171,7 +172,7 @@ func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
strPerPage := query.Get("per_page") strPerPage := query.Get("per_page")
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to fetch cards"}) a.errorResponse(w, r, model.NewErrPermission("access denied to fetch cards"))
return return
} }
@ -184,12 +185,14 @@ func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
page, err := strconv.Atoi(strPage) page, err := strconv.Atoi(strPage)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid `page` parameter", err) message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
} }
perPage, err := strconv.Atoi(strPerPage) perPage, err := strconv.Atoi(strPerPage)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid `per_page` parameter", err) message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
} }
auditRec := a.makeAuditRecord(r, "getCards", audit.Fail) auditRec := a.makeAuditRecord(r, "getCards", audit.Fail)
@ -200,7 +203,7 @@ func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
cards, err := a.app.GetCardsForBoard(boardID, page, perPage) cards, err := a.app.GetCardsForBoard(boardID, page, perPage)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -214,7 +217,7 @@ func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(cards) data, err := json.Marshal(cards)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -269,24 +272,25 @@ func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
requestBody, err := io.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
card, err := a.app.GetCardByID(cardID) card, err := a.app.GetCardByID(cardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "could not fetch card "+cardID, err) message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to patch card"}) a.errorResponse(w, r, model.NewErrPermission("access denied to patch card"))
return return
} }
var patch *model.CardPatch var patch *model.CardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil { if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -298,7 +302,7 @@ func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
// patch card // patch card
cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify) cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -310,7 +314,7 @@ func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(cardPatched) data, err := json.Marshal(cardPatched)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -351,12 +355,13 @@ func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
card, err := a.app.GetCardByID(cardID) card, err := a.app.GetCardByID(cardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "could not fetch card "+cardID, err) message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to fetch card"}) a.errorResponse(w, r, model.NewErrPermission("access denied to fetch card"))
return return
} }
@ -373,7 +378,7 @@ func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(card) data, err := json.Marshal(card)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,13 +2,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
) )
@ -54,9 +52,9 @@ func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -64,7 +62,7 @@ func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(requestBody, &category) err = json.Unmarshal(requestBody, &category)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -76,13 +74,8 @@ func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
// user can only create category for themselves // user can only create category for themselves
if category.UserID != session.UserID { if category.UserID != session.UserID {
a.errorResponse( message := fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID)
w, a.errorResponse(w, r, model.NewErrBadRequest(message))
r.URL.Path,
http.StatusBadRequest,
fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID),
nil,
)
return return
} }
@ -90,25 +83,19 @@ func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
teamID := vars["teamID"] teamID := vars["teamID"]
if category.TeamID != teamID { if category.TeamID != teamID {
a.errorResponse( a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
w,
r.URL.Path,
http.StatusBadRequest,
"teamID mismatch",
nil,
)
return return
} }
createdCategory, err := a.app.CreateCategory(&category) createdCategory, err := a.app.CreateCategory(&category)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(createdCategory) data, err := json.Marshal(createdCategory)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -157,16 +144,16 @@ func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
categoryID := vars["categoryID"] categoryID := vars["categoryID"]
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var category model.Category var category model.Category
err = json.Unmarshal(requestBody, &category) err = json.Unmarshal(requestBody, &category)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -174,7 +161,7 @@ func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelModify, auditRec) defer a.audit.LogRecord(audit.LevelModify, auditRec)
if categoryID != category.ID { if categoryID != category.ID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "categoryID mismatch in patch and body", nil) a.errorResponse(w, r, model.NewErrBadRequest("categoryID mismatch in patch and body"))
return return
} }
@ -183,40 +170,25 @@ func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
// user can only update category for themselves // user can only update category for themselves
if category.UserID != session.UserID { if category.UserID != session.UserID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "user ID mismatch in session and category", nil) a.errorResponse(w, r, model.NewErrBadRequest("user ID mismatch in session and category"))
return return
} }
teamID := vars["teamID"] teamID := vars["teamID"]
if category.TeamID != teamID { if category.TeamID != teamID {
a.errorResponse( a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
w,
r.URL.Path,
http.StatusBadRequest,
"teamID mismatch",
nil,
)
return return
} }
updatedCategory, err := a.app.UpdateCategory(&category) updatedCategory, err := a.app.UpdateCategory(&category)
if err != nil { if err != nil {
switch { a.errorResponse(w, r, err)
case errors.Is(err, app.ErrorCategoryDeleted):
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err)
case errors.Is(err, app.ErrorCategoryPermissionDenied):
// TODO: The permissions should be handled as much as possible at
// the API level, this needs to be changed
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
default:
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
return return
} }
data, err := json.Marshal(updatedCategory) data, err := json.Marshal(updatedCategory)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -266,22 +238,13 @@ func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) {
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID) deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
if err != nil { if err != nil {
switch { a.errorResponse(w, r, err)
case errors.Is(err, app.ErrorInvalidCategory):
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
case errors.Is(err, app.ErrorCategoryPermissionDenied):
// TODO: The permissions should be handled as much as possible at
// the API level, this needs to be changed
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
default:
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
return return
} }
data, err := json.Marshal(deletedCategory) data, err := json.Marshal(deletedCategory)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -329,13 +292,13 @@ func (a *API) handleGetUserCategoryBoards(w http.ResponseWriter, r *http.Request
categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID) categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(categoryBlocks) data, err := json.Marshal(categoryBlocks)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -392,7 +355,7 @@ func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request)
// TODO: Check the category and the team matches // TODO: Check the category and the team matches
err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, boardID) err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -50,7 +51,7 @@ func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }
@ -59,12 +60,12 @@ func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) { if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to channel"}) a.errorResponse(w, r, model.NewErrPermission("access denied to channel"))
return return
} }
@ -75,7 +76,7 @@ func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
channel, err := a.app.GetChannel(teamID, channelID) channel, err := a.app.GetChannel(teamID, channelID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -86,14 +87,15 @@ func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
if channel.TeamId != teamID { if channel.TeamId != teamID {
if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup { if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) message := fmt.Sprintf("channel ID=%s on TeamID=%s", channel.Id, teamID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return return
} }
} }
data, err := json.Marshal(channel) data, err := json.Marshal(channel)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -34,7 +34,7 @@ func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) {
configData, err := json.Marshal(clientConfig) configData, err := json.Marshal(clientConfig)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
jsonBytesResponse(w, http.StatusOK, configData) jsonBytesResponse(w, http.StatusOK, configData)

View File

@ -84,22 +84,18 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken { if userID == "" && !hasValidReadToken {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", nil) a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return return
} }
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
@ -124,7 +120,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
fileInfo, err := a.app.GetFileInfo(filename) fileInfo, err := a.app.GetFileInfo(filename)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -139,7 +135,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
data, jsonErr := json.Marshal(fileMetadata) data, jsonErr := json.Marshal(fileMetadata)
if jsonErr != nil { if jsonErr != nil {
a.logger.Error("failed to marshal archived file metadata", mlog.String("filename", filename), mlog.Err(jsonErr)) a.logger.Error("failed to marshal archived file metadata", mlog.String("filename", filename), mlog.Err(jsonErr))
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", jsonErr) a.errorResponse(w, r, jsonErr)
return return
} }
@ -149,7 +145,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
defer fileReader.Close() defer fileReader.Close()
@ -201,17 +197,13 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
@ -222,10 +214,10 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
file, handle, err := r.FormFile(UploadFormFileKey) file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil { if err != nil {
if strings.HasSuffix(err.Error(), "http: request body too large") { if strings.HasSuffix(err.Error(), "http: request body too large") {
a.errorResponse(w, r.URL.Path, http.StatusRequestEntityTooLarge, "", err) a.errorResponse(w, r, model.ErrRequestEntityTooLarge)
return return
} }
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
defer file.Close() defer file.Close()
@ -238,7 +230,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename) fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -248,7 +240,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
) )
data, err := json.Marshal(FileUploadResponse{FileID: fileID}) data, err := json.Marshal(FileUploadResponse{FileID: fileID})
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -63,7 +64,7 @@ func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }
@ -74,7 +75,7 @@ func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
timeRange := query.Get("time_range") timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to team", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -83,25 +84,28 @@ func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
page, err := strconv.Atoi(query.Get("page")) page, err := strconv.Atoi(query.Get("page"))
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting page parameter to integer", err) message := fmt.Sprintf("error converting page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if page < 0 { if page < 0 {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
} }
perPage, err := strconv.Atoi(query.Get("per_page")) perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting per_page parameter to integer", err) message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if perPage < 0 { if perPage < 0 {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
} }
userTimezone, aErr := a.app.GetUserTimezone(userID) userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil { if aErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Error getting time zone of user", aErr) message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
userLocation, _ := time.LoadLocation(userTimezone) userLocation, _ := time.LoadLocation(userTimezone)
@ -116,13 +120,13 @@ func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
PerPage: perPage, PerPage: perPage,
}) })
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "time_range="+timeRange, err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(boardsInsights) data, err := json.Marshal(boardsInsights)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -176,7 +180,7 @@ func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }
@ -186,7 +190,7 @@ func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
timeRange := query.Get("time_range") timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to team", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -194,25 +198,27 @@ func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec) defer a.audit.LogRecord(audit.LevelRead, auditRec)
page, err := strconv.Atoi(query.Get("page")) page, err := strconv.Atoi(query.Get("page"))
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting page parameter to integer", err) a.errorResponse(w, r, model.NewErrBadRequest("error converting page parameter to integer"))
return return
} }
if page < 0 { if page < 0 {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
} }
perPage, err := strconv.Atoi(query.Get("per_page")) perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting per_page parameter to integer", err) message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
if perPage < 0 { if perPage < 0 {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
} }
userTimezone, aErr := a.app.GetUserTimezone(userID) userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil { if aErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Error getting time zone of user", aErr) message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
userLocation, _ := time.LoadLocation(userTimezone) userLocation, _ := time.LoadLocation(userTimezone)
@ -227,12 +233,12 @@ func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
PerPage: perPage, PerPage: perPage,
}) })
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "time_range="+timeRange, err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(boardsInsights) data, err := json.Marshal(boardsInsights)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
jsonBytesResponse(w, http.StatusOK, data) jsonBytesResponse(w, http.StatusOK, data)

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
) )
func (a *API) registerLimitsRoutes(r *mux.Router) { func (a *API) registerLimitsRoutes(r *mux.Router) {
@ -35,13 +36,13 @@ func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) {
boardsCloudLimits, err := a.app.GetBoardsCloudLimits() boardsCloudLimits, err := a.app.GetBoardsCloudLimits()
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(boardsCloudLimits) data, err := json.Marshal(boardsCloudLimits)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -73,7 +74,7 @@ func (a *API) handleNotifyAdminUpgrade(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", errAPINotSupportedInStandaloneMode) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }

View File

@ -2,12 +2,10 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors" "io"
"io/ioutil"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -55,7 +53,7 @@ func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board members"}) a.errorResponse(w, r, model.NewErrPermission("access denied to board members"))
return return
} }
@ -65,7 +63,7 @@ func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) {
members, err := a.app.GetMembersForBoard(boardID) members, err := a.app.GetMembersForBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -76,7 +74,7 @@ func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(members) data, err := json.Marshal(members)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -125,33 +123,29 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return return
} }
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var reqBoardMember *model.BoardMember var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
if reqBoardMember.UserID == "" { if reqBoardMember.UserID == "" {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -169,7 +163,7 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
member, err := a.app.AddMemberToBoard(newBoardMember) member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -180,7 +174,7 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(member) data, err := json.Marshal(member)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -222,37 +216,33 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if userID == "" { if userID == "" {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("missing user ID"))
return return
} }
boardID := mux.Vars(r)["boardID"] boardID := mux.Vars(r)["boardID"]
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
if board.Type != model.BoardTypeOpen { if board.Type != model.BoardTypeOpen {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
return return
} }
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guests not allowed to join boards"}) a.errorResponse(w, r, model.NewErrPermission("guests not allowed to join boards"))
return return
} }
@ -272,7 +262,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
member, err := a.app.AddMemberToBoard(newBoardMember) member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -283,7 +273,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(member) data, err := json.Marshal(member)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -323,24 +313,20 @@ func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if userID == "" { if userID == "" {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) a.errorResponse(w, r, model.NewErrBadRequest("invalid session"))
return return
} }
boardID := mux.Vars(r)["boardID"] boardID := mux.Vars(r)["boardID"]
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return return
} }
board, err := a.app.GetBoard(boardID) board, err := a.app.GetBoard(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return return
} }
@ -350,12 +336,8 @@ func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("addedUserID", userID) auditRec.AddMeta("addedUserID", userID)
err = a.app.DeleteBoardMember(boardID, userID) err = a.app.DeleteBoardMember(boardID, userID)
if errors.Is(err, app.ErrBoardMemberIsLastAdmin) {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return
}
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -410,15 +392,15 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
paramsUserID := mux.Vars(r)["userID"] paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r) userID := getUserID(r)
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var reqBoardMember *model.BoardMember var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return return
} }
@ -433,7 +415,7 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(paramsUserID) isGuest, err := a.userIsGuest(paramsUserID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -442,7 +424,7 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return return
} }
@ -452,12 +434,8 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("patchedUserID", paramsUserID) auditRec.AddMeta("patchedUserID", paramsUserID)
member, err := a.app.UpdateBoardMember(newBoardMember) member, err := a.app.UpdateBoardMember(newBoardMember)
if errors.Is(err, app.ErrBoardMemberIsLastAdmin) {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return
}
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -468,7 +446,7 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(member) data, err := json.Marshal(member)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -513,18 +491,13 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
paramsUserID := mux.Vars(r)["userID"] paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r) userID := getUserID(r)
board, err := a.app.GetBoard(boardID) if _, err := a.app.GetBoard(boardID); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err)
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return return
} }
@ -534,12 +507,8 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("addedUserID", paramsUserID) auditRec.AddMeta("addedUserID", paramsUserID)
deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID) deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID)
if errors.Is(deleteErr, app.ErrBoardMemberIsLastAdmin) {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", deleteErr)
return
}
if deleteErr != nil { if deleteErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", deleteErr) a.errorResponse(w, r, deleteErr)
return return
} }

View File

@ -49,23 +49,23 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return return
} }
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID) teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -75,7 +75,7 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
} }
data, err := json.Marshal(response) data, err := json.Marshal(response)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -52,7 +52,7 @@ func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }
@ -63,7 +63,7 @@ func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -73,7 +73,7 @@ func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery) channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -84,7 +84,7 @@ func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(channels) data, err := json.Marshal(channels)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -133,7 +133,7 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -148,14 +148,14 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
// retrieve boards list // retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest) boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -166,7 +166,7 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(boards) data, err := json.Marshal(boards)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -212,7 +212,7 @@ func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request)
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth { if !a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return return
} }
@ -221,7 +221,7 @@ func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request)
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -237,7 +237,7 @@ func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request)
// retrieve boards list // retrieve boards list
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID) boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -255,7 +255,7 @@ func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request)
data, err := json.Marshal(linkableBoards) data, err := json.Marshal(linkableBoards)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -307,14 +307,14 @@ func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
// retrieve boards list // retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest) boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -324,7 +324,7 @@ func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(boards) data, err := json.Marshal(boards)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,7 +2,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "errors"
"io"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -12,6 +13,8 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
) )
var ErrTurningOnSharing = errors.New("turning on sharing for board failed, see log for details")
func (a *API) registerSharingRoutes(r *mux.Router) { func (a *API) registerSharingRoutes(r *mux.Router) {
// Sharing APIs // Sharing APIs
r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST") r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
@ -51,7 +54,7 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return return
} }
@ -61,17 +64,13 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
sharing, err := a.app.GetSharing(boardID) sharing, err := a.app.GetSharing(boardID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return
}
if sharing == nil {
jsonStringResponse(w, http.StatusOK, "")
return return
} }
sharingData, err := json.Marshal(sharing) sharingData, err := json.Marshal(sharing)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -121,20 +120,20 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return return
} }
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var sharing model.Sharing var sharing model.Sharing
err = json.Unmarshal(requestBody, &sharing) err = json.Unmarshal(requestBody, &sharing)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -162,7 +161,7 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
"Attempt to turn on sharing for board via API failed, sharing off in configuration.", "Attempt to turn on sharing for board via API failed, sharing off in configuration.",
mlog.String("boardID", sharing.ID), mlog.String("boardID", sharing.ID),
mlog.String("userID", userID)) mlog.String("userID", userID))
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "Turning on sharing for board failed, see log for details.", nil) a.errorResponse(w, r, ErrTurningOnSharing)
return return
} }
@ -170,7 +169,7 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
err = a.app.UpsertSharing(sharing) err = a.app.UpsertSharing(sharing)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,7 +2,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "fmt"
"io"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -48,22 +49,22 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var sub model.Subscription var sub model.Subscription
err = json.Unmarshal(requestBody, &sub) if err = json.Unmarshal(requestBody, &sub); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return return
} }
if err = sub.IsValid(); err != nil { if err = sub.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
} }
ctx := r.Context() ctx := r.Context()
@ -76,20 +77,21 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
// User can only create subscriptions for themselves (for now) // User can only create subscriptions for themselves (for now)
if session.UserID != sub.SubscriberID { if session.UserID != sub.SubscriberID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil) a.errorResponse(w, r, model.NewErrBadRequest("userID and subscriberID mismatch"))
return return
} }
// check for valid block // check for valid block
block, err := a.app.GetBlockByID(sub.BlockID) _, bErr := a.app.GetBlockByID(sub.BlockID)
if err != nil || block == nil { if bErr != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid blockID", err) message := fmt.Sprintf("invalid blockID: %s", bErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return return
} }
subNew, err := a.app.CreateSubscription(&sub) subNew, err := a.app.CreateSubscription(&sub)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -100,7 +102,7 @@ func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
json, err := json.Marshal(subNew) json, err := json.Marshal(subNew)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -151,13 +153,12 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
// User can only delete subscriptions for themselves // User can only delete subscriptions for themselves
if session.UserID != subscriberID { if session.UserID != subscriberID {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil) a.errorResponse(w, r, model.NewErrPermission("access denied"))
return return
} }
_, err := a.app.DeleteSubscription(blockID, subscriberID) if _, err := a.app.DeleteSubscription(blockID, subscriberID); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return return
} }
@ -209,13 +210,13 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
// User can only get subscriptions for themselves (for now) // User can only get subscriptions for themselves (for now)
if session.UserID != subscriberID { if session.UserID != subscriberID {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil) a.errorResponse(w, r, model.NewErrPermission("access denied"))
return return
} }
subs, err := a.app.GetSubscriptions(subscriberID) subs, err := a.app.GetSubscriptions(subscriberID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -226,7 +227,7 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
json, err := json.Marshal(subs) json, err := json.Marshal(subs)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
jsonBytesResponse(w, http.StatusOK, json) jsonBytesResponse(w, http.StatusOK, json)

View File

@ -44,7 +44,7 @@ func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) {
teams, err := a.app.GetTeamsForUser(userID) teams, err := a.app.GetTeamsForUser(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
} }
auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail) auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail)
@ -53,7 +53,7 @@ func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(teams) data, err := json.Marshal(teams)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -92,7 +92,7 @@ func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -101,17 +101,16 @@ func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) {
if a.MattermostAuth { if a.MattermostAuth {
team, err = a.app.GetTeam(teamID) team, err = a.app.GetTeam(teamID)
if err != nil { if model.IsErrNotFound(err) {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, model.NewErrUnauthorized("invalid team"))
} }
if team == nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid team", nil) a.errorResponse(w, r, err)
return
} }
} else { } else {
team, err = a.app.GetRootTeam() team, err = a.app.GetRootTeam()
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
} }
@ -122,7 +121,7 @@ func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(team) data, err := json.Marshal(team)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -154,13 +153,13 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth { if a.MattermostAuth {
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return return
} }
team, err := a.app.GetRootTeam() team, err := a.app.GetRootTeam()
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -169,9 +168,8 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
team.SignupToken = utils.NewID(utils.IDTypeToken) team.SignupToken = utils.NewID(utils.IDTypeToken)
err = a.app.UpsertTeamSignupToken(*team) if err = a.app.UpsertTeamSignupToken(*team); err != nil {
if err != nil { a.errorResponse(w, r, err)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return return
} }
@ -225,7 +223,7 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
excludeBots := r.URL.Query().Get("exclude_bots") == True excludeBots := r.URL.Query().Get("exclude_bots") == True
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "Access denied to team", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
@ -234,7 +232,7 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
asGuestUser := "" asGuestUser := ""
@ -244,13 +242,13 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser, excludeBots) users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser, excludeBots)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(users) data, err := json.Marshal(users)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -47,17 +47,17 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r) userID := getUserID(r)
if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return return
} }
isGuest, err := a.userIsGuest(userID) isGuest, err := a.userIsGuest(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if isGuest { if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to templates"}) a.errorResponse(w, r, model.NewErrPermission("access denied to templates"))
return return
} }
@ -68,7 +68,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
// retrieve boards list // retrieve boards list
boards, err := a.app.GetTemplateBoards(teamID, userID) boards, err := a.app.GetTemplateBoards(teamID, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -88,7 +88,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(results) data, err := json.Marshal(results)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -2,7 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -47,15 +47,15 @@ func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var userIDs []string var userIDs []string
if err = json.Unmarshal(requestBody, &userIDs); err != nil { if err = json.Unmarshal(requestBody, &userIDs); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -64,13 +64,13 @@ func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) {
users, err := a.app.GetUsersList(userIDs) users, err := a.app.GetUsersList(userIDs)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) a.errorResponse(w, r, err)
return return
} }
usersList, err := json.Marshal(users) usersList, err := json.Marshal(users)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) a.errorResponse(w, r, err)
return return
} }
@ -120,14 +120,15 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
} else { } else {
user, err = a.app.GetUser(userID) user, err = a.app.GetUser(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) // ToDo: wrap with an invalid token error
a.errorResponse(w, r, err)
return return
} }
} }
userData, err := json.Marshal(user) userData, err := json.Marshal(user)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
jsonBytesResponse(w, http.StatusOK, userData) jsonBytesResponse(w, http.StatusOK, userData)
@ -166,13 +167,13 @@ func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) {
members, err := a.app.GetMembersForUser(userID) members, err := a.app.GetMembersForUser(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
membersData, err := json.Marshal(members) membersData, err := json.Marshal(members)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -216,7 +217,7 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
user, err := a.app.GetUser(userID) user, err := a.app.GetUser(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -225,17 +226,17 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID) canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
if !canSeeUser { if !canSeeUser {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) a.errorResponse(w, r, model.NewErrNotFound("user ID="+userID))
return return
} }
userData, err := json.Marshal(user) userData, err := json.Marshal(user)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -273,16 +274,16 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
var patch *model.UserPropPatch var patch *model.UserPropPatch
err = json.Unmarshal(requestBody, &patch) err = json.Unmarshal(requestBody, &patch)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -297,19 +298,19 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// a user can update only own config // a user can update only own config
if userID != session.UserID { if userID != session.UserID {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) a.errorResponse(w, r, model.NewErrForbidden(""))
return return
} }
updatedConfig, err := a.app.UpdateUserConfig(userID, *patch) updatedConfig, err := a.app.UpdateUserConfig(userID, *patch)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(updatedConfig) data, err := json.Marshal(updatedConfig)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
@ -344,13 +345,13 @@ func (a *API) handleGetUserPreferences(w http.ResponseWriter, r *http.Request) {
preferences, err := a.app.GetUserPreferences(userID) preferences, err := a.app.GetUserPreferences(userID)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }
data, err := json.Marshal(preferences) data, err := json.Marshal(preferences)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r, err)
return return
} }

View File

@ -82,7 +82,7 @@ func (a *App) Login(username, email, password, mfaToken string) (string, error)
if username != "" { if username != "" {
var err error var err error
user, err = a.store.GetUserByUsername(username) user, err = a.store.GetUserByUsername(username)
if err != nil { if err != nil && !model.IsErrNotFound(err) {
a.metrics.IncrementLoginFailCount(1) a.metrics.IncrementLoginFailCount(1)
return "", errors.Wrap(err, "invalid username or password") return "", errors.Wrap(err, "invalid username or password")
} }
@ -91,11 +91,12 @@ func (a *App) Login(username, email, password, mfaToken string) (string, error)
if user == nil && email != "" { if user == nil && email != "" {
var err error var err error
user, err = a.store.GetUserByEmail(email) user, err = a.store.GetUserByEmail(email)
if err != nil { if err != nil && model.IsErrNotFound(err) {
a.metrics.IncrementLoginFailCount(1) a.metrics.IncrementLoginFailCount(1)
return "", errors.Wrap(err, "invalid username or password") return "", errors.Wrap(err, "invalid username or password")
} }
} }
if user == nil { if user == nil {
a.metrics.IncrementLoginFailCount(1) a.metrics.IncrementLoginFailCount(1)
return "", errors.New("invalid username or password") return "", errors.New("invalid username or password")
@ -148,7 +149,10 @@ func (a *App) RegisterUser(username, email, password string) error {
if username != "" { if username != "" {
var err error var err error
user, err = a.store.GetUserByUsername(username) user, err = a.store.GetUserByUsername(username)
if err == nil && user != nil { if err != nil && !model.IsErrNotFound(err) {
return err
}
if user != nil {
return errors.New("The username already exists") return errors.New("The username already exists")
} }
} }
@ -156,7 +160,10 @@ func (a *App) RegisterUser(username, email, password string) error {
if user == nil && email != "" { if user == nil && email != "" {
var err error var err error
user, err = a.store.GetUserByEmail(email) user, err = a.store.GetUserByEmail(email)
if err == nil && user != nil { if err != nil && !model.IsErrNotFound(err) {
return err
}
if user != nil {
return errors.New("The email already exists") return errors.New("The email already exists")
} }
} }

View File

@ -108,7 +108,7 @@ func TestRegisterUser(t *testing.T) {
th.Store.EXPECT().GetUserByUsername("existingUsername").Return(mockUser, nil) th.Store.EXPECT().GetUserByUsername("existingUsername").Return(mockUser, nil)
th.Store.EXPECT().GetUserByUsername("newUsername").Return(mockUser, errors.New("user not found")) th.Store.EXPECT().GetUserByUsername("newUsername").Return(mockUser, errors.New("user not found"))
th.Store.EXPECT().GetUserByEmail("existingEmail").Return(mockUser, nil) th.Store.EXPECT().GetUserByEmail("existingEmail").Return(mockUser, nil)
th.Store.EXPECT().GetUserByEmail("newEmail").Return(nil, errors.New("email not found")) th.Store.EXPECT().GetUserByEmail("newEmail").Return(nil, model.NewErrNotFound("user"))
th.Store.EXPECT().CreateUser(gomock.Any()).Return(nil) th.Store.EXPECT().CreateUser(gomock.Any()).Return(nil)
for _, test := range testcases { for _, test := range testcases {

View File

@ -13,8 +13,6 @@ import (
) )
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards") var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
var ErrViewsLimitReached = errors.New("views limit reached for board")
var ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) { func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
if boardID == "" { if boardID == "" {
@ -81,7 +79,7 @@ func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch,
return nil, lErr return nil, lErr
} }
if containsLimitedBlocks { if containsLimitedBlocks {
return nil, ErrPatchUpdatesLimitedCards return nil, model.ErrPatchUpdatesLimitedCards
} }
} }
@ -132,7 +130,7 @@ func (a *App) PatchBlocksAndNotify(teamID string, blockPatches *model.BlockPatch
return err return err
} }
if containsLimitedBlocks { if containsLimitedBlocks {
return ErrPatchUpdatesLimitedCards return model.ErrPatchUpdatesLimitedCards
} }
} }
@ -249,7 +247,7 @@ func (a *App) InsertBlocksAndNotify(blocks []model.Block, modifiedByID string, d
if !withinLimit { if !withinLimit {
a.logger.Info("views limit reached on board", mlog.String("board_id", blocks[i].ParentID), mlog.String("team_id", board.TeamID)) a.logger.Info("views limit reached on board", mlog.String("board_id", blocks[i].ParentID), mlog.String("team_id", board.TeamID))
return nil, ErrViewsLimitReached return nil, model.ErrViewsLimitReached
} }
} }
@ -438,13 +436,12 @@ func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, er
} }
block, err := a.store.GetBlock(blockID) block, err := a.store.GetBlock(blockID)
if err != nil { if model.IsErrNotFound(err) {
a.logger.Error("Error loading the block after a successful undelete, not propagating through websockets or notifications", mlog.String("blockID", blockID))
return nil, err return nil, err
} }
if err != nil {
if block == nil { return nil, err
a.logger.Error("Error loading the block after undelete, not propagating through websockets or notifications")
return nil, nil
} }
board, err := a.store.GetBoard(block.BoardID) board, err := a.store.GetBoard(block.BoardID)
@ -540,7 +537,10 @@ func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *mod
} }
iter, err = a.store.GetBlock(iter.ParentID) iter, err = a.store.GetBlock(iter.ParentID)
if err != nil || iter == nil { if model.IsErrNotFound(err) {
return board, card, nil
}
if err != nil {
return board, card, err return board, card, err
} }
} }

View File

@ -109,7 +109,7 @@ func TestPatchBlocks(t *testing.T) {
th.Store.EXPECT().GetLicense().Return(fakeLicense) th.Store.EXPECT().GetLicense().Return(fakeLicense)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil)
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
require.ErrorIs(t, err, ErrPatchUpdatesLimitedCards) require.ErrorIs(t, err, model.ErrPatchUpdatesLimitedCards)
}) })
} }

View File

@ -15,9 +15,7 @@ import (
) )
var ( var (
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID") ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID")
ErrInsufficientLicense = errors.New("appropriate license required")
) )
const linkBoardMessage = "@%s linked Board [%s](%s) with this channel" const linkBoardMessage = "@%s linked Board [%s](%s) with this channel"
@ -25,9 +23,6 @@ const unlinkBoardMessage = "@%s unlinked Board [%s](%s) with this channel"
func (a *App) GetBoard(boardID string) (*model.Board, error) { func (a *App) GetBoard(boardID string) (*model.Board, error) {
board, err := a.store.GetBoard(boardID) board, err := a.store.GetBoard(boardID)
if model.IsErrNotFound(err) {
return nil, nil
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -37,24 +32,19 @@ func (a *App) GetBoard(boardID string) (*model.Board, error) {
func (a *App) GetBoardMetadata(boardID string) (*model.Board, *model.BoardMetadata, error) { func (a *App) GetBoardMetadata(boardID string) (*model.Board, *model.BoardMetadata, error) {
license := a.store.GetLicense() license := a.store.GetLicense()
if license == nil || !(*license.Features.Compliance) { if license == nil || !(*license.Features.Compliance) {
return nil, nil, ErrInsufficientLicense return nil, nil, model.ErrInsufficientLicense
} }
board, err := a.GetBoard(boardID) board, err := a.GetBoard(boardID)
if err != nil { if model.IsErrNotFound(err) {
return nil, nil, err
}
if board == nil {
// Board may have been deleted, retrieve most recent history instead // Board may have been deleted, retrieve most recent history instead
board, err = a.getBoardHistory(boardID, true) board, err = a.getBoardHistory(boardID, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
} }
if err != nil {
if board == nil { return nil, nil, err
// Board not found
return nil, nil, nil
} }
earliestTime, _, err := a.getBoardDescendantModifiedInfo(boardID, false) earliestTime, _, err := a.getBoardDescendantModifiedInfo(boardID, false)
@ -319,7 +309,7 @@ func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*mode
board, err := a.store.GetBoard(boardID) board, err := a.store.GetBoard(boardID)
if model.IsErrNotFound(err) { if model.IsErrNotFound(err) {
return nil, model.NewErrNotFound(boardID) return nil, model.NewErrNotFound("board ID=" + boardID)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -519,7 +509,7 @@ func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember,
return nil, err2 return nil, err2
} }
if isLastAdmin { if isLastAdmin {
return nil, ErrBoardMemberIsLastAdmin return nil, model.ErrBoardMemberIsLastAdmin
} }
} }
@ -575,7 +565,7 @@ func (a *App) DeleteBoardMember(boardID, userID string) error {
return err return err
} }
if isLastAdmin { if isLastAdmin {
return ErrBoardMemberIsLastAdmin return model.ErrBoardMemberIsLastAdmin
} }
} }

View File

@ -70,7 +70,7 @@ func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID stri
return nil, cErr return nil, cErr
} }
if containsLimitedBlocks { if containsLimitedBlocks {
return nil, ErrPatchUpdatesLimitedCards return nil, model.ErrPatchUpdatesLimitedCards
} }
} }

View File

@ -1,18 +1,10 @@
package app package app
import ( import (
"errors"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/utils"
) )
var (
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
ErrorCategoryDeleted = errors.New("category is deleted")
ErrorInvalidCategory = errors.New("invalid category")
)
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) { func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
category.Hydrate() category.Hydrate()
if err := category.IsValid(); err != nil { if err := category.IsValid(); err != nil {
@ -43,11 +35,11 @@ func (a *App) UpdateCategory(category *model.Category) (*model.Category, error)
} }
if existingCategory.DeleteAt != 0 { if existingCategory.DeleteAt != 0 {
return nil, ErrorCategoryDeleted return nil, model.ErrCategoryDeleted
} }
if existingCategory.UserID != category.UserID { if existingCategory.UserID != category.UserID {
return nil, ErrorCategoryPermissionDenied return nil, model.ErrCategoryPermissionDenied
} }
category.UpdateAt = utils.GetMillis() category.UpdateAt = utils.GetMillis()
@ -84,12 +76,12 @@ func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category
// verify if category belongs to the user // verify if category belongs to the user
if existingCategory.UserID != userID { if existingCategory.UserID != userID {
return nil, ErrorCategoryPermissionDenied return nil, model.ErrCategoryPermissionDenied
} }
// verify if category belongs to the team // verify if category belongs to the team
if existingCategory.TeamID != teamID { if existingCategory.TeamID != teamID {
return nil, ErrorInvalidCategory return nil, model.NewErrInvalidCategory("category doesn't belong to the team")
} }
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil { if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {

View File

@ -6,9 +6,6 @@ import (
func (a *App) GetSharing(boardID string) (*model.Sharing, error) { func (a *App) GetSharing(boardID string) (*model.Sharing, error) {
sharing, err := a.store.GetSharing(boardID) sharing, err := a.store.GetSharing(boardID)
if model.IsErrNotFound(err) {
return nil, nil
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -43,15 +43,15 @@ func TestGetSharing(t *testing.T) {
require.Equal(t, "sharing not found", err.Error()) require.Equal(t, "sharing not found", err.Error())
}) })
t.Run("should return a tuple of nil", func(t *testing.T) { t.Run("should return a not found error", func(t *testing.T) {
th.Store.EXPECT().GetSharing("test-id").Return( th.Store.EXPECT().GetSharing("test-id").Return(
nil, nil,
sql.ErrNoRows, sql.ErrNoRows,
) )
result, err := th.App.GetSharing("test-id") result, err := th.App.GetSharing("test-id")
require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, result) require.Nil(t, result)
require.NoError(t, err)
}) })
} }

View File

@ -142,7 +142,7 @@ func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* eta
if rp.StatusCode >= http.StatusMultipleChoices { if rp.StatusCode >= http.StatusMultipleChoices {
defer closeBody(rp) defer closeBody(rp)
b, err := ioutil.ReadAll(rp.Body) b, err := io.ReadAll(rp.Body)
if err != nil { if err != nil {
return rp, fmt.Errorf("error when parsing response with code %d: %w", rp.StatusCode, err) return rp, fmt.Errorf("error when parsing response with code %d: %w", rp.StatusCode, err)
} }
@ -856,7 +856,7 @@ func (c *Client) ExportBoardArchive(boardID string) ([]byte, *Response) {
} }
defer closeBody(r) defer closeBody(r)
buf, err := ioutil.ReadAll(r.Body) buf, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return nil, BuildErrorResponse(r, err) return nil, BuildErrorResponse(r, err)
} }

View File

@ -1068,7 +1068,8 @@ func TestDeleteBoard(t *testing.T) {
require.True(t, success) require.True(t, success)
dbBoard, err := th.Server.App().GetBoard(board.ID) dbBoard, err := th.Server.App().GetBoard(board.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, dbBoard) require.Nil(t, dbBoard)
}) })
} }
@ -1098,7 +1099,8 @@ func TestUndeleteBoard(t *testing.T) {
require.False(t, success) require.False(t, success)
dbBoard, err := th.Server.App().GetBoard(board.ID) dbBoard, err := th.Server.App().GetBoard(board.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, dbBoard) require.Nil(t, dbBoard)
}) })
@ -1123,7 +1125,8 @@ func TestUndeleteBoard(t *testing.T) {
require.False(t, success) require.False(t, success)
dbBoard, err := th.Server.App().GetBoard(board.ID) dbBoard, err := th.Server.App().GetBoard(board.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, dbBoard) require.Nil(t, dbBoard)
}) })
@ -1156,7 +1159,8 @@ func TestUndeleteBoard(t *testing.T) {
require.False(t, success) require.False(t, success)
dbBoard, err := th.Server.App().GetBoard(board.ID) dbBoard, err := th.Server.App().GetBoard(board.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, dbBoard) require.Nil(t, dbBoard)
}) })

View File

@ -303,7 +303,7 @@ func TestPatchBoardsAndBlocks(t *testing.T) {
BoardPatches: []*model.BoardPatch{ BoardPatches: []*model.BoardPatch{
{Title: &newTitle}, {Title: &newTitle},
}, },
BlockIDs: []string{block1.ID, "board-id-2"}, BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{ BlockPatches: []*model.BlockPatch{
{Title: &newTitle}, {Title: &newTitle},
{Title: &newTitle}, {Title: &newTitle},
@ -674,7 +674,7 @@ func TestDeleteBoardsAndBlocks(t *testing.T) {
th := SetupTestHelper(t).InitBasic() th := SetupTestHelper(t).InitBasic()
defer th.TearDown() defer th.TearDown()
// a board is required for the permission checks // a board and a block are required for the permission checks
newBoard := &model.Board{ newBoard := &model.Board{
TeamID: teamID, TeamID: teamID,
Type: model.BoardTypeOpen, Type: model.BoardTypeOpen,
@ -683,9 +683,19 @@ func TestDeleteBoardsAndBlocks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, board) require.NotNil(t, board)
newBlock := model.Block{
ID: "block-id-1",
BoardID: board.ID,
Title: "title",
}
require.NoError(t, th.Server.App().InsertBlock(newBlock, th.GetUser1().ID))
block, err := th.Server.App().GetBlockByID(newBlock.ID)
require.NoError(t, err)
require.NotNil(t, block)
t.Run("no boards", func(t *testing.T) { t.Run("no boards", func(t *testing.T) {
dbab := &model.DeleteBoardsAndBlocks{ dbab := &model.DeleteBoardsAndBlocks{
Blocks: []string{"block-id-1"}, Blocks: []string{block.ID},
} }
success, resp := th.Client.DeleteBoardsAndBlocks(dbab) success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
@ -790,17 +800,21 @@ func TestDeleteBoardsAndBlocks(t *testing.T) {
// ensure that the entities have been successfully deleted // ensure that the entities have been successfully deleted
board1, err = th.Server.App().GetBoard("board-id-1") board1, err = th.Server.App().GetBoard("board-id-1")
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, board1) require.Nil(t, board1)
block1, err = th.Server.App().GetBlockByID("block-id-1") block1, err = th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, block1) require.Nil(t, block1)
board2, err = th.Server.App().GetBoard("board-id-2") board2, err = th.Server.App().GetBoard("board-id-2")
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, board2) require.Nil(t, board2)
block2, err = th.Server.App().GetBlockByID("block-id-2") block2, err = th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, block2) require.Nil(t, block2)
}) })
} }

View File

@ -5,7 +5,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
@ -38,7 +38,7 @@ type TestCase struct {
url string url string
method string method string
body string body string
userRole string // userAnon, userNoTeamMember, userTeamMember, userViewer, userCommenter, userEditor or userAdmin userRole string // userAnon, userNoTeamMember, userTeamMember, userViewer, userCommenter, userEditor, userAdmin or userGuest
expectedStatusCode int expectedStatusCode int
totalResults int totalResults int
} }
@ -301,7 +301,7 @@ func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients C
require.NoError(t, err) require.NoError(t, err)
} }
if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 {
body, err := ioutil.ReadAll(response.Body) body, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
require.Fail(t, err.Error()) require.Fail(t, err.Error())
} }

View File

@ -38,14 +38,13 @@ func TestSharing(t *testing.T) {
boardID = board.ID boardID = board.ID
s, err := th.Server.App().GetSharing(boardID) s, err := th.Server.App().GetSharing(boardID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, s) require.Nil(t, s)
sharing, resp := th.Client.GetSharing(boardID) sharing, resp := th.Client.GetSharing(boardID)
require.NoError(t, resp.Error) th.CheckNotFound(resp)
require.NotNil(t, sharing) require.Nil(t, sharing)
require.False(t, sharing.Enabled)
require.Empty(t, sharing.ID)
}) })
t.Run("POST sharing, config = false", func(t *testing.T) { t.Run("POST sharing, config = false", func(t *testing.T) {
@ -64,11 +63,8 @@ func TestSharing(t *testing.T) {
t.Run("GET sharing", func(t *testing.T) { t.Run("GET sharing", func(t *testing.T) {
sharing, resp := th.Client.GetSharing(boardID) sharing, resp := th.Client.GetSharing(boardID)
// Expect empty sharing object // Expect empty sharing object
require.NoError(t, resp.Error) th.CheckNotFound(resp)
require.NotNil(t, sharing) require.Nil(t, sharing)
require.False(t, sharing.Enabled)
require.Empty(t, sharing.ID)
require.Empty(t, sharing.Token)
}) })
}) })

View File

@ -13,11 +13,17 @@ const (
MinimumPasswordLength = 8 MinimumPasswordLength = 8
) )
type AuthParamError struct { func NewErrAuthParam(msg string) *ErrAuthParam {
return &ErrAuthParam{
msg: msg,
}
}
type ErrAuthParam struct {
msg string msg string
} }
func (pe AuthParamError) Error() string { func (pe *ErrAuthParam) Error() string {
return pe.msg return pe.msg
} }
@ -84,16 +90,16 @@ type RegisterRequest struct {
func (rd *RegisterRequest) IsValid() error { func (rd *RegisterRequest) IsValid() error {
if strings.TrimSpace(rd.Username) == "" { if strings.TrimSpace(rd.Username) == "" {
return AuthParamError{"username is required"} return NewErrAuthParam("username is required")
} }
if strings.TrimSpace(rd.Email) == "" { if strings.TrimSpace(rd.Email) == "" {
return AuthParamError{"email is required"} return NewErrAuthParam("email is required")
} }
if !auth.IsEmailValid(rd.Email) { if !auth.IsEmailValid(rd.Email) {
return AuthParamError{"invalid email format"} return NewErrAuthParam("invalid email format")
} }
if rd.Password == "" { if rd.Password == "" {
return AuthParamError{"password is required"} return NewErrAuthParam("password is required")
} }
return isValidPassword(rd.Password) return isValidPassword(rd.Password)
} }
@ -113,17 +119,17 @@ type ChangePasswordRequest struct {
// IsValid validates a password change request. // IsValid validates a password change request.
func (rd *ChangePasswordRequest) IsValid() error { func (rd *ChangePasswordRequest) IsValid() error {
if rd.OldPassword == "" { if rd.OldPassword == "" {
return AuthParamError{"old password is required"} return NewErrAuthParam("old password is required")
} }
if rd.NewPassword == "" { if rd.NewPassword == "" {
return AuthParamError{"new password is required"} return NewErrAuthParam("new password is required")
} }
return isValidPassword(rd.NewPassword) return isValidPassword(rd.NewPassword)
} }
func isValidPassword(password string) error { func isValidPassword(password string) error {
if len(password) < MinimumPasswordLength { if len(password) < MinimumPasswordLength {
return AuthParamError{fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength)} return NewErrAuthParam(fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength))
} }
return nil return nil
} }

View File

@ -52,38 +52,24 @@ func (c *Category) Hydrate() {
func (c *Category) IsValid() error { func (c *Category) IsValid() error {
if strings.TrimSpace(c.ID) == "" { if strings.TrimSpace(c.ID) == "" {
return newErrInvalidCategory("category ID cannot be empty") return NewErrInvalidCategory("category ID cannot be empty")
} }
if strings.TrimSpace(c.Name) == "" { if strings.TrimSpace(c.Name) == "" {
return newErrInvalidCategory("category name cannot be empty") return NewErrInvalidCategory("category name cannot be empty")
} }
if strings.TrimSpace(c.UserID) == "" { if strings.TrimSpace(c.UserID) == "" {
return newErrInvalidCategory("category user ID cannot be empty") return NewErrInvalidCategory("category user ID cannot be empty")
} }
if strings.TrimSpace(c.TeamID) == "" { if strings.TrimSpace(c.TeamID) == "" {
return newErrInvalidCategory("category team id ID cannot be empty") return NewErrInvalidCategory("category team id ID cannot be empty")
} }
return nil return nil
} }
type ErrInvalidCategory struct {
msg string
}
func newErrInvalidCategory(msg string) *ErrInvalidCategory {
return &ErrInvalidCategory{
msg: msg,
}
}
func (e *ErrInvalidCategory) Error() string {
return e.msg
}
func CategoryFromJSON(data io.Reader) *Category { func CategoryFromJSON(data io.Reader) *Category {
var category *Category var category *Category
_ = json.NewDecoder(data).Decode(&category) _ = json.NewDecoder(data).Decode(&category)

View File

@ -4,78 +4,313 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strings" "strings"
mmModel "github.com/mattermost/mattermost-server/v6/model"
pluginapi "github.com/mattermost/mattermost-plugin-api" pluginapi "github.com/mattermost/mattermost-plugin-api"
) )
// ErrBlocksFromDifferentBoards is an error type that can be returned var (
// when a set of blocks belong to different boards. ErrViewsLimitReached = errors.New("views limit reached for board")
var ErrBlocksFromDifferentBoards = errors.New("blocks belong to different boards") ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
// ErrNotFound is an error type that can be returned by store APIs when a query unexpectedly fetches no records. ErrInsufficientLicense = errors.New("appropriate license required")
ErrCategoryPermissionDenied = errors.New("category doesn't belong to user")
ErrCategoryDeleted = errors.New("category is deleted")
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
ErrRequestEntityTooLarge = errors.New("request entity too large")
)
// ErrNotFound is an error type that can be returned by store APIs
// when a query unexpectedly fetches no records.
type ErrNotFound struct { type ErrNotFound struct {
resource string entity string
} }
// NewErrNotFound creates a new ErrNotFound instance. // NewErrNotFound creates a new ErrNotFound instance.
func NewErrNotFound(resource string) *ErrNotFound { func NewErrNotFound(entity string) *ErrNotFound {
return &ErrNotFound{ return &ErrNotFound{
resource: resource, entity: entity,
} }
} }
func (nf *ErrNotFound) Error() string { func (nf *ErrNotFound) Error() string {
return fmt.Sprintf("{%s} not found", nf.resource) return fmt.Sprintf("{%s} not found", nf.entity)
} }
// IsErrNotFound returns true if `err` is or wraps one of: // ErrNotAllFound is an error type that can be returned by store APIs
// - model.ErrNotFound // when a query that should fetch a certain amount of records
// - sql.ErrNoRows // unexpectedly fetches less.
// - mattermost-plugin-api/ErrNotFound. type ErrNotAllFound struct {
func IsErrNotFound(err error) bool { entity string
resources []string
}
func NewErrNotAllFound(entity string, resources []string) *ErrNotAllFound {
return &ErrNotAllFound{
entity: entity,
resources: resources,
}
}
func (naf *ErrNotAllFound) Error() string {
return fmt.Sprintf("not all instances of {%s} in {%s} found", naf.entity, strings.Join(naf.resources, ", "))
}
// ErrBadRequest can be returned when the API handler receives a
// malformed request.
type ErrBadRequest struct {
reason string
}
// NewErrNotFound creates a new ErrNotFound instance.
func NewErrBadRequest(reason string) *ErrBadRequest {
return &ErrBadRequest{
reason: reason,
}
}
func (br *ErrBadRequest) Error() string {
return br.reason
}
// ErrUnauthorized can be returned when requester has provided an
// invalid authorization for a given resource or has not provided any.
type ErrUnauthorized struct {
reason string
}
// NewErrUnauthorized creates a new ErrUnauthorized instance.
func NewErrUnauthorized(reason string) *ErrUnauthorized {
return &ErrUnauthorized{
reason: reason,
}
}
func (br *ErrUnauthorized) Error() string {
return br.reason
}
// ErrPermission can be returned when requester lacks a permission for
// a given resource.
type ErrPermission struct {
reason string
}
// NewErrPermission creates a new ErrPermission instance.
func NewErrPermission(reason string) *ErrPermission {
return &ErrPermission{
reason: reason,
}
}
func (br *ErrPermission) Error() string {
return br.reason
}
// ErrForbidden can be returned when requester doesn't have access to
// a given resource.
type ErrForbidden struct {
reason string
}
// NewErrForbidden creates a new ErrForbidden instance.
func NewErrForbidden(reason string) *ErrForbidden {
return &ErrForbidden{
reason: reason,
}
}
func (br *ErrForbidden) Error() string {
return br.reason
}
type ErrInvalidCategory struct {
msg string
}
func NewErrInvalidCategory(msg string) *ErrInvalidCategory {
return &ErrInvalidCategory{
msg: msg,
}
}
func (e *ErrInvalidCategory) Error() string {
return e.msg
}
type ErrNotImplemented struct {
msg string
}
func NewErrNotImplemented(msg string) *ErrNotImplemented {
return &ErrNotImplemented{
msg: msg,
}
}
func (ni *ErrNotImplemented) Error() string {
return ni.msg
}
// IsErrBadRequest returns true if `err` is or wraps one of:
// - model.ErrBadRequest
// - model.ErrViewsLimitReached
// - model.ErrAuthParam
// - model.ErrInvalidCategory
// - model.ErrBoardMemberIsLastAdmin
// - model.ErrBoardIDMismatch.
func IsErrBadRequest(err error) bool {
if err == nil { if err == nil {
return false return false
} }
// check if this is a sql.ErrNotFound // check if this is a model.ErrBadRequest
if errors.Is(err, sql.ErrNoRows) { var br *ErrBadRequest
if errors.As(err, &br) {
return true return true
} }
// check if this is a model.ErrAuthParam
var ap *ErrAuthParam
if errors.As(err, &ap) {
return true
}
// check if this is a model.ErrViewsLimitReached
if errors.Is(err, ErrViewsLimitReached) {
return true
}
// check if this is a model.ErrInvalidCategory
var ic *ErrInvalidCategory
if errors.As(err, &ic) {
return true
}
// check if this is a model.ErrBoardIDMismatch
if errors.Is(err, ErrBoardMemberIsLastAdmin) {
return true
}
// check if this is a model.ErrBoardMemberIsLastAdmin
return errors.Is(err, ErrBoardIDMismatch)
}
// IsErrUnauthorized returns true if `err` is or wraps one of:
// - model.ErrUnauthorized.
func IsErrUnauthorized(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrUnauthorized
var u *ErrUnauthorized
return errors.As(err, &u)
}
// IsErrForbidden returns true if `err` is or wraps one of:
// - model.ErrForbidden
// - model.ErrPermission
// - model.ErrPatchUpdatesLimitedCards
// - model.ErrorCategoryPermissionDenied.
func IsErrForbidden(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrForbidden
var f *ErrForbidden
if errors.As(err, &f) {
return true
}
// check if this is a model.ErrPermission
var p *ErrPermission
if errors.As(err, &p) {
return true
}
// check if this is a model.ErrPatchUpdatesLimitedCards
if errors.Is(err, ErrPatchUpdatesLimitedCards) {
return true
}
// check if this is a model.ErrCategoryPermissionDenied
return errors.Is(err, ErrCategoryPermissionDenied)
}
// IsErrNotFound returns true if `err` is or wraps one of:
// - model.ErrNotFound
// - model.ErrNotAllFound
// - sql.ErrNoRows
// - mattermost-plugin-api/ErrNotFound.
// - model.ErrCategoryDeleted.
func IsErrNotFound(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrNotFound // check if this is a model.ErrNotFound
var nf *ErrNotFound var nf *ErrNotFound
if errors.As(err, &nf) { if errors.As(err, &nf) {
return true return true
} }
// check if this is a plugin API error // check if this is a model.ErrNotAllFound
return errors.Is(err, pluginapi.ErrNotFound) var naf *ErrNotAllFound
} if errors.As(err, &naf) {
return true
// ErrNotAllFound is an error type that can be returned by store APIs
// when a query that should fetch a certain amount of records
// unexpectedly fetches less.
type ErrNotAllFound struct {
resources []string
}
func NewErrNotAllFound(resources []string) *ErrNotAllFound {
return &ErrNotAllFound{
resources: resources,
} }
// check if this is a sql.ErrNotFound
if errors.Is(err, sql.ErrNoRows) {
return true
}
// check if this is a plugin API error
if errors.Is(err, pluginapi.ErrNotFound) {
return true
}
// check if this is a Mattermost AppError with a Not Found status
var appErr *mmModel.AppError
if errors.As(err, &appErr) {
if appErr.StatusCode == http.StatusNotFound {
return true
}
}
// check if this is a model.ErrCategoryDeleted
return errors.Is(err, ErrCategoryDeleted)
} }
func (na *ErrNotAllFound) Error() string { // IsErrRequestEntityTooLarge returns true if `err` is or wraps one of:
return fmt.Sprintf("not all instances in {%s} found", strings.Join(na.resources, ", ")) // - model.ErrRequestEntityTooLarge.
func IsErrRequestEntityTooLarge(err error) bool {
// check if this is a model.ErrRequestEntityTooLarge
return errors.Is(err, ErrRequestEntityTooLarge)
} }
// IsErrNotAllFound returns true if `err` is or wraps a ErrNotAllFound. // IsErrNotImplemented returns true if `err` is or wraps one of:
func IsErrNotAllFound(err error) bool { // - model.ErrNotImplemented
// - model.ErrInsufficientLicense.
func IsErrNotImplemented(err error) bool {
if err == nil { if err == nil {
return false return false
} }
var na *ErrNotAllFound // check if this is a model.ErrNotImplemented
return errors.As(err, &na) var eni *ErrNotImplemented
if errors.As(err, &eni) {
return true
}
// check if this is a model.ErrInsufficientLicense
return errors.Is(err, ErrInsufficientLicense)
} }

View File

@ -10,6 +10,7 @@ import (
"go/format" "go/format"
"go/parser" "go/parser"
"go/token" "go/token"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -126,7 +127,7 @@ func extractStoreMetadata() (*storeMetadata, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to open store/store.go file: %w", err) return nil, fmt.Errorf("unable to open store/store.go file: %w", err)
} }
src, err := ioutil.ReadAll(file) src, err := io.ReadAll(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
@ -348,6 +349,10 @@ func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, err
return nil, err return nil, err
} }
if len(users) != len(userIDs) {
return users, model.NewErrNotAllFound("user", userIDs)
}
return users, nil return users, nil
} }
@ -497,7 +502,7 @@ func (s *MattermostAuthLayer) GetFileInfo(id string) (*mmModel.FileInfo, error)
var appErr *mmModel.AppError var appErr *mmModel.AppError
if errors.As(err, &appErr) { if errors.As(err, &appErr) {
if appErr.StatusCode == http.StatusNotFound { if appErr.StatusCode == http.StatusNotFound {
return nil, nil return nil, model.NewErrNotFound("file info ID=" + id)
} }
} }
@ -757,7 +762,8 @@ func (s *MattermostAuthLayer) GetMemberForBoard(boardID, userID string) (*model.
if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound { if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound {
// Plugin API returns error if channel member doesn't exist. // Plugin API returns error if channel member doesn't exist.
// We're fine if it doesn't exist, so its not an error for us. // We're fine if it doesn't exist, so its not an error for us.
return nil, model.NewErrNotFound(userID) message := fmt.Sprintf("member BoardID=%s UserID=%s", boardID, userID)
return nil, model.NewErrNotFound(message)
} }
return nil, memberErr return nil, memberErr

View File

@ -26,14 +26,6 @@ func (re BoardIDNilError) Error() string {
return "boardID is nil" return "boardID is nil"
} }
type BlockNotFoundErr struct {
blockID string
}
func (be BlockNotFoundErr) Error() string {
return fmt.Sprintf("block not found (block id: %s", be.blockID)
}
func (s *SQLStore) timestampToCharField(name string, as string) string { func (s *SQLStore) timestampToCharField(name string, as string) string {
switch s.dbType { switch s.dbType {
case model.MysqlDBType: case model.MysqlDBType:
@ -136,7 +128,7 @@ func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block
} }
if len(blocks) != len(ids) { if len(blocks) != len(ids) {
return nil, model.NewErrNotAllFound(ids) return blocks, model.NewErrNotAllFound("block", ids)
} }
return blocks, nil return blocks, nil
@ -248,7 +240,7 @@ func (s *SQLStore) insertBlock(db sq.BaseRunner, block *model.Block, userID stri
} }
existingBlock, err := s.getBlock(db, block.ID) existingBlock, err := s.getBlock(db, block.ID)
if err != nil { if err != nil && !model.IsErrNotFound(err) {
return err return err
} }
@ -329,9 +321,6 @@ func (s *SQLStore) patchBlock(db sq.BaseRunner, blockID string, blockPatch *mode
if err != nil { if err != nil {
return err return err
} }
if existingBlock == nil {
return BlockNotFoundErr{blockID}
}
block := blockPatch.Patch(existingBlock) block := blockPatch.Patch(existingBlock)
return s.insertBlock(db, block, userID) return s.insertBlock(db, block, userID)
@ -364,14 +353,13 @@ func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []model.Block, userID s
func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error { func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error {
block, err := s.getBlock(db, blockID) block, err := s.getBlock(db, blockID)
if err != nil { if model.IsErrNotFound(err) {
return err
}
if block == nil {
s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID)) s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID))
return nil // deleting non-exiting block is not considered an error (for now) return nil // deleting non-exiting block is not considered an error (for now)
} }
if err != nil {
return err
}
fieldsJSON, err := json.Marshal(block.Fields) fieldsJSON, err := json.Marshal(block.Fields)
if err != nil { if err != nil {
@ -432,7 +420,7 @@ func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy st
if len(blocks) == 0 { if len(blocks) == 0 {
s.logger.Warn("undeleteBlock block not found", mlog.String("block_id", blockID)) s.logger.Warn("undeleteBlock block not found", mlog.String("block_id", blockID))
return nil // deleting non-exiting block is not considered an error (for now) return nil // undeleting non-exiting block is not considered an error (for now)
} }
block := blocks[0] block := blocks[0]
@ -548,7 +536,7 @@ func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, err
} }
if len(blocks) == 0 { if len(blocks) == 0 {
return nil, nil return nil, model.NewErrNotFound("block ID=" + blockID)
} }
return &blocks[0], nil return &blocks[0], nil
@ -637,7 +625,7 @@ func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board
} }
if len(blocks) == 0 { if len(blocks) == 0 {
return nil, nil, model.NewErrNotFound(blockID) return nil, nil, model.NewErrNotFound("block history BlockID=" + blockID)
} }
return s.getBoardAndCard(db, &blocks[0]) return s.getBoardAndCard(db, &blocks[0])
@ -755,7 +743,8 @@ func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID stri
return nil, err return nil, err
} }
if len(blocks) == 0 { if len(blocks) == 0 {
return nil, BlockNotFoundErr{blockID} message := fmt.Sprintf("block subtree BoardID=%s BlockID=%s", boardID, blockID)
return nil, model.NewErrNotFound(message)
} }
var rootBlock model.Block var rootBlock model.Block

View File

@ -17,14 +17,6 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
) )
type BoardNotFoundErr struct {
boardID string
}
func (be BoardNotFoundErr) Error() string {
return fmt.Sprintf("board not found (board id: %s", be.boardID)
}
func boardFields(prefix string) []string { func boardFields(prefix string) []string {
fields := []string{ fields := []string{
"id", "id",
@ -231,7 +223,7 @@ func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string,
rows, err := query.Query() rows, err := query.Query()
if err != nil { if err != nil {
s.logger.Error(`getBoardsByCondition ERROR`, mlog.Err(err)) s.logger.Error(`getBoardsFieldsByCondition ERROR`, mlog.Err(err))
return nil, err return nil, err
} }
defer s.CloseRows(rows) defer s.CloseRows(rows)
@ -242,7 +234,7 @@ func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string,
} }
if len(boards) == 0 { if len(boards) == 0 {
return nil, sql.ErrNoRows return nil, model.NewErrNotFound("boards")
} }
return boards, nil return boards, nil
@ -297,7 +289,16 @@ func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, tea
} }
defer s.CloseRows(rows) defer s.CloseRows(rows)
return s.boardsFromRows(rows) boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
if len(boards) != len(boardIDs) {
return boards, model.NewErrNotAllFound("board", boardIDs)
}
return boards, nil
} }
func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) { func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) {
@ -410,7 +411,7 @@ func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *mode
return nil, err return nil, err
} }
if existingBoard == nil { if existingBoard == nil {
return nil, BoardNotFoundErr{boardID} return nil, model.NewErrNotFound("board ID=" + boardID)
} }
board := boardPatch.Patch(existingBoard) board := boardPatch.Patch(existingBoard)
@ -598,7 +599,8 @@ func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (
} }
if len(members) == 0 { if len(members) == 0 {
return nil, sql.ErrNoRows message := fmt.Sprintf("board member BoardID=%s UserID=%s", boardID, userID)
return nil, model.NewErrNotFound(message)
} }
return members[0], nil return members[0], nil

View File

@ -1,7 +1,6 @@
package sqlstore package sqlstore
import ( import (
"database/sql"
"fmt" "fmt"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
@ -115,9 +114,6 @@ func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoa
if err != nil { if err != nil {
return err return err
} }
if block == nil {
return sql.ErrNoRows
}
if _, ok := boardIDMap[block.BoardID]; !ok { if _, ok := boardIDMap[block.BoardID]; !ok {
return BlockDoesntBelongToBoardsErr{blockID} return BlockDoesntBelongToBoardsErr{blockID}

View File

@ -29,7 +29,7 @@ func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, er
} }
if len(categories) == 0 { if len(categories) == 0 {
return nil, model.NewErrNotFound(id) return nil, model.NewErrNotFound("category ID=" + id)
} }
return &categories[0], nil return &categories[0], nil

View File

@ -67,29 +67,6 @@ func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID, categoryID,
return s.addUserCategoryBoard(db, userID, categoryID, boardID) return s.addUserCategoryBoard(db, userID, categoryID, boardID)
} }
/*
func (s *SQLStore) userCategoryBoardExists(db sq.BaseRunner, userID, teamID, categoryID, boardID string) (bool, error) {
query := s.getQueryBuilder(db).
Select("blocks.id").
From(s.tablePrefix + "categories AS categories").
Join(s.tablePrefix + "category_boards AS blocks ON blocks.category_id = categories.id").
Where(sq.Eq{
"user_id": userID,
"team_id": teamID,
"categories.id": categoryID,
"board_id": boardID,
})
rows, err := query.Query()
if err != nil {
s.logger.Error("getCategoryBoard error", mlog.Err(err))
return false, err
}
return rows.Next(), nil
}
*/
func (s *SQLStore) addUserCategoryBoard(db sq.BaseRunner, userID, categoryID, boardID string) error { func (s *SQLStore) addUserCategoryBoard(db sq.BaseRunner, userID, categoryID, boardID string) error {
_, err := s.getQueryBuilder(db). _, err := s.getQueryBuilder(db).
Insert(s.tablePrefix+"category_boards"). Insert(s.tablePrefix+"category_boards").

View File

@ -127,8 +127,8 @@ func idsFromRows(rows *sql.Rows) ([]string, error) {
return deleteIds, nil return deleteIds, nil
} }
// genericRetentionPoliciesDeletion actually executes the DELETE query using a sq.SelectBuilder // genericRetentionPoliciesDeletion actually executes the DELETE query
// which selects the rows to delete. // using a sq.SelectBuilder which selects the rows to delete.
func (s *SQLStore) genericRetentionPoliciesDeletion( func (s *SQLStore) genericRetentionPoliciesDeletion(
db sq.BaseRunner, db sq.BaseRunner,
info RetentionTableDeletionInfo, info RetentionTableDeletionInfo,

View File

@ -5,11 +5,14 @@ import (
"errors" "errors"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/focalboard/server/model"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/shared/mlog"
) )
func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *model.FileInfo) error { func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mmModel.FileInfo) error {
query := s.getQueryBuilder(db). query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"file_info"). Insert(s.tablePrefix+"file_info").
Columns( Columns(
@ -44,7 +47,7 @@ func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *model.FileInfo) erro
return nil return nil
} }
func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*model.FileInfo, error) { func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mmModel.FileInfo, error) {
query := s.getQueryBuilder(db). query := s.getQueryBuilder(db).
Select( Select(
"id", "id",
@ -60,7 +63,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*model.FileInfo, er
row := query.QueryRow() row := query.QueryRow()
fileInfo := model.FileInfo{} fileInfo := mmModel.FileInfo{}
err := row.Scan( err := row.Scan(
&fileInfo.Id, &fileInfo.Id,
@ -74,7 +77,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*model.FileInfo, er
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, model.NewErrNotFound("file info ID=" + id)
} }
s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err)) s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err))

View File

@ -102,7 +102,7 @@ func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, blockID string) erro
} }
if count == 0 { if count == 0 {
return model.NewErrNotFound(blockID) return model.NewErrNotFound("notification hint BlockID=" + blockID)
} }
return nil return nil
@ -134,7 +134,7 @@ func (s *SQLStore) getNotificationHint(db sq.BaseRunner, blockID string) (*model
return nil, err return nil, err
} }
if len(hint) == 0 { if len(hint) == 0 {
return nil, model.NewErrNotFound(blockID) return nil, model.NewErrNotFound("notification hint BlockID=" + blockID)
} }
return hint[0], nil return hint[0], nil
} }
@ -165,7 +165,7 @@ func (s *SQLStore) getNextNotificationHint(db sq.BaseRunner, remove bool) (*mode
return nil, err return nil, err
} }
if len(hints) == 0 { if len(hints) == 0 {
return nil, model.NewErrNotFound("") return nil, model.NewErrNotFound("next notification hint")
} }
hint := hints[0] hint := hints[0]
@ -186,7 +186,7 @@ func (s *SQLStore) getNextNotificationHint(db sq.BaseRunner, remove bool) (*mode
if rows == 0 { if rows == 0 {
// another node likely has grabbed this hint for processing concurrently; let that node handle it // another node likely has grabbed this hint for processing concurrently; let that node handle it
// and we'll return an error here so we try again. // and we'll return an error here so we try again.
return nil, model.NewErrNotFound(hint.BlockID) return nil, model.NewErrNotFound("notification hint")
} }
} }

View File

@ -5,6 +5,7 @@ package sqlstore
import ( import (
"database/sql" "database/sql"
"fmt"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
@ -113,7 +114,8 @@ func (s *SQLStore) deleteSubscription(db sq.BaseRunner, blockID string, subscrib
} }
if count == 0 { if count == 0 {
return model.NewErrNotFound(blockID + "," + subscriberID) message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID)
return model.NewErrNotFound(message)
} }
return nil return nil
@ -149,7 +151,8 @@ func (s *SQLStore) getSubscription(db sq.BaseRunner, blockID string, subscriberI
return nil, err return nil, err
} }
if len(subscriptions) == 0 { if len(subscriptions) == 0 {
return nil, model.NewErrNotFound(blockID + "," + subscriberID) message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID)
return nil, model.NewErrNotFound(message)
} }
return subscriptions[0], nil return subscriptions[0], nil
} }

View File

@ -201,9 +201,5 @@ func (s *SQLStore) getAllTeams(db sq.BaseRunner) ([]*model.Team, error) {
return nil, err return nil, err
} }
if len(teams) == 0 {
return nil, sql.ErrNoRows
}
return teams, nil return teams, nil
} }

View File

@ -52,7 +52,7 @@ func (s *SQLStore) getUserByCondition(db sq.BaseRunner, condition sq.Eq) (*model
} }
if len(users) == 0 { if len(users) == 0 {
return nil, nil return nil, model.NewErrNotFound("user")
} }
return users[0], nil return users[0], nil
@ -94,7 +94,7 @@ func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition interface{},
} }
if len(users) == 0 { if len(users) == 0 {
return nil, sql.ErrNoRows return nil, model.NewErrNotFound("user")
} }
return users, nil return users, nil
@ -105,7 +105,16 @@ func (s *SQLStore) getUserByID(db sq.BaseRunner, userID string) (*model.User, er
} }
func (s *SQLStore) getUsersList(db sq.BaseRunner, userIDs []string) ([]*model.User, error) { func (s *SQLStore) getUsersList(db sq.BaseRunner, userIDs []string) ([]*model.User, error) {
return s.getUsersByCondition(db, sq.Eq{"id": userIDs}, 0) users, err := s.getUsersByCondition(db, sq.Eq{"id": userIDs}, 0)
if err != nil {
return nil, err
}
if len(users) != len(userIDs) {
return users, model.NewErrNotAllFound("user", userIDs)
}
return users, nil
} }
func (s *SQLStore) getUserByEmail(db sq.BaseRunner, email string) (*model.User, error) { func (s *SQLStore) getUserByEmail(db sq.BaseRunner, email string) (*model.User, error) {
@ -215,11 +224,21 @@ func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password str
} }
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string) ([]*model.User, error) { func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string) ([]*model.User, error) {
return s.getUsersByCondition(db, nil, 0) users, err := s.getUsersByCondition(db, nil, 0)
if model.IsErrNotFound(err) {
return []*model.User{}, nil
}
return users, err
} }
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string, _ bool) ([]*model.User, error) { func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string, _ bool) ([]*model.User, error) {
return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10) users, err := s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
if model.IsErrNotFound(err) {
return []*model.User{}, nil
}
return users, err
} }
func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) { func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {

View File

@ -24,10 +24,6 @@ func (s *SQLStore) IsErrNotFound(err error) bool {
return model.IsErrNotFound(err) return model.IsErrNotFound(err)
} }
func (s *SQLStore) IsErrNotAllFound(err error) bool {
return model.IsErrNotAllFound(err)
}
func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) { func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) {
b, err := json.Marshal(data) b, err := json.Marshal(data)
if err != nil { if err != nil {

View File

@ -259,7 +259,9 @@ func testPatchBlock(t *testing.T, store store.Store) {
t.Run("not existing block id", func(t *testing.T) { t.Run("not existing block id", func(t *testing.T) {
err := store.PatchBlock("invalid-block-id", &model.BlockPatch{}, "user-id-1") err := store.PatchBlock("invalid-block-id", &model.BlockPatch{}, "user-id-1")
require.Error(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.True(t, model.IsErrNotFound(err))
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocksForBoard(boardID)
require.NoError(t, err) require.NoError(t, err)
@ -407,7 +409,8 @@ func testPatchBlocks(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1")
require.Error(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
retrievedBlock, err := store.GetBlock("id-test") retrievedBlock, err := store.GetBlock("id-test")
require.NoError(t, err) require.NoError(t, err)
@ -489,7 +492,7 @@ func testGetSubTree2(t *testing.T, store store.Store) {
t.Run("from not existing id", func(t *testing.T) { t.Run("from not existing id", func(t *testing.T) {
blocks, err = store.GetSubTree2(boardID, "not-exists", model.QuerySubtreeOptions{}) blocks, err = store.GetSubTree2(boardID, "not-exists", model.QuerySubtreeOptions{})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
} }
@ -590,7 +593,8 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
require.NoError(t, err) require.NoError(t, err)
block, err := store.GetBlock("block1") block, err := store.GetBlock("block1")
require.NoError(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, block) require.Nil(t, block)
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
@ -609,7 +613,8 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
require.NoError(t, err) require.NoError(t, err)
block, err := store.GetBlock("block1") block, err := store.GetBlock("block1")
require.NoError(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, block) require.Nil(t, block)
// Wait for not colliding the ID+insert_at key // Wait for not colliding the ID+insert_at key
@ -638,7 +643,8 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
require.NoError(t, err) require.NoError(t, err)
block, err := store.GetBlock("not-exists") block, err := store.GetBlock("not-exists")
require.NoError(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, block) require.Nil(t, block)
}) })
} }
@ -692,14 +698,14 @@ func testGetBlocks(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParentAndType(boardID, "not-exists", "test") blocks, err = store.GetBlocksWithParentAndType(boardID, "not-exists", "test")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
t.Run("not existing type", func(t *testing.T) { t.Run("not existing type", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "not-existing") blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "not-existing")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
t.Run("valid parent and type", func(t *testing.T) { t.Run("valid parent and type", func(t *testing.T) {
@ -713,7 +719,7 @@ func testGetBlocks(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParent(boardID, "not-exists") blocks, err = store.GetBlocksWithParent(boardID, "not-exists")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
t.Run("valid parent", func(t *testing.T) { t.Run("valid parent", func(t *testing.T) {
@ -727,7 +733,7 @@ func testGetBlocks(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithType(boardID, "not-exists") blocks, err = store.GetBlocksWithType(boardID, "not-exists")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
t.Run("valid type", func(t *testing.T) { t.Run("valid type", func(t *testing.T) {
@ -741,7 +747,7 @@ func testGetBlocks(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksForBoard("not-exists") blocks, err = store.GetBlocksForBoard("not-exists")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 0) require.Empty(t, blocks)
}) })
t.Run("all blocks of the a board", func(t *testing.T) { t.Run("all blocks of the a board", func(t *testing.T) {
@ -750,6 +756,31 @@ func testGetBlocks(t *testing.T, store store.Store) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 5) require.Len(t, blocks, 5)
}) })
t.Run("several blocks by ids", func(t *testing.T) {
time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksByIDs([]string{"block2", "block4"})
require.NoError(t, err)
require.Len(t, blocks, 2)
})
t.Run("blocks by ids where some are not found", func(t *testing.T) {
time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksByIDs([]string{"block2", "blockNonexistent"})
var naf *model.ErrNotAllFound
require.ErrorAs(t, err, &naf)
require.True(t, model.IsErrNotFound(err))
require.Len(t, blocks, 1)
})
t.Run("blocks by ids where none are found", func(t *testing.T) {
time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksByIDs([]string{"blockNonexistent1", "blockNonexistent2"})
var naf *model.ErrNotAllFound
require.ErrorAs(t, err, &naf)
require.True(t, model.IsErrNotFound(err))
require.Empty(t, blocks)
})
} }
func testGetBlock(t *testing.T, store store.Store) { func testGetBlock(t *testing.T, store store.Store) {
@ -776,7 +807,8 @@ func testGetBlock(t *testing.T, store store.Store) {
t.Run("get a non-existing block", func(t *testing.T) { t.Run("get a non-existing block", func(t *testing.T) {
fetchedBlock, err := store.GetBlock("non-existing-id") fetchedBlock, err := store.GetBlock("non-existing-id")
require.NoError(t, err) var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, fetchedBlock) require.Nil(t, fetchedBlock)
}) })
} }
@ -1020,4 +1052,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
require.Equal(t, expectedBlock.ID, block.ID) require.Equal(t, expectedBlock.ID, block.ID)
}) })
t.Run("get block history of a board with no history", func(t *testing.T) {
opts := model.QueryBlockHistoryOptions{}
blocks, err = store.GetBlockHistoryDescendants("nonexistent-board-id", opts)
require.NoError(t, err)
require.Empty(t, blocks)
})
} }

View File

@ -22,6 +22,11 @@ func StoreTestBoardStore(t *testing.T, setup func(t *testing.T) (store.Store, fu
defer tearDown() defer tearDown()
testGetBoardsForUserAndTeam(t, store) testGetBoardsForUserAndTeam(t, store)
}) })
t.Run("GetBoardsInTeamByIds", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetBoardsInTeamByIds(t, store)
})
t.Run("InsertBoard", func(t *testing.T) { t.Run("InsertBoard", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -62,6 +67,11 @@ func StoreTestBoardStore(t *testing.T, setup func(t *testing.T) (store.Store, fu
defer tearDown() defer tearDown()
testGetMembersForBoard(t, store) testGetMembersForBoard(t, store)
}) })
t.Run("GetMembersForUser", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetMembersForUser(t, store)
})
t.Run("DeleteMember", func(t *testing.T) { t.Run("DeleteMember", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -72,6 +82,11 @@ func StoreTestBoardStore(t *testing.T, setup func(t *testing.T) (store.Store, fu
defer tearDown() defer tearDown()
testSearchBoardsForUser(t, store) testSearchBoardsForUser(t, store)
}) })
t.Run("SearchBoardsForUserInTeam", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testSearchBoardsForUserInTeam(t, store)
})
t.Run("GetBoardHistory", func(t *testing.T) { t.Run("GetBoardHistory", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -105,6 +120,8 @@ func testGetBoard(t *testing.T, store store.Store) {
t.Run("nonexisting board", func(t *testing.T) { t.Run("nonexisting board", func(t *testing.T) {
rBoard, err := store.GetBoard("nonexistent-id") rBoard, err := store.GetBoard("nonexistent-id")
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
require.Nil(t, rBoard) require.Nil(t, rBoard)
}) })
@ -113,6 +130,12 @@ func testGetBoard(t *testing.T, store store.Store) {
func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) { func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
userID := "user-id-1" userID := "user-id-1"
t.Run("should return empty list if no results are found", func(t *testing.T) {
boards, err := store.GetBoardsForUserAndTeam(testUserID, testTeamID, true)
require.NoError(t, err)
require.Empty(t, boards)
})
t.Run("should return only the boards of the team that the user is a member of", func(t *testing.T) { t.Run("should return only the boards of the team that the user is a member of", func(t *testing.T) {
teamID1 := "team-id-1" teamID1 := "team-id-1"
teamID2 := "team-id-2" teamID2 := "team-id-2"
@ -195,6 +218,61 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
}) })
} }
func testGetBoardsInTeamByIds(t *testing.T, store store.Store) {
t.Run("should return err not all found if one or more of the ids are not found", func(t *testing.T) {
for _, boardID := range []string{"board-id-1", "board-id-2"} {
board := &model.Board{
ID: boardID,
TeamID: testTeamID,
Type: model.BoardTypeOpen,
}
rBoard, _, err := store.InsertBoardWithAdmin(board, testUserID)
require.NoError(t, err)
require.NotNil(t, rBoard)
}
testCases := []struct {
Name string
BoardIDs []string
ExpectedError bool
ExpectedLen int
}{
{
Name: "if none of the IDs are found",
BoardIDs: []string{"nonexistent-1", "nonexistent-2"},
ExpectedError: true,
ExpectedLen: 0,
},
{
Name: "if not all of the IDs are found",
BoardIDs: []string{"nonexistent-1", "board-id-1"},
ExpectedError: true,
ExpectedLen: 1,
},
{
Name: "if all of the IDs are found",
BoardIDs: []string{"board-id-1", "board-id-2"},
ExpectedError: false,
ExpectedLen: 2,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
boards, err := store.GetBoardsInTeamByIds(tc.BoardIDs, testTeamID)
if tc.ExpectedError {
var naf *model.ErrNotAllFound
require.ErrorAs(t, err, &naf)
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
} else {
require.NoError(t, err)
}
require.Len(t, boards, tc.ExpectedLen)
})
}
})
}
func testInsertBoard(t *testing.T, store store.Store) { func testInsertBoard(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
@ -569,14 +647,22 @@ func testSaveMember(t *testing.T, store store.Store) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, memberHistory, initialMemberHistory) require.Len(t, memberHistory, initialMemberHistory)
}) })
t.Run("should return empty list if no results are found", func(t *testing.T) {
memberHistory, err := store.GetBoardMemberHistory(boardID, "nonexistent-user", 0)
require.NoError(t, err)
require.Empty(t, memberHistory)
})
} }
func testGetMemberForBoard(t *testing.T, store store.Store) { func testGetMemberForBoard(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
boardID := testBoardID boardID := testBoardID
t.Run("should return a no rows error for nonexisting membership", func(t *testing.T) { t.Run("should return an error not found for nonexisting membership", func(t *testing.T) {
bm, err := store.GetMemberForBoard(boardID, userID) bm, err := store.GetMemberForBoard(boardID, userID)
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
require.Nil(t, bm) require.Nil(t, bm)
}) })
@ -602,7 +688,7 @@ func testGetMemberForBoard(t *testing.T, store store.Store) {
} }
func testGetMembersForBoard(t *testing.T, store store.Store) { func testGetMembersForBoard(t *testing.T, store store.Store) {
t.Run("should return empty if there are no members on a board", func(t *testing.T) { t.Run("should return empty list if there are no members on a board", func(t *testing.T) {
members, err := store.GetMembersForBoard(testBoardID) members, err := store.GetMembersForBoard(testBoardID)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, members) require.Empty(t, members)
@ -648,6 +734,14 @@ func testGetMembersForBoard(t *testing.T, store store.Store) {
}) })
} }
func testGetMembersForUser(t *testing.T, store store.Store) {
t.Run("should return empty list if there are no memberships for a user", func(t *testing.T) {
members, err := store.GetMembersForUser(testUserID)
require.NoError(t, err)
require.Empty(t, members)
})
}
func testDeleteMember(t *testing.T, store store.Store) { func testDeleteMember(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
boardID := testBoardID boardID := testBoardID
@ -819,6 +913,14 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
} }
} }
func testSearchBoardsForUserInTeam(t *testing.T, store store.Store) {
t.Run("should return empty list if there are no resutls", func(t *testing.T) {
boards, err := store.SearchBoardsForUserInTeam("nonexistent-team-id", "", testUserID)
require.NoError(t, err)
require.Empty(t, boards)
})
}
func testUndeleteBoard(t *testing.T, store store.Store) { func testUndeleteBoard(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID

View File

@ -1,7 +1,6 @@
package storetests package storetests
import ( import (
"database/sql"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@ -399,7 +398,7 @@ func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
require.ErrorIs(t, store.DeleteBoardsAndBlocks(dbab, userID), sql.ErrNoRows) require.True(t, model.IsErrNotFound(store.DeleteBoardsAndBlocks(dbab, userID)))
// all the entities should still exist // all the entities should still exist
rBoard1, err := store.GetBoard(board1.ID) rBoard1, err := store.GetBoard(board1.ID)
@ -475,7 +474,7 @@ func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
require.ErrorIs(t, store.DeleteBoardsAndBlocks(dbab, userID), sql.ErrNoRows) require.True(t, model.IsErrNotFound(store.DeleteBoardsAndBlocks(dbab, userID)))
// all the entities should still exist // all the entities should still exist
rBoard1, err := store.GetBoard(board1.ID) rBoard1, err := store.GetBoard(board1.ID)
@ -499,7 +498,7 @@ func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) {
require.NotNil(t, rBlock4) require.NotNil(t, rBlock4)
}) })
t.Run("should not work properly if all the entities are related", func(t *testing.T) { t.Run("should work properly if all the entities are related", func(t *testing.T) {
newBoard1 := &model.Board{ newBoard1 := &model.Board{
ID: utils.NewID(utils.IDTypeBoard), ID: utils.NewID(utils.IDTypeBoard),
TeamID: teamID, TeamID: teamID,
@ -551,22 +550,28 @@ func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) {
rBoard1, err := store.GetBoard(board1.ID) rBoard1, err := store.GetBoard(board1.ID)
require.Error(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBoard1) require.Nil(t, rBoard1)
rBlock1, err := store.GetBlock(block1.ID) rBlock1, err := store.GetBlock(block1.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBlock1) require.Nil(t, rBlock1)
rBlock2, err := store.GetBlock(block2.ID) rBlock2, err := store.GetBlock(block2.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBlock2) require.Nil(t, rBlock2)
rBoard2, err := store.GetBoard(board2.ID) rBoard2, err := store.GetBoard(board2.ID)
require.Error(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBoard2) require.Nil(t, rBoard2)
rBlock3, err := store.GetBlock(block3.ID) rBlock3, err := store.GetBlock(block3.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBlock3) require.Nil(t, rBlock3)
rBlock4, err := store.GetBlock(block4.ID) rBlock4, err := store.GetBlock(block4.ID)
require.NoError(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, rBlock4) require.Nil(t, rBlock4)
}) })
} }

View File

@ -25,7 +25,7 @@ func StoreTestCategoryStore(t *testing.T, setup func(t *testing.T) (store.Store,
defer tearDown() defer tearDown()
testDeleteCategory(t, store) testDeleteCategory(t, store)
}) })
t.Run("GetUserCategoriesCategory", func(t *testing.T) { t.Run("GetUserCategories", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
testGetUserCategories(t, store) testGetUserCategories(t, store)
@ -80,6 +80,14 @@ func testGetCreateCategory(t *testing.T, store store.Store) {
assert.Equal(t, "team_id_1", createdCategory.TeamID) assert.Equal(t, "team_id_1", createdCategory.TeamID)
assert.Equal(t, true, createdCategory.Collapsed) assert.Equal(t, true, createdCategory.Collapsed)
}) })
t.Run("get nonexistent category", func(t *testing.T) {
category, err := store.GetCategory("nonexistent")
assert.Error(t, err)
var nf *model.ErrNotFound
assert.ErrorAs(t, err, &nf)
assert.Nil(t, category)
})
} }
func testUpdateCategory(t *testing.T, store store.Store) { func testUpdateCategory(t *testing.T, store store.Store) {

View File

@ -101,4 +101,10 @@ func testGetUserCategoryBoards(t *testing.T, store store.Store) {
assert.NotEmpty(t, category1BoardCategory) assert.NotEmpty(t, category1BoardCategory)
assert.Equal(t, 0, len(category3BoardCategory.BoardIDs)) assert.Equal(t, 0, len(category3BoardCategory.BoardIDs))
t.Run("get empty category boards", func(t *testing.T) {
userCategoryBoards, err := store.GetUserCategoryBoards("nonexistent-user-id", "nonexistent-team-id")
assert.NoError(t, err)
assert.Empty(t, userCategoryBoards)
})
} }

View File

@ -3,7 +3,6 @@
package storetests package storetests
import ( import (
"database/sql"
"testing" "testing"
"time" "time"
@ -124,13 +123,13 @@ func testRunDataRetention(t *testing.T, store store.Store, batchSize int) {
// GetMemberForBoard throws error on now rows found // GetMemberForBoard throws error on now rows found
member, err := store.GetMemberForBoard(boardID, testUserID) member, err := store.GetMemberForBoard(boardID, testUserID)
require.Error(t, err) require.Error(t, err)
require.Equal(t, sql.ErrNoRows, err) require.True(t, model.IsErrNotFound(err), err)
require.Nil(t, member) require.Nil(t, member)
// GetSharing throws error on now rows found // GetSharing throws error on now rows found
sharing, err := store.GetSharing(boardID) sharing, err := store.GetSharing(boardID)
require.Error(t, err) require.Error(t, err)
require.Equal(t, sql.ErrNoRows, err) require.True(t, model.IsErrNotFound(err), err)
require.Nil(t, sharing) require.Nil(t, sharing)
category, err := store.GetUserCategoryBoards(boardID, testTeamID) category, err := store.GetUserCategoryBoards(boardID, testTeamID)

View File

@ -3,10 +3,12 @@ package storetests
import ( import (
"testing" "testing"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/utils"
mmModel "github.com/mattermost/mattermost-server/v6/model" mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func StoreTestFileStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { func StoreTestFileStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
@ -24,15 +26,23 @@ func StoreTestFileStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
} }
err := sqlStore.SaveFileInfo(fileInfo) err := sqlStore.SaveFileInfo(fileInfo)
assert.NoError(t, err) require.NoError(t, err)
retrievedFileInfo, err := sqlStore.GetFileInfo("file_info_1") retrievedFileInfo, err := sqlStore.GetFileInfo("file_info_1")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "file_info_1", retrievedFileInfo.Id) require.Equal(t, "file_info_1", retrievedFileInfo.Id)
assert.Equal(t, "Dunder Mifflin Sales Report 2022", retrievedFileInfo.Name) require.Equal(t, "Dunder Mifflin Sales Report 2022", retrievedFileInfo.Name)
assert.Equal(t, ".sales", retrievedFileInfo.Extension) require.Equal(t, ".sales", retrievedFileInfo.Extension)
assert.Equal(t, int64(112233), retrievedFileInfo.Size) require.Equal(t, int64(112233), retrievedFileInfo.Size)
assert.Equal(t, int64(0), retrievedFileInfo.DeleteAt) require.Equal(t, int64(0), retrievedFileInfo.DeleteAt)
assert.False(t, retrievedFileInfo.Archived) require.False(t, retrievedFileInfo.Archived)
})
t.Run("should return an error on not found", func(t *testing.T) {
fileInfo, err := sqlStore.GetFileInfo("nonexistent")
require.Error(t, err)
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, fileInfo)
}) })
} }

View File

@ -45,6 +45,12 @@ func testCreateAndGetAndDeleteSession(t *testing.T, store store.Store) {
require.Equal(t, session, got) require.Equal(t, session, got)
}) })
t.Run("Get nonexistent session", func(t *testing.T) {
got, err := store.GetSession("nonexistent-token", 60*60)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, got)
})
t.Run("DeleteAndGetSession", func(t *testing.T) { t.Run("DeleteAndGetSession", func(t *testing.T) {
err := store.DeleteSession(session.ID) err := store.DeleteSession(session.ID)
require.NoError(t, err) require.NoError(t, err)

View File

@ -55,5 +55,6 @@ func testUpsertSharingAndGetSharing(t *testing.T, store store.Store) {
t.Run("Get not existing sharing", func(t *testing.T) { t.Run("Get not existing sharing", func(t *testing.T) {
_, err := store.GetSharing("not-existing") _, err := store.GetSharing("not-existing")
require.Error(t, err) require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
}) })
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store"
) )
//nolint:dupl
func StoreTestSubscriptionsStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { func StoreTestSubscriptionsStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
t.Run("CreateSubscription", func(t *testing.T) { t.Run("CreateSubscription", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)

View File

@ -17,6 +17,12 @@ import (
) )
func StoreTestTeamStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { func StoreTestTeamStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
t.Run("GetTeam", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetTeam(t, store)
})
t.Run("UpsertTeamSignupToken", func(t *testing.T) { t.Run("UpsertTeamSignupToken", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -36,6 +42,30 @@ func StoreTestTeamStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
}) })
} }
func testGetTeam(t *testing.T, store store.Store) {
t.Run("Nonexistent team", func(t *testing.T) {
got, err := store.GetTeam("nonexistent-id")
require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
require.Nil(t, got)
})
t.Run("Valid team", func(t *testing.T) {
teamID := "0"
team := &model.Team{
ID: teamID,
SignupToken: utils.NewID(utils.IDTypeToken),
}
err := store.UpsertTeamSignupToken(*team)
require.NoError(t, err)
got, err := store.GetTeam(teamID)
require.NoError(t, err)
require.Equal(t, teamID, got.ID)
})
}
func testUpsertTeamSignupToken(t *testing.T, store store.Store) { func testUpsertTeamSignupToken(t *testing.T, store store.Store) {
t.Run("Insert and update team with signup token", func(t *testing.T) { t.Run("Insert and update team with signup token", func(t *testing.T) {
teamID := "0" teamID := "0"
@ -100,6 +130,12 @@ func testUpsertTeamSettings(t *testing.T, store store.Store) {
} }
func testGetAllTeams(t *testing.T, store store.Store) { func testGetAllTeams(t *testing.T, store store.Store) {
t.Run("No teams response", func(t *testing.T) {
got, err := store.GetAllTeams()
require.NoError(t, err)
require.Empty(t, got)
})
t.Run("Insert multiple team and get all teams", func(t *testing.T) { t.Run("Insert multiple team and get all teams", func(t *testing.T) {
// insert // insert
teamCount := 10 teamCount := 10

View File

@ -4,6 +4,7 @@
package storetests package storetests
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -14,11 +15,12 @@ import (
"github.com/mattermost/focalboard/server/utils" "github.com/mattermost/focalboard/server/utils"
) )
//nolint:dupl
func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
t.Run("SetGetSystemSettings", func(t *testing.T) { t.Run("GetUsersByTeam", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
testGetTeamUsers(t, store) testGetUsersByTeam(t, store)
}) })
t.Run("CreateAndGetUser", func(t *testing.T) { t.Run("CreateAndGetUser", func(t *testing.T) {
@ -27,6 +29,12 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
testCreateAndGetUser(t, store) testCreateAndGetUser(t, store)
}) })
t.Run("GetUsersList", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetUsersList(t, store)
})
t.Run("CreateAndUpdateUser", func(t *testing.T) { t.Run("CreateAndUpdateUser", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -38,6 +46,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
defer tearDown() defer tearDown()
testCreateAndGetRegisteredUserCount(t, store) testCreateAndGetRegisteredUserCount(t, store)
}) })
t.Run("TestPatchUserProps", func(t *testing.T) { t.Run("TestPatchUserProps", func(t *testing.T) {
store, tearDown := setup(t) store, tearDown := setup(t)
defer tearDown() defer tearDown()
@ -45,11 +54,11 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
}) })
} }
func testGetTeamUsers(t *testing.T, store store.Store) { func testGetUsersByTeam(t *testing.T, store store.Store) {
t.Run("GetTeamUSers", func(t *testing.T) { t.Run("GetTeamUsers", func(t *testing.T) {
users, err := store.GetUsersByTeam("team_1", "") users, err := store.GetUsersByTeam("team_1", "")
require.Equal(t, 0, len(users)) require.Equal(t, 0, len(users))
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") require.NoError(t, err)
userID := utils.NewID(utils.IDTypeUser) userID := utils.NewID(utils.IDTypeUser)
@ -93,6 +102,13 @@ func testCreateAndGetUser(t *testing.T, store store.Store) {
require.Equal(t, user.Email, got.Email) require.Equal(t, user.Email, got.Email)
}) })
t.Run("GetUserByID nonexistent", func(t *testing.T) {
got, err := store.GetUserByID("nonexistent-id")
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, got)
})
t.Run("GetUserByUsername", func(t *testing.T) { t.Run("GetUserByUsername", func(t *testing.T) {
got, err := store.GetUserByUsername(user.Username) got, err := store.GetUserByUsername(user.Username)
require.NoError(t, err) require.NoError(t, err)
@ -101,6 +117,13 @@ func testCreateAndGetUser(t *testing.T, store store.Store) {
require.Equal(t, user.Email, got.Email) require.Equal(t, user.Email, got.Email)
}) })
t.Run("GetUserByUsername nonexistent", func(t *testing.T) {
got, err := store.GetUserByID("nonexistent-username")
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, got)
})
t.Run("GetUserByEmail", func(t *testing.T) { t.Run("GetUserByEmail", func(t *testing.T) {
got, err := store.GetUserByEmail(user.Email) got, err := store.GetUserByEmail(user.Email)
require.NoError(t, err) require.NoError(t, err)
@ -108,6 +131,69 @@ func testCreateAndGetUser(t *testing.T, store store.Store) {
require.Equal(t, user.Username, got.Username) require.Equal(t, user.Username, got.Username)
require.Equal(t, user.Email, got.Email) require.Equal(t, user.Email, got.Email)
}) })
t.Run("GetUserByEmail nonexistent", func(t *testing.T) {
got, err := store.GetUserByID("nonexistent-email")
var nf *model.ErrNotFound
require.ErrorAs(t, err, &nf)
require.Nil(t, got)
})
}
func testGetUsersList(t *testing.T, store store.Store) {
for _, id := range []string{"user1", "user2"} {
user := &model.User{
ID: id,
Username: fmt.Sprintf("%s-username", id),
Email: fmt.Sprintf("%s@sample.com", id),
}
err := store.CreateUser(user)
require.NoError(t, err)
}
testCases := []struct {
Name string
UserIDs []string
ExpectedError bool
ExpectedIDs []string
}{
{
Name: "all of the IDs are found",
UserIDs: []string{"user1", "user2"},
ExpectedError: false,
ExpectedIDs: []string{"user1", "user2"},
},
{
Name: "some of the IDs are found",
UserIDs: []string{"user2", "non-existent"},
ExpectedError: true,
ExpectedIDs: []string{"user2"},
},
{
Name: "none of the IDs are found",
UserIDs: []string{"non-existent-1", "non-existent-2"},
ExpectedError: true,
ExpectedIDs: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
users, err := store.GetUsersList(tc.UserIDs)
if tc.ExpectedError {
require.Error(t, err)
require.True(t, model.IsErrNotFound(err))
} else {
require.NoError(t, err)
}
userIDs := []string{}
for _, user := range users {
userIDs = append(userIDs, user.ID)
}
require.ElementsMatch(t, tc.ExpectedIDs, userIDs)
})
}
} }
func testCreateAndUpdateUser(t *testing.T, store store.Store) { func testCreateAndUpdateUser(t *testing.T, store store.Store) {

View File

@ -3,7 +3,7 @@ package webhook
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net/http" "net/http"
"github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/model"
@ -24,7 +24,7 @@ func (wh *Client) NotifyUpdate(block model.Block) {
} }
for _, url := range wh.config.WebhookUpdate { for _, url := range wh.config.WebhookUpdate {
resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
_, _ = ioutil.ReadAll(resp.Body) _, _ = io.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url)) wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url))