Files
task-master/internal/server/ticket_handlers.go
T
skrueckenandCursor 6e6b13270a Add ticket file attachments with simple in-browser Excel editing.
Store uploads on disk under DataDir and expose them in the web ticket view so .xlsx files can be previewed and saved as cell values without YouTrack or TUI sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 23:53:17 +01:00

236 lines
6.2 KiB
Go

package server
import (
"encoding/json"
"net/http"
"task/internal/sqlite"
"task/internal/ticket"
)
func (s *Server) handleListTickets(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok {
writeError(w, code, "forbidden")
return
}
list, err := s.Tickets.List(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, list)
}
func (s *Server) handleCreateTicket(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r)
if !ok {
writeError(w, code, "forbidden")
return
}
var body struct {
Title string `json:"title"`
Description string `json:"description"`
Status ticket.Status `json:"status"`
}
if err := readJSON(r, &body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
t, err := s.Tickets.Create(ticket.Ticket{
Title: body.Title,
Description: body.Description,
Status: body.Status,
ProjectID: id,
})
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
_, _ = s.DB.AppendRevision("ticket", t.ID, id, "create", u.ID, t)
writeJSON(w, http.StatusCreated, t)
}
func (s *Server) handleGetTicket(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok {
writeError(w, code, "forbidden")
return
}
t, err := s.Tickets.Get(tid)
if err != nil || t.ProjectID != id {
writeError(w, http.StatusNotFound, "ticket not found")
return
}
writeJSON(w, http.StatusOK, t)
}
func (s *Server) handlePatchTicket(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r)
if !ok {
writeError(w, code, "forbidden")
return
}
t, err := s.Tickets.Get(tid)
if err != nil || t.ProjectID != id {
writeError(w, http.StatusNotFound, "ticket not found")
return
}
var body struct {
Title *string `json:"title"`
Description *string `json:"description"`
Status *ticket.Status `json:"status"`
}
if err := readJSON(r, &body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
if body.Title != nil {
t.Title = *body.Title
}
if body.Description != nil {
t.Description = *body.Description
}
if body.Status != nil {
t.Status = *body.Status
}
if err := s.Tickets.Update(t); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
t, _ = s.Tickets.Get(tid)
_, _ = s.DB.AppendRevision("ticket", t.ID, id, "update", u.ID, t)
writeJSON(w, http.StatusOK, t)
}
func (s *Server) handleDeleteTicket(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r)
if !ok {
writeError(w, code, "forbidden")
return
}
t, err := s.Tickets.Get(tid)
if err != nil || t.ProjectID != id {
writeError(w, http.StatusNotFound, "ticket not found")
return
}
_, _ = s.DB.AppendRevision("ticket", t.ID, id, "delete", u.ID, t)
if err := s.Tickets.Delete(tid); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
s.removeTicketAttachmentDir(tid)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) handleMoveTicket(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r)
if !ok {
writeError(w, code, "forbidden")
return
}
var body struct {
ToProjectID string `json:"toProjectId"`
}
if err := readJSON(r, &body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
if _, ok, code := s.requireProjectRole(body.ToProjectID, sqlite.RoleMember, r); !ok {
writeError(w, code, "forbidden target project")
return
}
t, err := s.Tickets.Get(tid)
if err != nil || t.ProjectID != id {
writeError(w, http.StatusNotFound, "ticket not found")
return
}
moved, err := s.Tickets.Move(tid, body.ToProjectID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
_ = s.Times.RetargetTicket(tid, body.ToProjectID)
_, _ = s.DB.AppendRevision("ticket", moved.ID, body.ToProjectID, "move", u.ID, moved)
writeJSON(w, http.StatusOK, moved)
}
func (s *Server) handleListTicketRevisions(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok {
writeError(w, code, "forbidden")
return
}
revs, err := s.DB.ListRevisions("ticket", tid)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, revs)
}
func (s *Server) handleRestoreTicketRevision(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
tid := r.PathValue("tid")
rid := r.PathValue("rid")
u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r)
if !ok {
writeError(w, code, "forbidden")
return
}
var revID int64
if _, err := parseInt64(rid, &revID); err != nil {
writeError(w, http.StatusBadRequest, "invalid revision id")
return
}
rev, err := s.DB.GetRevision(revID)
if err != nil || rev.EntityID != tid {
writeError(w, http.StatusNotFound, "revision not found")
return
}
var snap ticket.Ticket
if err := json.Unmarshal(rev.Snapshot, &snap); err != nil {
writeError(w, http.StatusInternalServerError, "bad snapshot")
return
}
snap.ID = tid
snap.ProjectID = id
if err := s.Tickets.Update(snap); err != nil {
// recreate if deleted
if _, cerr := s.Tickets.Create(snap); cerr != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
}
t, _ := s.Tickets.Get(tid)
_, _ = s.DB.AppendRevision("ticket", tid, id, "restore", u.ID, t)
writeJSON(w, http.StatusOK, t)
}
func parseInt64(s string, dest *int64) (int64, error) {
var n int64
for _, c := range s {
if c < '0' || c > '9' {
return 0, errInvalid
}
n = n*10 + int64(c-'0')
}
*dest = n
return n, nil
}
type invalidErr struct{}
func (invalidErr) Error() string { return "invalid" }
var errInvalid = invalidErr{}