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>
236 lines
6.2 KiB
Go
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{}
|