Changeset 0.37.15 (#227)
This commit is contained in:
@@ -302,6 +302,128 @@ func (h *WorkflowInstanceHandler) Reject(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Cancel (v0.37.15) ────────────────────────
|
||||
|
||||
// CancelInstance cancels a workflow instance and all its open assignments.
|
||||
// POST /api/v1/channels/:id/workflow/cancel
|
||||
func (h *WorkflowInstanceHandler) CancelInstance(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
channelID := c.Param("id")
|
||||
userID := c.GetString("user_id")
|
||||
role, _ := c.Get("role")
|
||||
|
||||
workflowID, _, status, err := h.readWorkflowState(ctx, channelID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "workflow channel not found"})
|
||||
return
|
||||
}
|
||||
if status != "active" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow is " + status + ", cannot cancel"})
|
||||
return
|
||||
}
|
||||
|
||||
// Auth: instance owner, team admin, or global admin
|
||||
if role != "admin" {
|
||||
ch, err := h.stores.Channels.GetByID(ctx, channelID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
|
||||
return
|
||||
}
|
||||
isOwner := ch.UserID == userID
|
||||
isTeamAdmin := false
|
||||
if ch.TeamID != nil && h.stores.Teams != nil {
|
||||
isTeamAdmin, _ = h.stores.Teams.IsTeamAdmin(ctx, *ch.TeamID, userID)
|
||||
}
|
||||
if !isOwner && !isTeamAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "only instance owner, team admin, or global admin can cancel"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel the workflow instance
|
||||
if err := h.stores.Channels.CancelWorkflow(ctx, channelID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to cancel workflow"})
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel all open assignments
|
||||
cancelled, _ := h.stores.Workflows.CancelAssignmentsForChannel(ctx, channelID)
|
||||
|
||||
// Emit WS event
|
||||
h.emitWorkflowEvent("workflow.cancelled", channelID, map[string]any{
|
||||
"channel_id": channelID,
|
||||
"workflow_id": workflowID,
|
||||
"cancelled_by": userID,
|
||||
"assignments_cancelled": cancelled,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"cancelled": true,
|
||||
"channel_id": channelID,
|
||||
"assignments_cancelled": cancelled,
|
||||
})
|
||||
}
|
||||
|
||||
// CancelTeamInstance cancels a workflow instance via the team-admin monitor.
|
||||
// POST /api/v1/teams/:teamId/workflows/monitor/instances/:channelId/cancel
|
||||
func (h *WorkflowInstanceHandler) CancelTeamInstance(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
channelID := c.Param("channelId")
|
||||
userID := c.GetString("user_id")
|
||||
role, _ := c.Get("role")
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Auth: team admin or global admin
|
||||
if role != "admin" {
|
||||
if h.stores.Teams == nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "team admin access required"})
|
||||
return
|
||||
}
|
||||
isTA, _ := h.stores.Teams.IsTeamAdmin(ctx, teamID, userID)
|
||||
if !isTA {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "team admin access required"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the channel belongs to this team
|
||||
ch, err := h.stores.Channels.GetByID(ctx, channelID)
|
||||
if err != nil || ch.TeamID == nil || *ch.TeamID != teamID {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "workflow instance not found for this team"})
|
||||
return
|
||||
}
|
||||
|
||||
_, _, status, err := h.readWorkflowState(ctx, channelID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "workflow channel not found"})
|
||||
return
|
||||
}
|
||||
if status != "active" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow is " + status + ", cannot cancel"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.Channels.CancelWorkflow(ctx, channelID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to cancel workflow"})
|
||||
return
|
||||
}
|
||||
|
||||
cancelled, _ := h.stores.Workflows.CancelAssignmentsForChannel(ctx, channelID)
|
||||
|
||||
h.emitWorkflowEvent("workflow.cancelled", channelID, map[string]any{
|
||||
"channel_id": channelID,
|
||||
"cancelled_by": userID,
|
||||
"assignments_cancelled": cancelled,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"cancelled": true,
|
||||
"channel_id": channelID,
|
||||
"assignments_cancelled": cancelled,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────
|
||||
|
||||
func (h *WorkflowInstanceHandler) readWorkflowState(ctx context.Context, channelID string) (workflowID string, currentStage int, status string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user