You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Rest API reorganization (#3624)
* reorg API * reorg api * fix system route * remove newline Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										4763
									
								
								server/api/api.go
									
									
									
									
									
								
							
							
						
						
									
										4763
									
								
								server/api/api.go
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -16,6 +16,13 @@ const ( | ||||
| 	archiveExtension = ".boardarchive" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerAchivesRoutes(r *mux.Router) { | ||||
| 	// Archive APIs | ||||
| 	r.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST") | ||||
| 	r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID}/archive/export archiveExportBoard | ||||
| 	// | ||||
| @@ -84,75 +91,6 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/archive/export archiveExportTeam | ||||
| 	// | ||||
| 	// Exports an archive of all blocks for all the boards in a team. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Id of team | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     content: | ||||
| 	//       application-octet-stream: | ||||
| 	//         type: string | ||||
| 	//         format: binary | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	if a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session, _ := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("TeamID", teamID) | ||||
|  | ||||
| 	boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ids := []string{} | ||||
| 	for _, board := range boards { | ||||
| 		ids = append(ids, board.ID) | ||||
| 	} | ||||
|  | ||||
| 	opts := model.ExportArchiveOptions{ | ||||
| 		TeamID:   teamID, | ||||
| 		BoardIDs: ids, | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) | ||||
| 	w.Header().Set("Content-Type", "application/octet-stream") | ||||
| 	w.Header().Set("Content-Disposition", "attachment; filename="+filename) | ||||
| 	w.Header().Set("Content-Transfer-Encoding", "binary") | ||||
|  | ||||
| 	if err := a.app.ExportArchive(w, opts); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 	} | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/archive/import archiveImport | ||||
| 	// | ||||
| @@ -225,3 +163,72 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/archive/export archiveExportTeam | ||||
| 	// | ||||
| 	// Exports an archive of all blocks for all the boards in a team. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Id of team | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     content: | ||||
| 	//       application-octet-stream: | ||||
| 	//         type: string | ||||
| 	//         format: binary | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	if a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session, _ := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("TeamID", teamID) | ||||
|  | ||||
| 	boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ids := []string{} | ||||
| 	for _, board := range boards { | ||||
| 		ids = append(ids, board.ID) | ||||
| 	} | ||||
|  | ||||
| 	opts := model.ExportArchiveOptions{ | ||||
| 		TeamID:   teamID, | ||||
| 		BoardIDs: ids, | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) | ||||
| 	w.Header().Set("Content-Type", "application/octet-stream") | ||||
| 	w.Header().Set("Content-Disposition", "attachment; filename="+filename) | ||||
| 	w.Header().Set("Content-Transfer-Encoding", "binary") | ||||
|  | ||||
| 	if err := a.app.ExportArchive(w, opts); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 	} | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|   | ||||
| @@ -3,8 +3,6 @@ package api | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| @@ -19,123 +17,16 @@ import ( | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	MinimumPasswordLength = 8 | ||||
| ) | ||||
|  | ||||
| type ParamError struct { | ||||
| 	msg string | ||||
| } | ||||
|  | ||||
| func (pe ParamError) Error() string { | ||||
| 	return pe.msg | ||||
| } | ||||
|  | ||||
| // LoginRequest is a login request | ||||
| // swagger:model | ||||
| type LoginRequest struct { | ||||
| 	// Type of login, currently must be set to "normal" | ||||
| 	// required: true | ||||
| 	Type string `json:"type"` | ||||
|  | ||||
| 	// If specified, login using username | ||||
| 	// required: false | ||||
| 	Username string `json:"username"` | ||||
|  | ||||
| 	// If specified, login using email | ||||
| 	// required: false | ||||
| 	Email string `json:"email"` | ||||
|  | ||||
| 	// Password | ||||
| 	// required: true | ||||
| 	Password string `json:"password"` | ||||
|  | ||||
| 	// MFA token | ||||
| 	// required: false | ||||
| 	// swagger:ignore | ||||
| 	MfaToken string `json:"mfa_token"` | ||||
| } | ||||
|  | ||||
| // LoginResponse is a login response | ||||
| // swagger:model | ||||
| type LoginResponse struct { | ||||
| 	// Session token | ||||
| 	// required: true | ||||
| 	Token string `json:"token"` | ||||
| } | ||||
|  | ||||
| func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) { | ||||
| 	var resp LoginResponse | ||||
| 	if err := json.NewDecoder(data).Decode(&resp); err != nil { | ||||
| 		return nil, err | ||||
| func (a *API) registerAuthRoutes(r *mux.Router) { | ||||
| 	// personal-server specific routes. These are not needed in plugin mode. | ||||
| 	if !a.isPlugin { | ||||
| 		r.HandleFunc("/login", a.handleLogin).Methods("POST") | ||||
| 		r.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST") | ||||
| 		r.HandleFunc("/register", a.handleRegister).Methods("POST") | ||||
| 		r.HandleFunc("/teams/{teamID}/regenerate_signup_token", a.sessionRequired(a.handlePostTeamRegenerateSignupToken)).Methods("POST") | ||||
| 	} | ||||
| 	return &resp, nil | ||||
| } | ||||
|  | ||||
| // RegisterRequest is a user registration request | ||||
| // swagger:model | ||||
| type RegisterRequest struct { | ||||
| 	// User name | ||||
| 	// required: true | ||||
| 	Username string `json:"username"` | ||||
|  | ||||
| 	// User's email | ||||
| 	// required: true | ||||
| 	Email string `json:"email"` | ||||
|  | ||||
| 	// Password | ||||
| 	// required: true | ||||
| 	Password string `json:"password"` | ||||
|  | ||||
| 	// Registration authorization token | ||||
| 	// required: true | ||||
| 	Token string `json:"token"` | ||||
| } | ||||
|  | ||||
| func (rd *RegisterRequest) IsValid() error { | ||||
| 	if strings.TrimSpace(rd.Username) == "" { | ||||
| 		return ParamError{"username is required"} | ||||
| 	} | ||||
| 	if strings.TrimSpace(rd.Email) == "" { | ||||
| 		return ParamError{"email is required"} | ||||
| 	} | ||||
| 	if !auth.IsEmailValid(rd.Email) { | ||||
| 		return ParamError{"invalid email format"} | ||||
| 	} | ||||
| 	if rd.Password == "" { | ||||
| 		return ParamError{"password is required"} | ||||
| 	} | ||||
| 	return isValidPassword(rd.Password) | ||||
| } | ||||
|  | ||||
| // ChangePasswordRequest is a user password change request | ||||
| // swagger:model | ||||
| type ChangePasswordRequest struct { | ||||
| 	// Old password | ||||
| 	// required: true | ||||
| 	OldPassword string `json:"oldPassword"` | ||||
|  | ||||
| 	// New password | ||||
| 	// required: true | ||||
| 	NewPassword string `json:"newPassword"` | ||||
| } | ||||
|  | ||||
| // IsValid validates a password change request. | ||||
| func (rd *ChangePasswordRequest) IsValid() error { | ||||
| 	if rd.OldPassword == "" { | ||||
| 		return ParamError{"old password is required"} | ||||
| 	} | ||||
| 	if rd.NewPassword == "" { | ||||
| 		return ParamError{"new password is required"} | ||||
| 	} | ||||
| 	return isValidPassword(rd.NewPassword) | ||||
| } | ||||
|  | ||||
| func isValidPassword(password string) error { | ||||
| 	if len(password) < MinimumPasswordLength { | ||||
| 		return ParamError{fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength)} | ||||
| 	} | ||||
| 	return nil | ||||
| 	r.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") | ||||
| } | ||||
|  | ||||
| func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { | ||||
| @@ -187,7 +78,7 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var loginData LoginRequest | ||||
| 	var loginData model.LoginRequest | ||||
| 	err = json.Unmarshal(requestBody, &loginData) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| @@ -205,7 +96,7 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "incorrect login", err) | ||||
| 			return | ||||
| 		} | ||||
| 		json, err := json.Marshal(LoginResponse{Token: token}) | ||||
| 		json, err := json.Marshal(model.LoginResponse{Token: token}) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| @@ -315,7 +206,7 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var registerData RegisterRequest | ||||
| 	var registerData model.RegisterRequest | ||||
| 	err = json.Unmarshal(requestBody, ®isterData) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| @@ -425,7 +316,7 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var requestData ChangePasswordRequest | ||||
| 	var requestData model.ChangePasswordRequest | ||||
| 	if err = json.Unmarshal(requestBody, &requestData); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
|   | ||||
							
								
								
									
										758
									
								
								server/api/blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										758
									
								
								server/api/blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,758 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerBlocksRoutes(r *mux.Router) { | ||||
| 	// Blocks APIs | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks", a.attachSession(a.handleGetBlocks, false)).Methods("GET") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID}/blocks getBlocks | ||||
| 	// | ||||
| 	// Returns blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: parent_id | ||||
| 	//   in: query | ||||
| 	//   description: ID of parent block, omit to specify all blocks | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// - name: type | ||||
| 	//   in: query | ||||
| 	//   description: Type of blocks to return, omit to specify all types | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Block" | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	query := r.URL.Query() | ||||
| 	parentID := query.Get("parent_id") | ||||
| 	blockType := query.Get("type") | ||||
| 	all := query.Get("all") | ||||
| 	blockID := query.Get("block_id") | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) | ||||
| 	if userID == "" && !hasValidReadToken { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "Board not found", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !hasValidReadToken { | ||||
| 		if board.IsTemplate && board.Type == model.BoardTypeOpen { | ||||
| 			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"}) | ||||
| 				return | ||||
| 			} | ||||
| 		} else { | ||||
| 			if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 				a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("parentID", parentID) | ||||
| 	auditRec.AddMeta("blockType", blockType) | ||||
| 	auditRec.AddMeta("all", all) | ||||
| 	auditRec.AddMeta("blockID", blockID) | ||||
|  | ||||
| 	var blocks []model.Block | ||||
| 	var block *model.Block | ||||
| 	switch { | ||||
| 	case all != "": | ||||
| 		blocks, err = a.app.GetBlocksForBoard(boardID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 	case blockID != "": | ||||
| 		block, err = a.app.GetBlockByID(blockID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if block != nil { | ||||
| 			if block.BoardID != boardID { | ||||
| 				a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			blocks = append(blocks, *block) | ||||
| 		} | ||||
| 	default: | ||||
| 		blocks, err = a.app.GetBlocks(boardID, parentID, blockType) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetBlocks", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("parentID", parentID), | ||||
| 		mlog.String("blockType", blockType), | ||||
| 		mlog.String("blockID", blockID), | ||||
| 		mlog.Int("block_count", len(blocks)), | ||||
| 	) | ||||
|  | ||||
| 	var bErr error | ||||
| 	blocks, bErr = a.app.ApplyCloudLimits(blocks) | ||||
| 	if bErr != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", bErr) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	json, err := json.Marshal(blocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, json) | ||||
|  | ||||
| 	auditRec.AddMeta("blockCount", len(blocks)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/blocks updateBlocks | ||||
| 	// | ||||
| 	// Insert blocks. The specified IDs will only be used to link | ||||
| 	// blocks with existing ones, the rest will be replaced by server | ||||
| 	// generated IDs | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: disable_notify | ||||
| 	//   in: query | ||||
| 	//   description: Disables notifications (for bulk data inserting) | ||||
| 	//   required: false | ||||
| 	//   type: bool | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: array of blocks to insert or update | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     type: array | ||||
| 	//     items: | ||||
| 	//       "$ref": "#/definitions/Block" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       items: | ||||
| 	//         $ref: '#/definitions/Block' | ||||
| 	//       type: array | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	val := r.URL.Query().Get("disable_notify") | ||||
| 	disableNotify := val == True | ||||
|  | ||||
| 	// in phase 1 we use "manage_board_cards", but we would have to | ||||
| 	// check on specific actions for phase 2 | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var blocks []model.Block | ||||
|  | ||||
| 	err = json.Unmarshal(requestBody, &blocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, block := range blocks { | ||||
| 		// Error checking | ||||
| 		if len(block.Type) < 1 { | ||||
| 			message := fmt.Sprintf("missing type for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if block.CreateAt < 1 { | ||||
| 			message := fmt.Sprintf("invalid createAt for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if block.UpdateAt < 1 { | ||||
| 			message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if block.BoardID != boardID { | ||||
| 			message := fmt.Sprintf("invalid BoardID for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	blocks = model.GenerateBlockIDs(blocks, a.logger) | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("disable_notify", disableNotify) | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	model.StampModificationMetadata(userID, blocks, auditRec) | ||||
|  | ||||
| 	// this query param exists when creating template from board, or board from template | ||||
| 	sourceBoardID := r.URL.Query().Get("sourceBoardID") | ||||
| 	if sourceBoardID != "" { | ||||
| 		if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", updateFileIDsErr) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, !disableNotify) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, app.ErrViewsLimitReached) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) | ||||
| 		} else { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		} | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("POST Blocks", | ||||
| 		mlog.Int("block_count", len(blocks)), | ||||
| 		mlog.Bool("disable_notify", disableNotify), | ||||
| 	) | ||||
|  | ||||
| 	json, err := json.Marshal(newBlocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, json) | ||||
|  | ||||
| 	auditRec.AddMeta("blockCount", len(blocks)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /boards/{boardID}/blocks/{blockID} deleteBlock | ||||
| 	// | ||||
| 	// Deletes a block | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: blockID | ||||
| 	//   in: path | ||||
| 	//   description: ID of block to delete | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: block not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
| 	blockID := vars["blockID"] | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	block, err := a.app.GetBlockByID(blockID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if block == nil || block.BoardID != boardID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("blockID", blockID) | ||||
|  | ||||
| 	err = a.app.DeleteBlock(blockID, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("DELETE Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/blocks/{blockID}/undelete undeleteBlock | ||||
| 	// | ||||
| 	// Undeletes a block | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: blockID | ||||
| 	//   in: path | ||||
| 	//   description: ID of block to undelete | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/BlockPatch" | ||||
| 	//   '404': | ||||
| 	//     description: block not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	blockID := vars["blockID"] | ||||
| 	boardID := vars["boardID"] | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	block, err := a.app.GetLastBlockHistoryEntry(blockID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if block == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if board.ID != block.BoardID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("blockID", blockID) | ||||
|  | ||||
| 	undeletedBlock, err := a.app.UndeleteBlock(blockID, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	undeletedBlockData, err := json.Marshal(undeletedBlock) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID)) | ||||
| 	jsonBytesResponse(w, http.StatusOK, undeletedBlockData) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PATCH /boards/{boardID}/blocks/{blockID} patchBlock | ||||
| 	// | ||||
| 	// Partially updates a block | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: blockID | ||||
| 	//   in: path | ||||
| 	//   description: ID of block to patch | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: block patch to apply | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BlockPatch" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: block not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
| 	blockID := vars["blockID"] | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	block, err := a.app.GetBlockByID(blockID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if block == nil || block.BoardID != boardID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var patch *model.BlockPatch | ||||
| 	err = json.Unmarshal(requestBody, &patch) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("blockID", blockID) | ||||
|  | ||||
| 	err = a.app.PatchBlock(blockID, patch, userID) | ||||
| 	if errors.Is(err, app.ErrPatchUpdatesLimitedCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("PATCH Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PATCH /boards/{boardID}/blocks/ patchBlocks | ||||
| 	// | ||||
| 	// Partially updates batch of blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Workspace ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: block Ids and block patches to apply | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BlockPatchBatch" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var patches *model.BlockPatchBatch | ||||
| 	err = json.Unmarshal(requestBody, &patches) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "patchBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	for i := range patches.BlockIDs { | ||||
| 		auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i]) | ||||
| 	} | ||||
|  | ||||
| 	for _, blockID := range patches.BlockIDs { | ||||
| 		var block *model.Block | ||||
| 		block, err = a.app.GetBlockByID(blockID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 			return | ||||
| 		} | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = a.app.PatchBlocks(teamID, patches, userID) | ||||
| 	if errors.Is(err, app.ErrPatchUpdatesLimitedCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("PATCH Blocks", mlog.String("patches", strconv.Itoa(len(patches.BlockIDs)))) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/blocks/{blockID}/duplicate duplicateBlock | ||||
| 	// | ||||
| 	// Returns the new created blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: blockID | ||||
| 	//   in: path | ||||
| 	//   description: Block ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Block" | ||||
| 	//   '404': | ||||
| 	//     description: board or block not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	blockID := mux.Vars(r)["blockID"] | ||||
| 	userID := getUserID(r) | ||||
| 	query := r.URL.Query() | ||||
| 	asTemplate := query.Get("asTemplate") | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 	} | ||||
|  | ||||
| 	if userID == "" { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	block, err := a.app.GetBlockByID(blockID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if block == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if board.ID != block.BoardID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("blockID", blockID) | ||||
|  | ||||
| 	a.logger.Debug("DuplicateBlock", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("blockID", blockID), | ||||
| 	) | ||||
|  | ||||
| 	blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(blocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										656
									
								
								server/api/boards.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										656
									
								
								server/api/boards.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,656 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerBoardsRoutes(r *mux.Router) { | ||||
| 	r.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET") | ||||
| 	r.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET") | ||||
| 	r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH") | ||||
| 	r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE") | ||||
| 	r.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/undelete", a.sessionRequired(a.handleUndeleteBoard)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/boards getBoards | ||||
| 	// | ||||
| 	// Returns team boards | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Board" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getBoards", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
|  | ||||
| 	// retrieve boards list | ||||
| 	boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetBoards", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.Int("boardsCount", len(boards)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(boards) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("boardsCount", len(boards)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards createBoard | ||||
| 	// | ||||
| 	// Creates a new board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: the board to create | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/Board" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/Board' | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var newBoard *model.Board | ||||
| 	if err = json.Unmarshal(requestBody, &newBoard); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if newBoard.Type == model.BoardTypeOpen { | ||||
| 		if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err = newBoard.IsValid(); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "createBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("teamID", newBoard.TeamID) | ||||
| 	auditRec.AddMeta("boardType", newBoard.Type) | ||||
|  | ||||
| 	// create board | ||||
| 	board, err := a.app.CreateBoard(newBoard, userID, true) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("CreateBoard", | ||||
| 		mlog.String("teamID", board.TeamID), | ||||
| 		mlog.String("boardID", board.ID), | ||||
| 		mlog.String("boardType", string(board.Type)), | ||||
| 		mlog.String("userID", userID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(board) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID} getBoard | ||||
| 	// | ||||
| 	// Returns a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/Board" | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) | ||||
| 	if userID == "" && !hasValidReadToken { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !hasValidReadToken { | ||||
| 		if board.Type == model.BoardTypePrivate { | ||||
| 			if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 				a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 				return | ||||
| 			} | ||||
| 		} else { | ||||
| 			if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| 				a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	a.logger.Debug("GetBoard", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(board) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PATCH /boards/{boardID} patchBoard | ||||
| 	// | ||||
| 	// Partially updates a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: board patch to apply | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BoardPatch" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/Board' | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var patch *model.BoardPatch | ||||
| 	if err = json.Unmarshal(requestBody, &patch); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err = patch.IsValid(); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if patch.Type != nil || patch.MinimumRole != nil { | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	if patch.ChannelID != nil { | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board access"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "patchBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("userID", userID) | ||||
|  | ||||
| 	// patch board | ||||
| 	updatedBoard, err := a.app.PatchBoard(patch, boardID, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("PatchBoard", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("userID", userID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(updatedBoard) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /boards/{boardID} deleteBoard | ||||
| 	// | ||||
| 	// Removes a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	// Check if board exists | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	if err := a.app.DeleteBoard(boardID, userID); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("DELETE Board", mlog.String("boardID", boardID)) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/duplicate duplicateBoard | ||||
| 	// | ||||
| 	// Returns the new created board and all the blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardsAndBlocks' | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
| 	query := r.URL.Query() | ||||
| 	asTemplate := query.Get("asTemplate") | ||||
| 	toTeam := query.Get("toTeam") | ||||
|  | ||||
| 	if userID == "" { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if board.IsTemplate && board.Type == model.BoardTypeOpen { | ||||
| 		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"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	a.logger.Debug("DuplicateBoard", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 	) | ||||
|  | ||||
| 	boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(boardsAndBlocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUndeleteBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/undelete undeleteBoard | ||||
| 	// | ||||
| 	// Undeletes a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: ID of board to undelete | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "undeleteBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to undelete board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := a.app.UndeleteBoard(boardID, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("UNDELETE Board", mlog.String("boardID", boardID)) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID}/metadata getBoardMetadata | ||||
| 	// | ||||
| 	// Returns a board's metadata | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/BoardMetadata" | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   '501': | ||||
| 	//     description: required license not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	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 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil || boardMetadata == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if board.Type == model.BoardTypePrivate { | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getBoardMetadata", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	data, err := json.Marshal(boardMetadata) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										426
									
								
								server/api/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										426
									
								
								server/api/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,426 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerBoardsAndBlocksRoutes(r *mux.Router) { | ||||
| 	// BoardsAndBlocks APIs | ||||
| 	r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleCreateBoardsAndBlocks)).Methods("POST") | ||||
| 	r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handlePatchBoardsAndBlocks)).Methods("PATCH") | ||||
| 	r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleDeleteBoardsAndBlocks)).Methods("DELETE") | ||||
| } | ||||
|  | ||||
| func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards-and-blocks insertBoardsAndBlocks | ||||
| 	// | ||||
| 	// Creates new boards and blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: the boards and blocks to create | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BoardsAndBlocks" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardsAndBlocks' | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var newBab *model.BoardsAndBlocks | ||||
| 	if err = json.Unmarshal(requestBody, &newBab); err != nil { | ||||
| 		// a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(newBab.Boards) == 0 { | ||||
| 		message := "at least one board is required" | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID := "" | ||||
| 	boardIDs := map[string]bool{} | ||||
| 	for _, board := range newBab.Boards { | ||||
| 		boardIDs[board.ID] = true | ||||
|  | ||||
| 		if teamID == "" { | ||||
| 			teamID = board.TeamID | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if teamID != board.TeamID { | ||||
| 			message := "cannot create boards for multiple teams" | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if board.ID == "" { | ||||
| 			message := "boards need an ID to be referenced from the blocks" | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, block := range newBab.Blocks { | ||||
| 		// Error checking | ||||
| 		if len(block.Type) < 1 { | ||||
| 			message := fmt.Sprintf("missing type for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if block.CreateAt < 1 { | ||||
| 			message := fmt.Sprintf("invalid createAt for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if block.UpdateAt < 1 { | ||||
| 			message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !boardIDs[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) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// IDs of boards and blocks are used to confirm that they're | ||||
| 	// linked and then regenerated by the server | ||||
| 	newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
| 	auditRec.AddMeta("userID", userID) | ||||
| 	auditRec.AddMeta("boardsCount", len(newBab.Boards)) | ||||
| 	auditRec.AddMeta("blocksCount", len(newBab.Blocks)) | ||||
|  | ||||
| 	// create boards and blocks | ||||
| 	bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("CreateBoardsAndBlocks", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.String("userID", userID), | ||||
| 		mlog.Int("boardCount", len(bab.Boards)), | ||||
| 		mlog.Int("blockCount", len(bab.Blocks)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(bab) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PATCH /boards-and-blocks patchBoardsAndBlocks | ||||
| 	// | ||||
| 	// Patches a set of related boards and blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: the patches for the boards and blocks | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/PatchBoardsAndBlocks" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardsAndBlocks' | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var pbab *model.PatchBoardsAndBlocks | ||||
| 	if err = json.Unmarshal(requestBody, &pbab); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err = pbab.IsValid(); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID := "" | ||||
| 	boardIDMap := map[string]bool{} | ||||
| 	for i, boardID := range pbab.BoardIDs { | ||||
| 		boardIDMap[boardID] = true | ||||
| 		patch := pbab.BoardPatches[i] | ||||
|  | ||||
| 		if err = patch.IsValid(); err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board properties"}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if patch.Type != nil || patch.MinimumRole != nil { | ||||
| 			if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { | ||||
| 				a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		board, err2 := a.app.GetBoard(boardID) | ||||
| 		if err2 != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) | ||||
| 			return | ||||
| 		} | ||||
| 		if board == nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if teamID == "" { | ||||
| 			teamID = board.TeamID | ||||
| 		} | ||||
| 		if teamID != board.TeamID { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, blockID := range pbab.BlockIDs { | ||||
| 		block, err2 := a.app.GetBlockByID(blockID) | ||||
| 		if err2 != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) | ||||
| 			return | ||||
| 		} | ||||
| 		if block == nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if _, ok := boardIDMap[block.BoardID]; !ok { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardsCount", len(pbab.BoardIDs)) | ||||
| 	auditRec.AddMeta("blocksCount", len(pbab.BlockIDs)) | ||||
|  | ||||
| 	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 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("PATCH BoardsAndBlocks", | ||||
| 		mlog.Int("boardsCount", len(pbab.BoardIDs)), | ||||
| 		mlog.Int("blocksCount", len(pbab.BlockIDs)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(bab) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /boards-and-blocks deleteBoardsAndBlocks | ||||
| 	// | ||||
| 	// Deletes boards and blocks | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: the boards and blocks to delete | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/DeleteBoardsAndBlocks" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var dbab *model.DeleteBoardsAndBlocks | ||||
| 	if err = json.Unmarshal(requestBody, &dbab); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// user must have permission to delete all the boards, and that | ||||
| 	// would include the permission to manage their blocks | ||||
| 	teamID := "" | ||||
| 	boardIDMap := map[string]bool{} | ||||
| 	for _, boardID := range dbab.Boards { | ||||
| 		boardIDMap[boardID] = true | ||||
| 		// all boards in the request should belong to the same team | ||||
| 		board, err := a.app.GetBoard(boardID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if board == nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if teamID == "" { | ||||
| 			teamID = board.TeamID | ||||
| 		} | ||||
| 		if teamID != board.TeamID { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// permission check | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, blockID := range dbab.Blocks { | ||||
| 		block, err2 := a.app.GetBlockByID(blockID) | ||||
| 		if err2 != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) | ||||
| 			return | ||||
| 		} | ||||
| 		if block == nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if _, ok := boardIDMap[block.BoardID]; !ok { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := dbab.IsValid(); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteBoardsAndBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardsCount", len(dbab.Boards)) | ||||
| 	auditRec.AddMeta("blocksCount", len(dbab.Blocks)) | ||||
|  | ||||
| 	if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("DELETE BoardsAndBlocks", | ||||
| 		mlog.Int("boardsCount", len(dbab.Boards)), | ||||
| 		mlog.Int("blocksCount", len(dbab.Blocks)), | ||||
| 	) | ||||
|  | ||||
| 	// response | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										401
									
								
								server/api/categories.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								server/api/categories.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,401 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerCategoriesRoutes(r *mux.Router) { | ||||
| 	// Category APIs | ||||
| 	r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleCreateCategory)).Methods(http.MethodPost) | ||||
| 	r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleUpdateCategory)).Methods(http.MethodPut) | ||||
| 	r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleDeleteCategory)).Methods(http.MethodDelete) | ||||
| 	r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBoards)).Methods(http.MethodGet) | ||||
| 	r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}", a.sessionRequired(a.handleUpdateCategoryBoard)).Methods(http.MethodPost) | ||||
| } | ||||
|  | ||||
| func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/categories createCategory | ||||
| 	// | ||||
| 	// Create a category for boards | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: category to create | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/Category" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/Category" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var category model.Category | ||||
|  | ||||
| 	err = json.Unmarshal(requestBody, &category) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "createCategory", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	// user can only create category for themselves | ||||
| 	if category.UserID != session.UserID { | ||||
| 		a.errorResponse( | ||||
| 			w, | ||||
| 			r.URL.Path, | ||||
| 			http.StatusBadRequest, | ||||
| 			fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID), | ||||
| 			nil, | ||||
| 		) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	if category.TeamID != teamID { | ||||
| 		a.errorResponse( | ||||
| 			w, | ||||
| 			r.URL.Path, | ||||
| 			http.StatusBadRequest, | ||||
| 			"teamID mismatch", | ||||
| 			nil, | ||||
| 		) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	createdCategory, err := a.app.CreateCategory(&category) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(createdCategory) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.AddMeta("categoryID", createdCategory.ID) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PUT /teams/{teamID}/categories/{categoryID} updateCategory | ||||
| 	// | ||||
| 	// Create a category for boards | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: categoryID | ||||
| 	//   in: path | ||||
| 	//   description: Category ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: category to update | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/Category" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/Category" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	categoryID := vars["categoryID"] | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var category model.Category | ||||
| 	err = json.Unmarshal(requestBody, &category) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "updateCategory", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	if categoryID != category.ID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "categoryID mismatch in patch and body", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	// user can only update category for themselves | ||||
| 	if category.UserID != session.UserID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "user ID mismatch in session and category", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID := vars["teamID"] | ||||
| 	if category.TeamID != teamID { | ||||
| 		a.errorResponse( | ||||
| 			w, | ||||
| 			r.URL.Path, | ||||
| 			http.StatusBadRequest, | ||||
| 			"teamID mismatch", | ||||
| 			nil, | ||||
| 		) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	updatedCategory, err := a.app.UpdateCategory(&category) | ||||
| 	if err != nil { | ||||
| 		switch { | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(updatedCategory) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /teams/{teamID}/categories/{categoryID} deleteCategory | ||||
| 	// | ||||
| 	// Delete a category | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: categoryID | ||||
| 	//   in: path | ||||
| 	//   description: Category ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	vars := mux.Vars(r) | ||||
|  | ||||
| 	userID := session.UserID | ||||
| 	teamID := vars["teamID"] | ||||
| 	categoryID := vars["categoryID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteCategory", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID) | ||||
| 	if err != nil { | ||||
| 		switch { | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(deletedCategory) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetUserCategoryBoards(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/categories getUserCategoryBoards | ||||
| 	// | ||||
| 	// Gets the user's board categories | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/CategoryBoards" | ||||
| 	//       type: array | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getUserCategoryBoards", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(categoryBlocks) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID} updateCategoryBoard | ||||
| 	// | ||||
| 	// Set the category of a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: categoryID | ||||
| 	//   in: path | ||||
| 	//   description: Category ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "updateCategoryBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	categoryID := vars["categoryID"] | ||||
| 	boardID := vars["boardID"] | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	// TODO: Check the category and the team matches | ||||
| 	err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, []byte("ok")) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										104
									
								
								server/api/channels.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								server/api/channels.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	mm_model "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerChannelsRoutes(r *mux.Router) { | ||||
| 	r.HandleFunc("/teams/{teamID}/channels/{channelID}", a.sessionRequired(a.handleGetChannel)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/channels/{channelID} getChannel | ||||
| 	// | ||||
| 	// Returns the requested channel | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: channelID | ||||
| 	//   in: path | ||||
| 	//   description: Channel ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Channel" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	channelID := mux.Vars(r)["channelID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to channel"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getChannel", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
| 	auditRec.AddMeta("channelID", teamID) | ||||
|  | ||||
| 	channel, err := a.app.GetChannel(teamID, channelID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetChannel", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.String("channelID", channelID), | ||||
| 	) | ||||
|  | ||||
| 	if channel.TeamId != teamID { | ||||
| 		if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(channel) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										41
									
								
								server/api/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								server/api/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerConfigRoutes(r *mux.Router) { | ||||
| 	// Config APIs | ||||
| 	r.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /clientConfig getClientConfig | ||||
| 	// | ||||
| 	// Returns the client configuration | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ClientConfig" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	clientConfig := a.app.GetClientConfig() | ||||
|  | ||||
| 	configData, err := json.Marshal(clientConfig) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	jsonBytesResponse(w, http.StatusOK, configData) | ||||
| } | ||||
							
								
								
									
										259
									
								
								server/api/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								server/api/files.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| // FileUploadResponse is the response to a file upload | ||||
| // swagger:model | ||||
| type FileUploadResponse struct { | ||||
| 	// The FileID to retrieve the uploaded file | ||||
| 	// required: true | ||||
| 	FileID string `json:"fileId"` | ||||
| } | ||||
|  | ||||
| func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) { | ||||
| 	var fileUploadResponse FileUploadResponse | ||||
|  | ||||
| 	if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &fileUploadResponse, nil | ||||
| } | ||||
|  | ||||
| func (a *API) registerFilesRoutes(r *mux.Router) { | ||||
| 	// Files API | ||||
| 	r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") | ||||
| } | ||||
|  | ||||
| func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile | ||||
| 	// | ||||
| 	// Returns the contents of an uploaded file | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// - image/jpg | ||||
| 	// - image/png | ||||
| 	// - image/gif | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: filename | ||||
| 	//   in: path | ||||
| 	//   description: name of the file | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: file not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
| 	filename := vars["filename"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) | ||||
| 	if userID == "" && !hasValidReadToken { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getFile", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("teamID", board.TeamID) | ||||
| 	auditRec.AddMeta("filename", filename) | ||||
|  | ||||
| 	contentType := "image/jpg" | ||||
|  | ||||
| 	fileExtension := strings.ToLower(filepath.Ext(filename)) | ||||
| 	if fileExtension == "png" { | ||||
| 		contentType = "image/png" | ||||
| 	} | ||||
|  | ||||
| 	if fileExtension == "gif" { | ||||
| 		contentType = "image/gif" | ||||
| 	} | ||||
|  | ||||
| 	w.Header().Set("Content-Type", contentType) | ||||
|  | ||||
| 	fileInfo, err := a.app.GetFileInfo(filename) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if fileInfo != nil && fileInfo.Archived { | ||||
| 		fileMetadata := map[string]interface{}{ | ||||
| 			"archived":  true, | ||||
| 			"name":      fileInfo.Name, | ||||
| 			"size":      fileInfo.Size, | ||||
| 			"extension": fileInfo.Extension, | ||||
| 		} | ||||
|  | ||||
| 		data, jsonErr := json.Marshal(fileMetadata) | ||||
| 		if jsonErr != nil { | ||||
| 			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) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		jsonBytesResponse(w, http.StatusBadRequest, data) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer fileReader.Close() | ||||
| 	http.ServeContent(w, r, filename, time.Now(), fileReader) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/boards/{boardID}/files uploadFile | ||||
| 	// | ||||
| 	// Upload a binary file, attached to a root block | ||||
| 	// | ||||
| 	// --- | ||||
| 	// consumes: | ||||
| 	// - multipart/form-data | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: ID of the team | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: uploaded file | ||||
| 	//   in: formData | ||||
| 	//   type: file | ||||
| 	//   description: The file to upload | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/FileUploadResponse" | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if a.app.GetConfig().MaxFileSize > 0 { | ||||
| 		r.Body = http.MaxBytesReader(w, r.Body, a.app.GetConfig().MaxFileSize) | ||||
| 	} | ||||
|  | ||||
| 	file, handle, err := r.FormFile(UploadFormFileKey) | ||||
| 	if err != nil { | ||||
| 		if strings.HasSuffix(err.Error(), "http: request body too large") { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusRequestEntityTooLarge, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("teamID", board.TeamID) | ||||
| 	auditRec.AddMeta("filename", handle.Filename) | ||||
|  | ||||
| 	fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("uploadFile", | ||||
| 		mlog.String("filename", handle.Filename), | ||||
| 		mlog.String("fileID", fileID), | ||||
| 	) | ||||
| 	data, err := json.Marshal(FileUploadResponse{FileID: fileID}) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("fileID", fileID) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										242
									
								
								server/api/insights.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								server/api/insights.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,242 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerInsightsRoutes(r *mux.Router) { | ||||
| 	// Insights APIs | ||||
| 	r.HandleFunc("/teams/{teamID}/boards/insights", a.sessionRequired(a.handleTeamBoardsInsights)).Methods("GET") | ||||
| 	r.HandleFunc("/users/me/boards/insights", a.sessionRequired(a.handleUserBoardsInsights)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/boards/insights handleTeamBoardsInsights | ||||
| 	// | ||||
| 	// Returns team boards insights | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: time_range | ||||
| 	//   in: query | ||||
| 	//   description: duration of data to calculate insights for | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: page | ||||
| 	//   in: query | ||||
| 	//   description: page offset for top boards | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: per_page | ||||
| 	//   in: query | ||||
| 	//   description: limit for boards in a page. | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/BoardInsight" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
| 	userID := getUserID(r) | ||||
| 	query := r.URL.Query() | ||||
| 	timeRange := query.Get("time_range") | ||||
|  | ||||
| 	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"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getTeamBoardsInsights", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	page, err := strconv.Atoi(query.Get("page")) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting page parameter to integer", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if page < 0 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) | ||||
| 	} | ||||
|  | ||||
| 	perPage, err := strconv.Atoi(query.Get("per_page")) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting per_page parameter to integer", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if perPage < 0 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) | ||||
| 	} | ||||
|  | ||||
| 	userTimezone, aErr := a.app.GetUserTimezone(userID) | ||||
| 	if aErr != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Error getting time zone of user", aErr) | ||||
| 		return | ||||
| 	} | ||||
| 	userLocation, _ := time.LoadLocation(userTimezone) | ||||
| 	if userLocation == nil { | ||||
| 		userLocation = time.Now().UTC().Location() | ||||
| 	} | ||||
| 	// get unix time for duration | ||||
| 	startTime := mmModel.StartOfDayForTimeRange(timeRange, userLocation) | ||||
| 	boardsInsights, err := a.app.GetTeamBoardsInsights(userID, teamID, &mmModel.InsightsOpts{ | ||||
| 		StartUnixMilli: mmModel.GetMillisForTime(*startTime), | ||||
| 		Page:           page, | ||||
| 		PerPage:        perPage, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "time_range="+timeRange, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(boardsInsights) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("teamBoardsInsightCount", len(boardsInsights.Items)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /users/me/boards/insights getUserBoardsInsights | ||||
| 	// | ||||
| 	// Returns user boards insights | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: time_range | ||||
| 	//   in: query | ||||
| 	//   description: duration of data to calculate insights for | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: page | ||||
| 	//   in: query | ||||
| 	//   description: page offset for top boards | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: per_page | ||||
| 	//   in: query | ||||
| 	//   description: limit for boards in a page. | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/BoardInsight" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	query := r.URL.Query() | ||||
| 	teamID := query.Get("team_id") | ||||
| 	timeRange := query.Get("time_range") | ||||
|  | ||||
| 	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"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getUserBoardsInsights", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	page, err := strconv.Atoi(query.Get("page")) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting page parameter to integer", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if page < 0 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) | ||||
| 	} | ||||
| 	perPage, err := strconv.Atoi(query.Get("per_page")) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "error converting per_page parameter to integer", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if perPage < 0 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Invalid page parameter", nil) | ||||
| 	} | ||||
| 	userTimezone, aErr := a.app.GetUserTimezone(userID) | ||||
| 	if aErr != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "Error getting time zone of user", aErr) | ||||
| 		return | ||||
| 	} | ||||
| 	userLocation, _ := time.LoadLocation(userTimezone) | ||||
| 	if userLocation == nil { | ||||
| 		userLocation = time.Now().UTC().Location() | ||||
| 	} | ||||
| 	// get unix time for duration | ||||
| 	startTime := mmModel.StartOfDayForTimeRange(timeRange, userLocation) | ||||
| 	boardsInsights, err := a.app.GetUserBoardsInsights(userID, teamID, &mmModel.InsightsOpts{ | ||||
| 		StartUnixMilli: mmModel.GetMillisForTime(*startTime), | ||||
| 		Page:           page, | ||||
| 		PerPage:        perPage, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "time_range="+timeRange, err) | ||||
| 		return | ||||
| 	} | ||||
| 	data, err := json.Marshal(boardsInsights) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("userBoardInsightCount", len(boardsInsights.Items)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										80
									
								
								server/api/limits.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/api/limits.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerLimitsRoutes(r *mux.Router) { | ||||
| 	// limits | ||||
| 	r.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/notifyadminupgrade", a.sessionRequired(a.handleNotifyAdminUpgrade)).Methods(http.MethodPost) | ||||
| } | ||||
|  | ||||
| func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /limits cloudLimits | ||||
| 	// | ||||
| 	// Fetches the cloud limits of the server. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//         "$ref": "#/definitions/BoardsCloudLimits" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardsCloudLimits, err := a.app.GetBoardsCloudLimits() | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(boardsCloudLimits) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| } | ||||
|  | ||||
| func (a *API) handleNotifyAdminUpgrade(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /api/v2/teams/{teamID}/notifyadminupgrade handleNotifyAdminUpgrade | ||||
| 	// | ||||
| 	// Notifies admins for upgrade request. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", errAPINotSupportedInStandaloneMode) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	if err := a.app.NotifyPortalAdminsUpgradeRequest(teamID); err != nil { | ||||
| 		jsonStringResponse(w, http.StatusOK, "{}") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										535
									
								
								server/api/members.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										535
									
								
								server/api/members.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,535 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerMembersRoutes(r *mux.Router) { | ||||
| 	// Member APIs | ||||
| 	r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET") | ||||
| 	r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT") | ||||
| 	r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE") | ||||
| 	r.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID}/members getMembersForBoard | ||||
| 	// | ||||
| 	// Returns the members of the board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/BoardMember" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getMembersForBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	members, err := a.app.GetMembersForBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetMembersForBoard", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.Int("membersCount", len(members)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(members) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/members addMember | ||||
| 	// | ||||
| 	// Adds a new member to a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: membership to replace the current one with | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BoardMember" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardMember' | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var reqBoardMember *model.BoardMember | ||||
| 	if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if reqBoardMember.UserID == "" { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// currently all memberships are created as editors by default | ||||
| 	newBoardMember := &model.BoardMember{ | ||||
| 		UserID:       reqBoardMember.UserID, | ||||
| 		BoardID:      boardID, | ||||
| 		SchemeEditor: true, | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "addMember", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("addedUserID", reqBoardMember.UserID) | ||||
|  | ||||
| 	member, err := a.app.AddMemberToBoard(newBoardMember) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("AddMember", | ||||
| 		mlog.String("boardID", board.ID), | ||||
| 		mlog.String("addedUserID", reqBoardMember.UserID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(member) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/join joinBoard | ||||
| 	// | ||||
| 	// Become a member of a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardMember' | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   '403': | ||||
| 	//     description: access denied | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	if userID == "" { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
| 	if board.Type != model.BoardTypeOpen { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	newBoardMember := &model.BoardMember{ | ||||
| 		UserID:          userID, | ||||
| 		BoardID:         boardID, | ||||
| 		SchemeAdmin:     board.MinimumRole == model.BoardRoleAdmin, | ||||
| 		SchemeEditor:    board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor, | ||||
| 		SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter, | ||||
| 		SchemeViewer:    board.MinimumRole == model.BoardRoleViewer, | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("addedUserID", userID) | ||||
|  | ||||
| 	member, err := a.app.AddMemberToBoard(newBoardMember) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("JoinBoard", | ||||
| 		mlog.String("boardID", board.ID), | ||||
| 		mlog.String("addedUserID", userID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(member) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/leave leaveBoard | ||||
| 	// | ||||
| 	// Remove your own membership from a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   '403': | ||||
| 	//     description: access denied | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	if userID == "" { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "leaveBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("addedUserID", 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 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("LeaveBoard", | ||||
| 		mlog.String("boardID", board.ID), | ||||
| 		mlog.String("addedUserID", userID), | ||||
| 	) | ||||
|  | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember | ||||
| 	// | ||||
| 	// Updates a board member | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: userID | ||||
| 	//   in: path | ||||
| 	//   description: User ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: membership to replace the current one with | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/BoardMember" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       $ref: '#/definitions/BoardMember' | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	paramsUserID := mux.Vars(r)["userID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var reqBoardMember *model.BoardMember | ||||
| 	if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	newBoardMember := &model.BoardMember{ | ||||
| 		UserID:          paramsUserID, | ||||
| 		BoardID:         boardID, | ||||
| 		SchemeAdmin:     reqBoardMember.SchemeAdmin, | ||||
| 		SchemeEditor:    reqBoardMember.SchemeEditor, | ||||
| 		SchemeCommenter: reqBoardMember.SchemeCommenter, | ||||
| 		SchemeViewer:    reqBoardMember.SchemeViewer, | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "patchMember", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("patchedUserID", paramsUserID) | ||||
|  | ||||
| 	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 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("PatchMember", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("patchedUserID", paramsUserID), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(member) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /boards/{boardID}/members/{userID} deleteMember | ||||
| 	// | ||||
| 	// Deletes a member from a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: userID | ||||
| 	//   in: path | ||||
| 	//   description: User ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
| 	paramsUserID := mux.Vars(r)["userID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
| 	auditRec.AddMeta("addedUserID", 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 { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", deleteErr) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("DeleteMember", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("addedUserID", paramsUserID), | ||||
| 	) | ||||
|  | ||||
| 	// response | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										73
									
								
								server/api/onboarding.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								server/api/onboarding.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerOnboardingRoutes(r *mux.Router) { | ||||
| 	// Onboarding tour endpoints APIs | ||||
| 	r.HandleFunc("/teams/{teamID}/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost) | ||||
| } | ||||
|  | ||||
| func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /team/{teamID}/onboard onboard | ||||
| 	// | ||||
| 	// Onboards a user on Boards. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: object | ||||
| 	//       properties: | ||||
| 	//         teamID: | ||||
| 	//           type: string | ||||
| 	//           description: Team ID | ||||
| 	//         boardID: | ||||
| 	//           type: string | ||||
| 	//           description: Board ID | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	response := map[string]string{ | ||||
| 		"teamID":  teamID, | ||||
| 		"boardID": boardID, | ||||
| 	} | ||||
| 	data, err := json.Marshal(response) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| } | ||||
							
								
								
									
										324
									
								
								server/api/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								server/api/search.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerSearchRoutes(r *mux.Router) { | ||||
| 	r.HandleFunc("/teams/{teamID}/channels", a.sessionRequired(a.handleSearchMyChannels)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/boards/search", a.sessionRequired(a.handleSearchBoards)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/boards/search/linkable", a.sessionRequired(a.handleSearchLinkableBoards)).Methods("GET") | ||||
| 	r.HandleFunc("/boards/search", a.sessionRequired(a.handleSearchAllBoards)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/channels searchMyChannels | ||||
| 	// | ||||
| 	// Returns the user available channels | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: search | ||||
| 	//   in: query | ||||
| 	//   description: string to filter channels list | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Channel" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	query := r.URL.Query() | ||||
| 	searchQuery := query.Get("search") | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "searchMyChannels", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
|  | ||||
| 	channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetUserChannels", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.Int("channelsCount", len(channels)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(channels) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("channelsCount", len(channels)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/boards/search searchBoards | ||||
| 	// | ||||
| 	// Returns the boards that match with a search term in the team | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: q | ||||
| 	//   in: query | ||||
| 	//   description: The search term. Must have at least one character | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Board" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	term := r.URL.Query().Get("q") | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(term) == 0 { | ||||
| 		jsonStringResponse(w, http.StatusOK, "[]") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "searchBoards", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
|  | ||||
| 	// retrieve boards list | ||||
| 	boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("SearchBoards", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.Int("boardsCount", len(boards)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(boards) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("boardsCount", len(boards)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/boards/search/linkable searchLinkableBoards | ||||
| 	// | ||||
| 	// Returns the boards that match with a search term in the team and the | ||||
| 	// user has permission to manage members | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: q | ||||
| 	//   in: query | ||||
| 	//   description: The search term. Must have at least one character | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Board" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	if !a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in standalone mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	term := r.URL.Query().Get("q") | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(term) == 0 { | ||||
| 		jsonStringResponse(w, http.StatusOK, "[]") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "searchLinkableBoards", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
|  | ||||
| 	// retrieve boards list | ||||
| 	boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	linkableBoards := []*model.Board{} | ||||
| 	for _, board := range boards { | ||||
| 		if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionManageBoardRoles) { | ||||
| 			linkableBoards = append(linkableBoards, board) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("SearchLinkableBoards", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.Int("boardsCount", len(linkableBoards)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(linkableBoards) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("boardsCount", len(linkableBoards)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/search searchBoards | ||||
| 	// | ||||
| 	// Returns the boards that match with a search term | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: q | ||||
| 	//   in: query | ||||
| 	//   description: The search term. Must have at least one character | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Board" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	term := r.URL.Query().Get("q") | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if len(term) == 0 { | ||||
| 		jsonStringResponse(w, http.StatusOK, "[]") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	// retrieve boards list | ||||
| 	boards, err := a.app.SearchBoardsForUser(term, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("SearchAllBoards", | ||||
| 		mlog.Int("boardsCount", len(boards)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(boards) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("boardsCount", len(boards)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										181
									
								
								server/api/sharing.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								server/api/sharing.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerSharingRoutes(r *mux.Router) { | ||||
| 	// Sharing APIs | ||||
| 	r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST") | ||||
| 	r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handleGetSharing)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /boards/{boardID}/sharing getSharing | ||||
| 	// | ||||
| 	// Returns sharing information for a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/Sharing" | ||||
| 	//   '404': | ||||
| 	//     description: board not found | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("boardID", boardID) | ||||
|  | ||||
| 	sharing, err := a.app.GetSharing(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if sharing == nil { | ||||
| 		jsonStringResponse(w, http.StatusOK, "") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sharingData, err := json.Marshal(sharing) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, sharingData) | ||||
|  | ||||
| 	a.logger.Debug("GET sharing", | ||||
| 		mlog.String("boardID", boardID), | ||||
| 		mlog.String("shareID", sharing.ID), | ||||
| 		mlog.Bool("enabled", sharing.Enabled), | ||||
| 	) | ||||
| 	auditRec.AddMeta("shareID", sharing.ID) | ||||
| 	auditRec.AddMeta("enabled", sharing.Enabled) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /boards/{boardID}/sharing postSharing | ||||
| 	// | ||||
| 	// Sets sharing information for a board | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Board ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: sharing information for a root block | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/Sharing" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	boardID := mux.Vars(r)["boardID"] | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
| 	if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var sharing model.Sharing | ||||
| 	err = json.Unmarshal(requestBody, &sharing) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Stamp boardID from the URL | ||||
| 	sharing.ID = boardID | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("shareID", sharing.ID) | ||||
| 	auditRec.AddMeta("enabled", sharing.Enabled) | ||||
|  | ||||
| 	// Stamp ModifiedBy | ||||
| 	modifiedBy := userID | ||||
| 	if userID == model.SingleUser { | ||||
| 		modifiedBy = "" | ||||
| 	} | ||||
| 	sharing.ModifiedBy = modifiedBy | ||||
|  | ||||
| 	if userID == model.SingleUser { | ||||
| 		userID = "" | ||||
| 	} | ||||
|  | ||||
| 	if !a.app.GetClientConfig().EnablePublicSharedBoards { | ||||
| 		a.logger.Warn( | ||||
| 			"Attempt to turn on sharing for board via API failed, sharing off in configuration.", | ||||
| 			mlog.String("boardID", sharing.ID), | ||||
| 			mlog.String("userID", userID)) | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "Turning on sharing for board failed, see log for details.", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sharing.ModifiedBy = userID | ||||
|  | ||||
| 	err = a.app.UpsertSharing(sharing) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										236
									
								
								server/api/subscriptions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								server/api/subscriptions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerSubscriptionsRoutes(r *mux.Router) { | ||||
| 	// Subscription APIs | ||||
| 	r.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST") | ||||
| 	r.HandleFunc("/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE") | ||||
| 	r.HandleFunc("/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET") | ||||
| } | ||||
|  | ||||
| // subscriptions | ||||
|  | ||||
| func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /subscriptions createSubscription | ||||
| 	// | ||||
| 	// Creates a subscription to a block for a user. The user will receive change notifications for the block. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: subscription definition | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/Subscription" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//         "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var sub model.Subscription | ||||
|  | ||||
| 	err = json.Unmarshal(requestBody, &sub) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err = sub.IsValid(); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 	} | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "createSubscription", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("subscriber_id", sub.SubscriberID) | ||||
| 	auditRec.AddMeta("block_id", sub.BlockID) | ||||
|  | ||||
| 	// User can only create subscriptions for themselves (for now) | ||||
| 	if session.UserID != sub.SubscriberID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// check for valid block | ||||
| 	block, err := a.app.GetBlockByID(sub.BlockID) | ||||
| 	if err != nil || block == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid blockID", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	subNew, err := a.app.CreateSubscription(&sub) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("CREATE subscription", | ||||
| 		mlog.String("subscriber_id", subNew.SubscriberID), | ||||
| 		mlog.String("block_id", subNew.BlockID), | ||||
| 	) | ||||
|  | ||||
| 	json, err := json.Marshal(subNew) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, json) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation DELETE /subscriptions/{blockID}/{subscriberID} deleteSubscription | ||||
| 	// | ||||
| 	// Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: blockID | ||||
| 	//   in: path | ||||
| 	//   description: Block ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: subscriberID | ||||
| 	//   in: path | ||||
| 	//   description: Subscriber ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	blockID := vars["blockID"] | ||||
| 	subscriberID := vars["subscriberID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
| 	auditRec.AddMeta("block_id", blockID) | ||||
| 	auditRec.AddMeta("subscriber_id", subscriberID) | ||||
|  | ||||
| 	// User can only delete subscriptions for themselves | ||||
| 	if session.UserID != subscriberID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_, err := a.app.DeleteSubscription(blockID, subscriberID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("DELETE subscription", | ||||
| 		mlog.String("blockID", blockID), | ||||
| 		mlog.String("subscriberID", subscriberID), | ||||
| 	) | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /subscriptions/{subscriberID} getSubscriptions | ||||
| 	// | ||||
| 	// Gets subscriptions for a user. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: subscriberID | ||||
| 	//   in: path | ||||
| 	//   description: Subscriber ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	subscriberID := vars["subscriberID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("subscriber_id", subscriberID) | ||||
|  | ||||
| 	// User can only get subscriptions for themselves (for now) | ||||
| 	if session.UserID != subscriberID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	subs, err := a.app.GetSubscriptions(subscriberID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GET subscriptions", | ||||
| 		mlog.String("subscriberID", subscriberID), | ||||
| 		mlog.Int("count", len(subs)), | ||||
| 	) | ||||
|  | ||||
| 	json, err := json.Marshal(subs) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	jsonBytesResponse(w, http.StatusOK, json) | ||||
|  | ||||
| 	auditRec.AddMeta("subscription_count", len(subs)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										26
									
								
								server/api/system.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/api/system.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerSystemRoutes(r *mux.Router) { | ||||
| 	// System APIs | ||||
| 	r.HandleFunc("/hello", a.handleHello).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleHello(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /hello hello | ||||
| 	// | ||||
| 	// Responds with `Hello` if the web service is running. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - text/plain | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	stringResponse(w, "Hello") | ||||
| } | ||||
							
								
								
									
										245
									
								
								server/api/teams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								server/api/teams.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerTeamsRoutes(r *mux.Router) { | ||||
| 	// Team APIs | ||||
| 	r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET") | ||||
| 	r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams getTeams | ||||
| 	// | ||||
| 	// Returns information of all the teams | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Team" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	teams, err := a.app.GetTeamsForUser(userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamCount", len(teams)) | ||||
|  | ||||
| 	data, err := json.Marshal(teams) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID} getTeam | ||||
| 	// | ||||
| 	// Returns information of the root team | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/Team" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var team *model.Team | ||||
| 	var err error | ||||
|  | ||||
| 	if a.MattermostAuth { | ||||
| 		team, err = a.app.GetTeam(teamID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		} | ||||
| 		if team == nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid team", nil) | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		team, err = a.app.GetRootTeam() | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getTeam", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("resultTeamID", team.ID) | ||||
|  | ||||
| 	data, err := json.Marshal(team) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /teams/{teamID}/regenerate_signup_token regenerateSignupToken | ||||
| 	// | ||||
| 	// Regenerates the signup token for the root team | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
| 	if a.MattermostAuth { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	team, err := a.app.GetRootTeam() | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	team.SignupToken = utils.NewID(utils.IDTypeToken) | ||||
|  | ||||
| 	err = a.app.UpsertTeamSignupToken(*team) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonStringResponse(w, http.StatusOK, "{}") | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/users getTeamUsers | ||||
| 	// | ||||
| 	// Returns team users | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: search | ||||
| 	//   in: query | ||||
| 	//   description: string to filter users list | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
| 	userID := getUserID(r) | ||||
| 	query := r.URL.Query() | ||||
| 	searchQuery := query.Get("search") | ||||
|  | ||||
| 	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"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	users, err := a.app.SearchTeamUsers(teamID, searchQuery) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(users) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("userCount", len(users)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										90
									
								
								server/api/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								server/api/templates.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerTemplatesRoutes(r *mux.Router) { | ||||
| 	r.HandleFunc("/teams/{teamID}/templates", a.sessionRequired(a.handleGetTemplates)).Methods("GET") | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /teams/{teamID}/templates getTemplates | ||||
| 	// | ||||
| 	// Returns team templates | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Team ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/Board" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	teamID := mux.Vars(r)["teamID"] | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("teamID", teamID) | ||||
|  | ||||
| 	// retrieve boards list | ||||
| 	boards, err := a.app.GetTemplateBoards(teamID, userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	results := []*model.Board{} | ||||
| 	for _, board := range boards { | ||||
| 		if board.Type == model.BoardTypeOpen { | ||||
| 			results = append(results, board) | ||||
| 		} else if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionViewBoard) { | ||||
| 			results = append(results, board) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("GetTemplates", | ||||
| 		mlog.String("teamID", teamID), | ||||
| 		mlog.Int("boardsCount", len(results)), | ||||
| 	) | ||||
|  | ||||
| 	data, err := json.Marshal(results) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// response | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
|  | ||||
| 	auditRec.AddMeta("templatesCount", len(results)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
							
								
								
									
										303
									
								
								server/api/users.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								server/api/users.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| func (a *API) registerUsersRoutes(r *mux.Router) { | ||||
| 	// Users APIs | ||||
| 	r.HandleFunc("/users", a.sessionRequired(a.handleGetUsersList)).Methods("POST") | ||||
| 	r.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") | ||||
| 	r.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET") | ||||
| 	r.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") | ||||
| 	r.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut) | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /users getUser | ||||
| 	// | ||||
| 	// Returns a user[] | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: userID | ||||
| 	//   in: path | ||||
| 	//   description: User ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var userIDs []string | ||||
| 	if err = json.Unmarshal(requestBody, &userIDs); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getUsersList", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelAuth, auditRec) | ||||
|  | ||||
| 	users, err := a.app.GetUsersList(userIDs) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	usersList, err := json.Marshal(users) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonStringResponse(w, http.StatusOK, string(usersList)) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /users/me getMe | ||||
| 	// | ||||
| 	// Returns the currently logged-in user | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	var user *model.User | ||||
| 	var err error | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getMe", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	if userID == model.SingleUser { | ||||
| 		ws, _ := a.app.GetRootTeam() | ||||
| 		now := utils.GetMillis() | ||||
| 		user = &model.User{ | ||||
| 			ID:       model.SingleUser, | ||||
| 			Username: model.SingleUser, | ||||
| 			Email:    model.SingleUser, | ||||
| 			CreateAt: ws.UpdateAt, | ||||
| 			UpdateAt: now, | ||||
| 		} | ||||
| 	} else { | ||||
| 		user, err = a.app.GetUser(userID) | ||||
| 		if err != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	userData, err := json.Marshal(user) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	jsonBytesResponse(w, http.StatusOK, userData) | ||||
|  | ||||
| 	auditRec.AddMeta("userID", user.ID) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /users/me/memberships getMyMemberships | ||||
| 	// | ||||
| 	// Returns the currently users board memberships | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       type: array | ||||
| 	//       items: | ||||
| 	//         "$ref": "#/definitions/BoardMember" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	userID := getUserID(r) | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "getMyBoardMemberships", audit.Fail) | ||||
| 	auditRec.AddMeta("userID", userID) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
|  | ||||
| 	members, err := a.app.GetMembersForUser(userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	membersData, err := json.Marshal(members) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, membersData) | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /users/{userID} getUser | ||||
| 	// | ||||
| 	// Returns a user | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: userID | ||||
| 	//   in: path | ||||
| 	//   description: User ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/User" | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	userID := vars["userID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("userID", userID) | ||||
|  | ||||
| 	user, err := a.app.GetUser(userID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userData, err := json.Marshal(user) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, userData) | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation PATCH /users/{userID}/config updateUserConfig | ||||
| 	// | ||||
| 	// Updates user config | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: userID | ||||
| 	//   in: path | ||||
| 	//   description: User ID | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: Body | ||||
| 	//   in: body | ||||
| 	//   description: User config patch to apply | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/UserPropPatch" | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var patch *model.UserPropPatch | ||||
| 	err = json.Unmarshal(requestBody, &patch) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	userID := vars["userID"] | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session := ctx.Value(sessionContextKey).(*model.Session) | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelModify, auditRec) | ||||
|  | ||||
| 	// a user can update only own config | ||||
| 	if userID != session.UserID { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	updatedConfig, err := a.app.UpdateUserConfig(userID, *patch) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.Marshal(updatedConfig) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, data) | ||||
| 	auditRec.Success() | ||||
| } | ||||
| @@ -431,7 +431,7 @@ func (c *Client) GetRegisterRoute() string { | ||||
| 	return "/register" | ||||
| } | ||||
|  | ||||
| func (c *Client) Register(request *api.RegisterRequest) (bool, *Response) { | ||||
| func (c *Client) Register(request *model.RegisterRequest) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetRegisterRoute(), toJSON(&request)) | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| @@ -445,14 +445,14 @@ func (c *Client) GetLoginRoute() string { | ||||
| 	return "/login" | ||||
| } | ||||
|  | ||||
| func (c *Client) Login(request *api.LoginRequest) (*api.LoginResponse, *Response) { | ||||
| func (c *Client) Login(request *model.LoginRequest) (*model.LoginResponse, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetLoginRoute(), toJSON(&request)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	data, err := api.LoginResponseFromJSON(r.Body) | ||||
| 	data, err := model.LoginResponseFromJSON(r.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -512,7 +512,7 @@ func (c *Client) GetUserChangePasswordRoute(id string) string { | ||||
| 	return fmt.Sprintf("/users/%s/changepassword", id) | ||||
| } | ||||
|  | ||||
| func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest) (bool, *Response) { | ||||
| func (c *Client) UserChangePassword(id string, data *model.ChangePasswordRequest) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetUserChangePasswordRoute(id), toJSON(&data)) | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/api" | ||||
| 	"github.com/mattermost/focalboard/server/client" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/server" | ||||
| @@ -363,7 +362,7 @@ func (th *TestHelper) TearDown() { | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) { | ||||
| 	req := &api.RegisterRequest{ | ||||
| 	req := &model.RegisterRequest{ | ||||
| 		Username: username, | ||||
| 		Email:    email, | ||||
| 		Password: password, | ||||
| @@ -378,7 +377,7 @@ func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, p | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Login(client *client.Client, username, password string) { | ||||
| 	req := &api.LoginRequest{ | ||||
| 	req := &model.LoginRequest{ | ||||
| 		Type:     "normal", | ||||
| 		Username: username, | ||||
| 		Password: password, | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/api" | ||||
| 	"github.com/mattermost/focalboard/server/client" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| @@ -2341,7 +2340,7 @@ func TestPermissionsGetUser(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestPermissionsUserChangePassword(t *testing.T) { | ||||
| 	postBody := toJSON(t, api.ChangePasswordRequest{ | ||||
| 	postBody := toJSON(t, model.ChangePasswordRequest{ | ||||
| 		OldPassword: password, | ||||
| 		NewPassword: "newpa$$word123", | ||||
| 	}) | ||||
| @@ -2549,7 +2548,7 @@ func TestPermissionsDeleteBoardsAndBlocks(t *testing.T) { | ||||
|  | ||||
| func TestPermissionsLogin(t *testing.T) { | ||||
| 	loginReq := func(username, password string) string { | ||||
| 		return toJSON(t, api.LoginRequest{ | ||||
| 		return toJSON(t, model.LoginRequest{ | ||||
| 			Type:     "normal", | ||||
| 			Username: username, | ||||
| 			Password: password, | ||||
| @@ -2628,7 +2627,7 @@ func TestPermissionsRegister(t *testing.T) { | ||||
| 		require.NotNil(th.T, team) | ||||
| 		require.NotNil(th.T, team.SignupToken) | ||||
|  | ||||
| 		postData := toJSON(t, api.RegisterRequest{ | ||||
| 		postData := toJSON(t, model.RegisterRequest{ | ||||
| 			Username: "newuser", | ||||
| 			Email:    "newuser@test.com", | ||||
| 			Password: password, | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import ( | ||||
| 	"crypto/rand" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/api" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| @@ -21,7 +20,7 @@ func TestUserRegister(t *testing.T) { | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	// register | ||||
| 	registerRequest := &api.RegisterRequest{ | ||||
| 	registerRequest := &model.RegisterRequest{ | ||||
| 		Username: fakeUsername, | ||||
| 		Email:    fakeEmail, | ||||
| 		Password: utils.NewID(utils.IDTypeNone), | ||||
| @@ -41,7 +40,7 @@ func TestUserLogin(t *testing.T) { | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	t.Run("with nonexist user", func(t *testing.T) { | ||||
| 		loginRequest := &api.LoginRequest{ | ||||
| 		loginRequest := &model.LoginRequest{ | ||||
| 			Type:     "normal", | ||||
| 			Username: "nonexistuser", | ||||
| 			Email:    "", | ||||
| @@ -55,7 +54,7 @@ func TestUserLogin(t *testing.T) { | ||||
| 	t.Run("with registered user", func(t *testing.T) { | ||||
| 		password := utils.NewID(utils.IDTypeNone) | ||||
| 		// register | ||||
| 		registerRequest := &api.RegisterRequest{ | ||||
| 		registerRequest := &model.RegisterRequest{ | ||||
| 			Username: fakeUsername, | ||||
| 			Email:    fakeEmail, | ||||
| 			Password: password, | ||||
| @@ -65,7 +64,7 @@ func TestUserLogin(t *testing.T) { | ||||
| 		require.True(t, success) | ||||
|  | ||||
| 		// login | ||||
| 		loginRequest := &api.LoginRequest{ | ||||
| 		loginRequest := &model.LoginRequest{ | ||||
| 			Type:     "normal", | ||||
| 			Username: fakeUsername, | ||||
| 			Email:    fakeEmail, | ||||
| @@ -91,7 +90,7 @@ func TestGetMe(t *testing.T) { | ||||
| 	t.Run("logged in", func(t *testing.T) { | ||||
| 		// register | ||||
| 		password := utils.NewID(utils.IDTypeNone) | ||||
| 		registerRequest := &api.RegisterRequest{ | ||||
| 		registerRequest := &model.RegisterRequest{ | ||||
| 			Username: fakeUsername, | ||||
| 			Email:    fakeEmail, | ||||
| 			Password: password, | ||||
| @@ -100,7 +99,7 @@ func TestGetMe(t *testing.T) { | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.True(t, success) | ||||
| 		// login | ||||
| 		loginRequest := &api.LoginRequest{ | ||||
| 		loginRequest := &model.LoginRequest{ | ||||
| 			Type:     "normal", | ||||
| 			Username: fakeUsername, | ||||
| 			Email:    fakeEmail, | ||||
| @@ -126,7 +125,7 @@ func TestGetUser(t *testing.T) { | ||||
|  | ||||
| 	// register | ||||
| 	password := utils.NewID(utils.IDTypeNone) | ||||
| 	registerRequest := &api.RegisterRequest{ | ||||
| 	registerRequest := &model.RegisterRequest{ | ||||
| 		Username: fakeUsername, | ||||
| 		Email:    fakeEmail, | ||||
| 		Password: password, | ||||
| @@ -135,7 +134,7 @@ func TestGetUser(t *testing.T) { | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	require.True(t, success) | ||||
| 	// login | ||||
| 	loginRequest := &api.LoginRequest{ | ||||
| 	loginRequest := &model.LoginRequest{ | ||||
| 		Type:     "normal", | ||||
| 		Username: fakeUsername, | ||||
| 		Email:    fakeEmail, | ||||
| @@ -171,7 +170,7 @@ func TestUserChangePassword(t *testing.T) { | ||||
|  | ||||
| 	// register | ||||
| 	password := utils.NewID(utils.IDTypeNone) | ||||
| 	registerRequest := &api.RegisterRequest{ | ||||
| 	registerRequest := &model.RegisterRequest{ | ||||
| 		Username: fakeUsername, | ||||
| 		Email:    fakeEmail, | ||||
| 		Password: password, | ||||
| @@ -180,7 +179,7 @@ func TestUserChangePassword(t *testing.T) { | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	require.True(t, success) | ||||
| 	// login | ||||
| 	loginRequest := &api.LoginRequest{ | ||||
| 	loginRequest := &model.LoginRequest{ | ||||
| 		Type:     "normal", | ||||
| 		Username: fakeUsername, | ||||
| 		Email:    fakeEmail, | ||||
| @@ -196,7 +195,7 @@ func TestUserChangePassword(t *testing.T) { | ||||
| 	require.NotNil(t, originalMe) | ||||
|  | ||||
| 	// change password | ||||
| 	success, resp = th.Client.UserChangePassword(originalMe.ID, &api.ChangePasswordRequest{ | ||||
| 	success, resp = th.Client.UserChangePassword(originalMe.ID, &model.ChangePasswordRequest{ | ||||
| 		OldPassword: password, | ||||
| 		NewPassword: utils.NewID(utils.IDTypeNone), | ||||
| 	}) | ||||
|   | ||||
							
								
								
									
										129
									
								
								server/model/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								server/model/auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/services/auth" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	MinimumPasswordLength = 8 | ||||
| ) | ||||
|  | ||||
| type AuthParamError struct { | ||||
| 	msg string | ||||
| } | ||||
|  | ||||
| func (pe AuthParamError) Error() string { | ||||
| 	return pe.msg | ||||
| } | ||||
|  | ||||
| // LoginRequest is a login request | ||||
| // swagger:model | ||||
| type LoginRequest struct { | ||||
| 	// Type of login, currently must be set to "normal" | ||||
| 	// required: true | ||||
| 	Type string `json:"type"` | ||||
|  | ||||
| 	// If specified, login using username | ||||
| 	// required: false | ||||
| 	Username string `json:"username"` | ||||
|  | ||||
| 	// If specified, login using email | ||||
| 	// required: false | ||||
| 	Email string `json:"email"` | ||||
|  | ||||
| 	// Password | ||||
| 	// required: true | ||||
| 	Password string `json:"password"` | ||||
|  | ||||
| 	// MFA token | ||||
| 	// required: false | ||||
| 	// swagger:ignore | ||||
| 	MfaToken string `json:"mfa_token"` | ||||
| } | ||||
|  | ||||
| // LoginResponse is a login response | ||||
| // swagger:model | ||||
| type LoginResponse struct { | ||||
| 	// Session token | ||||
| 	// required: true | ||||
| 	Token string `json:"token"` | ||||
| } | ||||
|  | ||||
| func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) { | ||||
| 	var resp LoginResponse | ||||
| 	if err := json.NewDecoder(data).Decode(&resp); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &resp, nil | ||||
| } | ||||
|  | ||||
| // RegisterRequest is a user registration request | ||||
| // swagger:model | ||||
| type RegisterRequest struct { | ||||
| 	// User name | ||||
| 	// required: true | ||||
| 	Username string `json:"username"` | ||||
|  | ||||
| 	// User's email | ||||
| 	// required: true | ||||
| 	Email string `json:"email"` | ||||
|  | ||||
| 	// Password | ||||
| 	// required: true | ||||
| 	Password string `json:"password"` | ||||
|  | ||||
| 	// Registration authorization token | ||||
| 	// required: true | ||||
| 	Token string `json:"token"` | ||||
| } | ||||
|  | ||||
| func (rd *RegisterRequest) IsValid() error { | ||||
| 	if strings.TrimSpace(rd.Username) == "" { | ||||
| 		return AuthParamError{"username is required"} | ||||
| 	} | ||||
| 	if strings.TrimSpace(rd.Email) == "" { | ||||
| 		return AuthParamError{"email is required"} | ||||
| 	} | ||||
| 	if !auth.IsEmailValid(rd.Email) { | ||||
| 		return AuthParamError{"invalid email format"} | ||||
| 	} | ||||
| 	if rd.Password == "" { | ||||
| 		return AuthParamError{"password is required"} | ||||
| 	} | ||||
| 	return isValidPassword(rd.Password) | ||||
| } | ||||
|  | ||||
| // ChangePasswordRequest is a user password change request | ||||
| // swagger:model | ||||
| type ChangePasswordRequest struct { | ||||
| 	// Old password | ||||
| 	// required: true | ||||
| 	OldPassword string `json:"oldPassword"` | ||||
|  | ||||
| 	// New password | ||||
| 	// required: true | ||||
| 	NewPassword string `json:"newPassword"` | ||||
| } | ||||
|  | ||||
| // IsValid validates a password change request. | ||||
| func (rd *ChangePasswordRequest) IsValid() error { | ||||
| 	if rd.OldPassword == "" { | ||||
| 		return AuthParamError{"old password is required"} | ||||
| 	} | ||||
| 	if rd.NewPassword == "" { | ||||
| 		return AuthParamError{"new password is required"} | ||||
| 	} | ||||
| 	return isValidPassword(rd.NewPassword) | ||||
| } | ||||
|  | ||||
| func isValidPassword(password string) error { | ||||
| 	if len(password) < MinimumPasswordLength { | ||||
| 		return AuthParamError{fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength)} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user