725 lines
20 KiB
Go
725 lines
20 KiB
Go
package handlers
|
|
|
|
import "database/sql"
|
|
import "errors"
|
|
import "net/http"
|
|
import "time"
|
|
|
|
import "codit/internal/db"
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
|
|
// boardRoleRank returns a numeric rank for board roles (higher = more access).
|
|
func boardRoleRank(role string) int {
|
|
switch role {
|
|
case models.RoleAdmin:
|
|
return 3
|
|
case models.RoleWriter:
|
|
return 2
|
|
case models.RoleViewer:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// boardRoleAllows checks whether actual satisfies the required board role.
|
|
// Hierarchy: admin > writer > viewer.
|
|
func boardRoleAllows(actual, required string) bool {
|
|
return boardRoleRank(actual) >= boardRoleRank(required)
|
|
}
|
|
|
|
func (api *API) boardRoleForUser(r *http.Request, boardID string, user models.User) (string, error) {
|
|
var role string
|
|
var boardRole string
|
|
var boardErr error
|
|
var projectID string
|
|
var projRole string
|
|
var err error
|
|
|
|
if user.IsAdmin {
|
|
return models.RoleAdmin, nil
|
|
}
|
|
projectID, err = api.store(r).GetBoardProjectID(boardID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
projRole, err = api.store(r).GetProjectRoleForUser(projectID, user.ID)
|
|
if err == nil {
|
|
role = projectRoleToBoardRole(projRole)
|
|
} else if err != sql.ErrNoRows {
|
|
return "", err
|
|
}
|
|
boardRole, boardErr = api.store(r).GetBoardMemberRole(boardID, user.ID)
|
|
if boardErr == nil && boardRoleRank(boardRole) > boardRoleRank(role) {
|
|
role = boardRole
|
|
} else if boardErr != nil && boardErr != sql.ErrNoRows {
|
|
return "", boardErr
|
|
}
|
|
return role, nil
|
|
}
|
|
|
|
func projectRoleToBoardRole(projectRole string) string {
|
|
switch projectRole {
|
|
case models.RoleAdmin:
|
|
return models.RoleAdmin
|
|
case models.RoleWriter:
|
|
return models.RoleWriter
|
|
default:
|
|
return models.RoleViewer
|
|
}
|
|
}
|
|
|
|
// normalizeBoardRole validates and returns the canonical role name.
|
|
// Returns "" for unknown roles.
|
|
func normalizeBoardRole(role string) string {
|
|
switch role {
|
|
case models.RoleAdmin, models.RoleWriter, models.RoleViewer:
|
|
return role
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// requireBoardRole checks that the caller has at least the required board role.
|
|
// Board membership and project role are both checked; the higher access wins.
|
|
func (api *API) requireBoardRole(w http.ResponseWriter, r *http.Request, boardID, required string) bool {
|
|
var user models.User
|
|
var principal models.ServicePrincipal
|
|
var ok bool
|
|
var role string
|
|
var err error
|
|
var projectID string
|
|
var projRole string
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok {
|
|
role, err = api.boardRoleForUser(r, boardID, user)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
if role == "" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
if !boardRoleAllows(role, required) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
principal, ok = middleware.PrincipalFromContext(r.Context())
|
|
if !ok || principal.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return false
|
|
}
|
|
if principal.IsAdmin {
|
|
return true
|
|
}
|
|
projectID, err = api.store(r).GetBoardProjectID(boardID)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
projRole, err = api.store(r).GetPrincipalProjectRole(principal.ID, projectID)
|
|
if err != nil || !boardRoleAllows(projectRoleToBoardRole(projRole), required) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ----- boards -----
|
|
|
|
type createBoardRequest struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Icon string `json:"icon"`
|
|
ShowDescription bool `json:"show_description"`
|
|
CardProperties string `json:"card_properties"`
|
|
IsTemplate bool `json:"is_template"`
|
|
SeedDefaultFields bool `json:"seed_default_fields"`
|
|
}
|
|
|
|
type updateBoardRequest struct {
|
|
Title *string `json:"title"`
|
|
Description *string `json:"description"`
|
|
Icon *string `json:"icon"`
|
|
ShowDescription *bool `json:"show_description"`
|
|
CardProperties *string `json:"card_properties"`
|
|
}
|
|
|
|
func (api *API) ListAllBoards(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var boards []models.Board
|
|
var err error
|
|
var user models.User
|
|
var userOK bool
|
|
|
|
user, userOK = middleware.UserFromContext(r.Context())
|
|
if !userOK {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if user.IsAdmin {
|
|
boards, err = api.store(r).ListAllBoardsAdmin()
|
|
} else {
|
|
boards, err = api.store(r).ListAllBoardsForUser(user.ID)
|
|
}
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if boards == nil {
|
|
boards = []models.Board{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, boards)
|
|
}
|
|
|
|
func (api *API) ListBoards(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var boards []models.Board
|
|
var err error
|
|
var projRoleErr error
|
|
var user models.User
|
|
var principal models.ServicePrincipal
|
|
var userOK bool
|
|
var principalOK bool
|
|
|
|
user, userOK = middleware.UserFromContext(r.Context())
|
|
principal, principalOK = middleware.PrincipalFromContext(r.Context())
|
|
|
|
if !userOK && (!principalOK || principal.Disabled) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if (userOK && user.IsAdmin) || (principalOK && !principal.Disabled && principal.IsAdmin) {
|
|
boards, err = api.store(r).ListBoards(params["projectId"])
|
|
} else if userOK {
|
|
_, projRoleErr = api.store(r).GetProjectRoleForUser(params["projectId"], user.ID)
|
|
if projRoleErr == nil {
|
|
// Project member: see all boards in the project.
|
|
boards, err = api.store(r).ListBoards(params["projectId"])
|
|
} else {
|
|
// No project role: return only boards with an explicit membership entry.
|
|
boards, err = api.store(r).ListBoardsWhereExplicitMember(params["projectId"], user.ID)
|
|
if err == nil && len(boards) == 0 {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
// Service principal: project-role access only; no per-board membership.
|
|
_, projRoleErr = api.store(r).GetPrincipalProjectRole(principal.ID, params["projectId"])
|
|
if projRoleErr != nil {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
boards, err = api.store(r).ListBoards(params["projectId"])
|
|
}
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if boards == nil {
|
|
boards = []models.Board{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, boards)
|
|
}
|
|
|
|
func (api *API) CreateBoard(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createBoardRequest
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
var board models.Board
|
|
var created models.Board
|
|
|
|
// Board mutations are attributed to a user account; service principals cannot create boards.
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board creation requires a user account")
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, params["projectId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
if req.Title == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "title is required")
|
|
return
|
|
}
|
|
board = models.Board{
|
|
ProjectID: params["projectId"],
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Icon: req.Icon,
|
|
ShowDescription: req.ShowDescription,
|
|
CardProperties: req.CardProperties,
|
|
IsTemplate: req.IsTemplate,
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.store(r).CreateBoard(r.Context(), board)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if req.SeedDefaultFields {
|
|
err = api.store(r).SeedDefaultBoardFieldValues(r.Context(), created.ID)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) GetBoard(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var board models.Board
|
|
var err error
|
|
|
|
board, err = api.store(r).GetBoard(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, board.ID, models.RoleViewer) {
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, board)
|
|
}
|
|
|
|
func (api *API) UpdateBoard(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req updateBoardRequest
|
|
var err error
|
|
var board models.Board
|
|
var updated models.Board
|
|
var user models.User
|
|
var ok bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board updates require a user account")
|
|
return
|
|
}
|
|
board, err = api.store(r).GetBoard(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, board.ID, models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
if req.Title != nil {
|
|
board.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
board.Description = *req.Description
|
|
}
|
|
if req.Icon != nil {
|
|
board.Icon = *req.Icon
|
|
}
|
|
if req.ShowDescription != nil {
|
|
board.ShowDescription = *req.ShowDescription
|
|
}
|
|
if req.CardProperties != nil {
|
|
board.CardProperties = *req.CardProperties
|
|
}
|
|
board.UpdatedBy = user.ID
|
|
updated, err = api.store(r).UpdateBoard(board)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
func (api *API) DeleteBoard(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
var board models.Board
|
|
var user models.User
|
|
var ok bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board deletion requires a user account")
|
|
return
|
|
}
|
|
board, err = api.store(r).GetBoard(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, board.ID, models.RoleAdmin) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteBoard(board.ID, user.ID)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ----- blocks -----
|
|
|
|
type createBlockRequest struct {
|
|
Type string `json:"type"`
|
|
ParentID string `json:"parent_id"`
|
|
Title string `json:"title"`
|
|
Fields string `json:"fields"`
|
|
}
|
|
|
|
type updateBlockRequest struct {
|
|
ParentID *string `json:"parent_id"`
|
|
Title *string `json:"title"`
|
|
Fields *string `json:"fields"`
|
|
Completed *bool `json:"completed"`
|
|
}
|
|
|
|
func (api *API) ListBlocks(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var blocks []models.Block
|
|
var err error
|
|
var blockType string
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
blockType = r.URL.Query().Get("type")
|
|
if blockType != "" {
|
|
blocks, err = api.store(r).ListBlocksByType(params["boardId"], blockType)
|
|
} else {
|
|
blocks, err = api.store(r).ListBlocks(params["boardId"])
|
|
}
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if blocks == nil {
|
|
blocks = []models.Block{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, blocks)
|
|
}
|
|
|
|
func (api *API) CreateBlock(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createBlockRequest
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
var block models.Block
|
|
var created models.Block
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block creation requires a user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
if req.Type == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "type is required")
|
|
return
|
|
}
|
|
block = models.Block{
|
|
BoardID: params["boardId"],
|
|
ParentID: req.ParentID,
|
|
Type: req.Type,
|
|
Title: req.Title,
|
|
Fields: req.Fields,
|
|
CreatedBy: user.ID,
|
|
UpdatedBy: user.ID,
|
|
}
|
|
created, err = api.store(r).CreateBlock(r.Context(), block)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrParentNotInBoard) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) GetBlock(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var block models.Block
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, block)
|
|
}
|
|
|
|
func (api *API) UpdateBlock(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req updateBlockRequest
|
|
var err error
|
|
var block models.Block
|
|
var updated models.Block
|
|
var user models.User
|
|
var ok bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
if req.ParentID != nil {
|
|
block.ParentID = *req.ParentID
|
|
}
|
|
if req.Title != nil {
|
|
block.Title = *req.Title
|
|
}
|
|
if req.Fields != nil {
|
|
block.Fields = *req.Fields
|
|
}
|
|
if req.Completed != nil {
|
|
if *req.Completed {
|
|
block.CompletedAt = time.Now().UTC().Unix()
|
|
block.CompletedBy = user.ID
|
|
} else {
|
|
block.CompletedAt = 0
|
|
block.CompletedBy = ""
|
|
}
|
|
}
|
|
block.UpdatedBy = user.ID
|
|
updated, err = api.store(r).UpdateBlock(r.Context(), block)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrParentNotInBoard) || errors.Is(err, db.ErrParentCycle) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
func (api *API) PatchBlocks(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var patches []models.BlockPatch
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &patches)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
err = api.store(r).PatchBlocks(r.Context(), params["boardId"], patches, user.ID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
if errors.Is(err, db.ErrParentNotInBoard) || errors.Is(err, db.ErrParentCycle) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) DeleteBlock(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block deletion requires a user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteBlock(r.Context(), params["boardId"], params["blockId"], user.ID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ----- board members -----
|
|
|
|
type upsertBoardMemberRequest struct {
|
|
UserID string `json:"user_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (api *API) ListBoardMembers(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var members []models.BoardMember
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
|
|
return
|
|
}
|
|
members, err = api.store(r).ListBoardMembers(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if members == nil {
|
|
members = []models.BoardMember{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, members)
|
|
}
|
|
|
|
func (api *API) ListBoardAssignableUsers(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var users []models.BoardAssignableUser
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
users, err = api.store(r).ListBoardAssignableUsers(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if users == nil {
|
|
users = []models.BoardAssignableUser{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, users)
|
|
}
|
|
|
|
func (api *API) AddBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req upsertBoardMemberRequest
|
|
var err error
|
|
var member models.BoardMember
|
|
var role string
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
if req.UserID == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "user_id is required")
|
|
return
|
|
}
|
|
if req.Role == "" {
|
|
req.Role = models.RoleWriter
|
|
}
|
|
role = normalizeBoardRole(req.Role)
|
|
if role == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
|
|
return
|
|
}
|
|
member = models.BoardMember{
|
|
BoardID: params["boardId"],
|
|
UserID: req.UserID,
|
|
Role: role,
|
|
}
|
|
err = api.store(r).UpsertBoardMember(r.Context(), member)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrBoardNotFound) || errors.Is(err, db.ErrUserNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req upsertBoardMemberRequest
|
|
var err error
|
|
var member models.BoardMember
|
|
var role string
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
role = normalizeBoardRole(req.Role)
|
|
if role == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
|
|
return
|
|
}
|
|
member = models.BoardMember{
|
|
BoardID: params["boardId"],
|
|
UserID: params["userId"],
|
|
Role: role,
|
|
}
|
|
err = api.store(r).UpsertBoardMember(r.Context(), member)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrBoardNotFound) || errors.Is(err, db.ErrUserNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) RemoveBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteBoardMember(params["boardId"], params["userId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|