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