package handlers import ( "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type IssueHandler struct { db *xorm.Engine } func NewIssueHandler(db *xorm.Engine) *IssueHandler { return &IssueHandler{db: db} } type issueResponse struct { models.Issue AuthorName string `json:"authorName"` } func (h *IssueHandler) enrichIssue(issue *models.Issue) issueResponse { var author models.User h.db.ID(issue.AuthorID).Get(&author) return issueResponse{Issue: *issue, AuthorName: author.Username} } func (h *IssueHandler) List(w http.ResponseWriter, r *http.Request) { repoID, ok := h.repoID(w, r) if !ok { return } state := r.URL.Query().Get("state") if state == "" { state = "open" } var issues []models.Issue if err := h.db.Where("repo_id = ? AND state = ?", repoID, state). OrderBy("id DESC").Find(&issues); err != nil { jsonError(w, "could not list issues", http.StatusInternalServerError) return } result := make([]issueResponse, len(issues)) for i := range issues { result[i] = h.enrichIssue(&issues[i]) } if result == nil { result = []issueResponse{} } jsonOK(w, result) } func (h *IssueHandler) Create(w http.ResponseWriter, r *http.Request) { repoID, ok := h.repoID(w, r) if !ok { return } authorID, _ := middleware.UserIDFromContext(r.Context()) var body struct { Title string `json:"title"` Body string `json:"body"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Title == "" { jsonError(w, "title is required", http.StatusBadRequest) return } // Auto-increment issue number per repo count, _ := h.db.Where("repo_id = ?", repoID).Count(&models.Issue{}) number := int(count) + 1 issue := &models.Issue{ RepoID: repoID, AuthorID: authorID, Number: number, Title: body.Title, Body: body.Body, State: models.IssueStateOpen, } if _, err := h.db.Insert(issue); err != nil { jsonError(w, "could not create issue", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(h.enrichIssue(issue)) } func (h *IssueHandler) Get(w http.ResponseWriter, r *http.Request) { issue, ok := h.lookupIssue(w, r) if !ok { return } jsonOK(w, h.enrichIssue(issue)) } func (h *IssueHandler) Close(w http.ResponseWriter, r *http.Request) { issue, ok := h.lookupIssue(w, r) if !ok { return } issue.State = models.IssueStateClosed if _, err := h.db.ID(issue.ID).Cols("state").Update(issue); err != nil { jsonError(w, "could not close issue", http.StatusInternalServerError) return } jsonOK(w, h.enrichIssue(issue)) } func (h *IssueHandler) Reopen(w http.ResponseWriter, r *http.Request) { issue, ok := h.lookupIssue(w, r) if !ok { return } issue.State = models.IssueStateOpen if _, err := h.db.ID(issue.ID).Cols("state").Update(issue); err != nil { jsonError(w, "could not reopen issue", http.StatusInternalServerError) return } jsonOK(w, h.enrichIssue(issue)) } func (h *IssueHandler) repoID(w http.ResponseWriter, r *http.Request) (int64, bool) { ownerName := chi.URLParam(r, "owner") repoName := chi.URLParam(r, "repo") var owner models.User if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { jsonError(w, "repository not found", http.StatusNotFound) return 0, false } var repo models.Repository if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { jsonError(w, "repository not found", http.StatusNotFound) return 0, false } return repo.ID, true } func (h *IssueHandler) lookupIssue(w http.ResponseWriter, r *http.Request) (*models.Issue, bool) { repoID, ok := h.repoID(w, r) if !ok { return nil, false } issueNum, err := strconv.Atoi(chi.URLParam(r, "issueNum")) if err != nil { jsonError(w, "invalid issue number", http.StatusBadRequest) return nil, false } var issue models.Issue if found, _ := h.db.Where("repo_id = ? AND number = ?", repoID, issueNum).Get(&issue); !found { jsonError(w, "issue not found", http.StatusNotFound) return nil, false } return &issue, true }