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