Files

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)
}