diff --git a/server/internal/handler/auth.go b/server/internal/handler/auth.go index 7380acce7..c6a17c0c3 100644 --- a/server/internal/handler/auth.go +++ b/server/internal/handler/auth.go @@ -412,7 +412,12 @@ func (h *Handler) GoogleLogin(w http.ResponseWriter, r *http.Request) { } // Fetch user info from Google. - userInfoReq, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo", nil) + userInfoReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo", nil) + if err != nil { + slog.Error("failed to create userinfo request", "error", err) + writeError(w, http.StatusInternalServerError, "internal error") + return + } userInfoReq.Header.Set("Authorization", "Bearer "+gToken.AccessToken) userInfoResp, err := http.DefaultClient.Do(userInfoReq) diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go index 2a322ab26..fb8a3ed74 100644 --- a/server/internal/handler/issue.go +++ b/server/internal/handler/issue.go @@ -1252,6 +1252,7 @@ func (h *Handler) BatchUpdateIssues(w http.ResponseWriter, r *http.Request) { AssigneeID: prevIssue.AssigneeID, DueDate: prevIssue.DueDate, ParentIssueID: prevIssue.ParentIssueID, + ProjectID: prevIssue.ProjectID, } if req.Updates.Title != nil { @@ -1295,6 +1296,33 @@ func (h *Handler) BatchUpdateIssues(w http.ResponseWriter, r *http.Request) { } } + if _, ok := rawUpdates["parent_issue_id"]; ok { + if req.Updates.ParentIssueID != nil { + newParentID := parseUUID(*req.Updates.ParentIssueID) + // Cannot set self as parent. + if uuidToString(newParentID) == issueID { + continue + } + // Validate parent exists in the same workspace. + if _, err := h.Queries.GetIssueInWorkspace(r.Context(), db.GetIssueInWorkspaceParams{ + ID: newParentID, + WorkspaceID: prevIssue.WorkspaceID, + }); err != nil { + continue + } + params.ParentIssueID = newParentID + } else { + params.ParentIssueID = pgtype.UUID{Valid: false} + } + } + if _, ok := rawUpdates["project_id"]; ok { + if req.Updates.ProjectID != nil { + params.ProjectID = parseUUID(*req.Updates.ProjectID) + } else { + params.ProjectID = pgtype.UUID{Valid: false} + } + } + // Enforce agent visibility for batch assignment. if req.Updates.AssigneeType != nil && *req.Updates.AssigneeType == "agent" && req.Updates.AssigneeID != nil { if ok, _ := h.canAssignAgent(r.Context(), r, *req.Updates.AssigneeID, workspaceID); !ok { @@ -1372,11 +1400,16 @@ func (h *Handler) BatchDeleteIssues(w http.ResponseWriter, r *http.Request) { h.TaskService.CancelTasksForIssue(r.Context(), issue.ID) - if err := h.Queries.DeleteIssue(r.Context(), parseUUID(issueID)); err != nil { + // Collect attachment URLs before CASCADE delete to clean up S3 objects. + attachmentURLs, _ := h.Queries.ListAttachmentURLsByIssueOrComments(r.Context(), issue.ID) + + if err := h.Queries.DeleteIssue(r.Context(), issue.ID); err != nil { slog.Warn("batch delete issue failed", "issue_id", issueID, "error", err) continue } + h.deleteS3Objects(r.Context(), attachmentURLs) + actorType, actorID := h.resolveActor(r, userID, workspaceID) h.publish(protocol.EventIssueDeleted, workspaceID, actorType, actorID, map[string]any{"issue_id": issueID}) deleted++