525 lines
16 KiB
Go
525 lines
16 KiB
Go
package handlers
|
|
|
|
import "database/sql"
|
|
import "errors"
|
|
import "io"
|
|
import "mime/multipart"
|
|
import "net/http"
|
|
import "os"
|
|
import "path/filepath"
|
|
import "strconv"
|
|
import "strings"
|
|
import "time"
|
|
|
|
import "codit/internal/db"
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
import "codit/internal/util"
|
|
|
|
func (api *API) ListBlockComments(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var comments []models.BlockComment
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
var role string
|
|
var i int
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if comments == nil {
|
|
comments = []models.BlockComment{}
|
|
}
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok {
|
|
role, err = api.boardRoleForUser(r, params["boardId"], user)
|
|
if err != nil {
|
|
role = ""
|
|
}
|
|
for i = 0; i < len(comments); i++ {
|
|
comments[i].CanDelete = user.IsAdmin || comments[i].CreatedBy == user.ID || boardRoleAllows(role, models.RoleAdmin)
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, comments)
|
|
}
|
|
|
|
type createBlockCommentRequest struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func (api *API) CreateBlockComment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createBlockCommentRequest
|
|
var user models.User
|
|
var ok bool
|
|
var comment models.BlockComment
|
|
var err error
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil || req.Content == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "content is required")
|
|
return
|
|
}
|
|
comment, err = api.store(r).CreateBlockComment(r.Context(), params["boardId"], params["blockId"], req.Content, user.ID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, comment)
|
|
}
|
|
|
|
func (api *API) DeleteBlockComment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
var role string
|
|
var comments []models.BlockComment
|
|
var i int
|
|
var found bool
|
|
var canDelete bool
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
for i = 0; i < len(comments); i++ {
|
|
if comments[i].ID == params["commentId"] {
|
|
found = true
|
|
canDelete = comments[i].CreatedBy == user.ID
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "comment not found")
|
|
return
|
|
}
|
|
if !canDelete {
|
|
role, err = api.boardRoleForUser(r, params["boardId"], user)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
|
|
return
|
|
}
|
|
canDelete = user.IsAdmin || boardRoleAllows(role, models.RoleAdmin)
|
|
}
|
|
if !canDelete {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
|
|
return
|
|
}
|
|
err = api.store(r).DeleteBlockComment(params["boardId"], params["blockId"], params["commentId"])
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrCommentNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "comment not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListBlockChecklistItems(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var items []models.BlockChecklistItem
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
items, err = api.store(r).ListBlockChecklistItems(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []models.BlockChecklistItem{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
type createBlockChecklistItemRequest struct {
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
func (api *API) CreateBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createBlockChecklistItemRequest
|
|
var user models.User
|
|
var ok bool
|
|
var item models.BlockChecklistItem
|
|
var err error
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil || req.Title == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "title is required")
|
|
return
|
|
}
|
|
item, err = api.store(r).CreateBlockChecklistItem(r.Context(), params["boardId"], params["blockId"], req.Title, user.ID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, item)
|
|
}
|
|
|
|
type updateBlockChecklistItemRequest struct {
|
|
Title string `json:"title"`
|
|
Done bool `json:"done"`
|
|
}
|
|
|
|
func (api *API) UpdateBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req updateBlockChecklistItemRequest
|
|
var user models.User
|
|
var ok bool
|
|
var item models.BlockChecklistItem
|
|
var err error
|
|
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
|
|
return
|
|
}
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil || req.Title == "" {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "title is required")
|
|
return
|
|
}
|
|
item, err = api.store(r).UpdateBlockChecklistItem(params["boardId"], params["blockId"], params["itemId"], req.Title, req.Done, user.ID)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrChecklistItemNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "checklist item not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (api *API) DeleteBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteBlockChecklistItem(params["boardId"], params["blockId"], params["itemId"])
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrChecklistItemNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "checklist item not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ReorderBlockChecklistItems(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req struct {
|
|
IDs []string `json:"ids"`
|
|
}
|
|
var items []models.BlockChecklistItem
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil || len(req.IDs) == 0 {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "ids are required")
|
|
return
|
|
}
|
|
items, err = api.store(r).ReorderBlockChecklistItems(r.Context(), params["boardId"], params["blockId"], req.IDs)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
if errors.Is(err, db.ErrChecklistItemNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "checklist item not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []models.BlockChecklistItem{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) ListBlockAttachments(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var items []models.BlockAttachment
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
items, err = api.store(r).ListBlockAttachments(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []models.BlockAttachment{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) CreateBlockAttachment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var file multipart.File
|
|
var header *multipart.FileHeader
|
|
var err error
|
|
var id string
|
|
var storedName string
|
|
var tempName string
|
|
var tempPath string
|
|
var finalPath string
|
|
var size int64
|
|
var contentType string
|
|
var attachment models.BlockAttachment
|
|
var created models.BlockAttachment
|
|
var ts string
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
|
|
return
|
|
}
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
|
|
return
|
|
}
|
|
file, header, err = r.FormFile("file")
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "file required")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
id, err = util.NewID()
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to generate id")
|
|
return
|
|
}
|
|
storedName = "board-attachment-" + id + filepath.Ext(header.Filename)
|
|
ts = strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
tempName = storedName + ".uploading-" + ts
|
|
tempPath, size, err = api.Uploads.Save(tempName, file)
|
|
if err != nil {
|
|
_ = os.Remove(tempPath)
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
finalPath = filepath.Join(api.Uploads.BaseDir, storedName)
|
|
err = os.Rename(tempPath, finalPath)
|
|
if err != nil {
|
|
_ = os.Remove(tempPath)
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
contentType = header.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
attachment = models.BlockAttachment{
|
|
ID: id,
|
|
BlockID: params["blockId"],
|
|
Filename: header.Filename,
|
|
ContentType: contentType,
|
|
Size: size,
|
|
StoragePath: finalPath,
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.store(r).CreateBlockAttachment(r.Context(), params["boardId"], params["blockId"], attachment)
|
|
if err != nil {
|
|
_ = os.Remove(finalPath)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) DownloadBlockAttachment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.BlockAttachment
|
|
var err error
|
|
var file *os.File
|
|
var inline bool
|
|
var stat os.FileInfo
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
item, err = api.store(r).GetBlockAttachment(params["boardId"], params["blockId"], params["attachmentId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "attachment not found")
|
|
return
|
|
}
|
|
file, err = api.Uploads.Open(item.StoragePath)
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
defer file.Close()
|
|
stat, err = file.Stat()
|
|
if err == nil {
|
|
w.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
|
|
}
|
|
inline = r.URL.Query().Get("inline") == "1" && strings.HasPrefix(item.ContentType, "image/")
|
|
w.Header().Set("Content-Type", item.ContentType)
|
|
if inline {
|
|
w.Header().Set("Content-Disposition", "inline; filename=\""+sanitizeFilename(item.Filename)+"\"")
|
|
} else {
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+sanitizeFilename(item.Filename)+"\"")
|
|
}
|
|
_, _ = io.Copy(w, file)
|
|
}
|
|
|
|
func (api *API) DeleteBlockAttachment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.BlockAttachment
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) { return }
|
|
|
|
item, err = api.store(r).DeleteBlockAttachment(r.Context(), params["boardId"], params["blockId"], params["attachmentId"])
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrBlockAttachmentNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "attachment not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
_ = os.Remove(item.StoragePath)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListBoardBlockProperties(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var props []models.BlockProperties
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
props, err = api.store(r).ListAllBlockProperties(params["boardId"])
|
|
if err != nil {
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if props == nil {
|
|
props = []models.BlockProperties{}
|
|
}
|
|
WriteJSON(w, http.StatusOK, props)
|
|
}
|
|
|
|
func (api *API) GetBlockProperties(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var bp models.BlockProperties
|
|
var err error
|
|
|
|
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
|
|
return
|
|
}
|
|
bp, err = api.store(r).GetBlockProperties(params["boardId"], params["blockId"])
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, bp)
|
|
}
|
|
|
|
type upsertBlockPropertiesRequest struct {
|
|
Status string `json:"status"`
|
|
CardType string `json:"card_type"`
|
|
Priority string `json:"priority"`
|
|
DueDate string `json:"due_date"`
|
|
AssigneeID string `json:"assignee_id"`
|
|
AssigneeIDs []string `json:"assignee_ids"`
|
|
Sprint string `json:"sprint"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (api *API) UpsertBlockProperties(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req upsertBlockPropertiesRequest
|
|
var bp models.BlockProperties
|
|
var err error
|
|
|
|
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
|
|
}
|
|
bp = models.BlockProperties{
|
|
Status: req.Status,
|
|
CardType: req.CardType,
|
|
Priority: req.Priority,
|
|
DueDate: req.DueDate,
|
|
AssigneeID: req.AssigneeID,
|
|
AssigneeIDs: req.AssigneeIDs,
|
|
Sprint: req.Sprint,
|
|
Description: req.Description,
|
|
}
|
|
bp, err = api.store(r).UpsertBlockProperties(r.Context(), params["boardId"], params["blockId"], bp)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "block not found")
|
|
return
|
|
}
|
|
if errors.Is(err, db.ErrUserNotFound) {
|
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "assignee not found")
|
|
return
|
|
}
|
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, bp)
|
|
}
|