package handlers import ( "database/sql" "log" "net/http" "strings" "github.com/gin-gonic/gin" "armature/models" "armature/store" ) // ── Team-Scoped Workflow Wrappers ──────────────── // // Thin wrappers that enforce team ownership before delegating // to the existing WorkflowHandler methods. Follows the same // pattern as CreateTeamTask / ListTeamTasks. // requireTeamWorkflow loads the workflow by :id and verifies // its team_id matches :teamId. Returns false (and writes an // HTTP error) if the check fails; true if the caller may proceed. func (h *WorkflowHandler) requireTeamWorkflow(c *gin.Context) bool { teamID := c.Param("teamId") wfID := c.Param("id") w, err := h.stores.Workflows.GetByID(c.Request.Context(), wfID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load workflow"}) } return false } if w.TeamID == nil || *w.TeamID != teamID { c.JSON(http.StatusForbidden, gin.H{"error": "workflow does not belong to this team"}) return false } return true } // ── Adopt Global Workflow ──────────────────── // AdoptTeamWorkflow clones a global (team_id=NULL) workflow into this team. // The global original is left untouched so other teams can also adopt it. // POST /api/v1/teams/:teamId/workflows/:id/adopt func (h *WorkflowHandler) AdoptTeamWorkflow(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("teamId") srcID := c.Param("id") src, err := h.stores.Workflows.GetByID(ctx, srcID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load workflow"}) } return } if src.TeamID != nil { c.JSON(http.StatusConflict, gin.H{"error": "workflow already belongs to a team"}) return } // Load stages from the global workflow stages, err := h.stores.Workflows.ListStages(ctx, srcID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load stages"}) return } // Clone the workflow into this team (global original stays untouched) clone := &models.Workflow{ TeamID: &teamID, Name: src.Name, Slug: src.Slug, Description: src.Description, Branding: src.Branding, EntryMode: src.EntryMode, IsActive: false, Version: 0, OnComplete: src.OnComplete, Retention: src.Retention, WebhookURL: src.WebhookURL, WebhookSecret: src.WebhookSecret, StalenessTimeoutHours: src.StalenessTimeoutHours, CreatedBy: c.GetString("user_id"), } if err := h.stores.Workflows.Create(ctx, clone); err != nil { if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "UNIQUE") { clone.Slug = src.Slug + "-" + store.NewID()[:6] if err2 := h.stores.Workflows.Create(ctx, clone); err2 != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to adopt workflow: " + err2.Error()}) return } } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to adopt workflow: " + err.Error()}) return } } // Clone stages for _, st := range stages { cloneSt := &models.WorkflowStage{ WorkflowID: clone.ID, Ordinal: st.Ordinal, Name: st.Name, AssignmentTeamID: st.AssignmentTeamID, FormTemplate: st.FormTemplate, StageMode: st.StageMode, Audience: st.Audience, StageType: st.StageType, AutoTransition: st.AutoTransition, StageConfig: st.StageConfig, BranchRules: st.BranchRules, StarlarkHook: st.StarlarkHook, SurfacePkgID: st.SurfacePkgID, SLASeconds: st.SLASeconds, } if err := h.stores.Workflows.CreateStage(ctx, cloneSt); err != nil { log.Printf("[workflows] adopt: failed to clone stage %s: %v", st.Name, err) } } clonedStages, _ := h.stores.Workflows.ListStages(ctx, clone.ID) if clonedStages == nil { clonedStages = []models.WorkflowStage{} } clone.Stages = clonedStages c.JSON(http.StatusCreated, clone) } // ListGlobalWorkflows returns unowned workflows available for adoption. // GET /api/v1/teams/:teamId/workflows/available func (h *WorkflowHandler) ListGlobalWorkflows(c *gin.Context) { result, err := h.stores.Workflows.ListGlobal(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list workflows"}) return } if result == nil { result = []models.Workflow{} } c.JSON(http.StatusOK, gin.H{"data": result}) } // ── Workflow CRUD ─────────────────────────── // ListTeamWorkflows returns workflows owned by the team. // GET /api/v1/teams/:teamId/workflows func (h *WorkflowHandler) ListTeamWorkflows(c *gin.Context) { teamID := c.Param("teamId") result, err := h.stores.Workflows.ListForTeam(c.Request.Context(), teamID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list workflows"}) return } if result == nil { result = []models.Workflow{} } c.JSON(http.StatusOK, gin.H{"data": result}) } // CreateTeamWorkflow creates a workflow scoped to the team. // POST /api/v1/teams/:teamId/workflows func (h *WorkflowHandler) CreateTeamWorkflow(c *gin.Context) { teamID := c.Param("teamId") c.Set("force_team_id", teamID) h.Create(c) } // GetTeamWorkflow returns a team workflow with stages. // GET /api/v1/teams/:teamId/workflows/:id func (h *WorkflowHandler) GetTeamWorkflow(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.Get(c) } // UpdateTeamWorkflow patches a team workflow. // PATCH /api/v1/teams/:teamId/workflows/:id func (h *WorkflowHandler) UpdateTeamWorkflow(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.Update(c) } // DeleteTeamWorkflow deletes a team workflow. // DELETE /api/v1/teams/:teamId/workflows/:id func (h *WorkflowHandler) DeleteTeamWorkflow(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.Delete(c) } // ── Stage CRUD ────────────────────────────── // ListTeamWorkflowStages returns stages for a team workflow. // GET /api/v1/teams/:teamId/workflows/:id/stages func (h *WorkflowHandler) ListTeamWorkflowStages(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.ListStages(c) } // CreateTeamWorkflowStage adds a stage to a team workflow. // POST /api/v1/teams/:teamId/workflows/:id/stages func (h *WorkflowHandler) CreateTeamWorkflowStage(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.CreateStage(c) } // UpdateTeamWorkflowStage updates a stage on a team workflow. // PUT /api/v1/teams/:teamId/workflows/:id/stages/:sid func (h *WorkflowHandler) UpdateTeamWorkflowStage(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.UpdateStage(c) } // DeleteTeamWorkflowStage removes a stage from a team workflow. // DELETE /api/v1/teams/:teamId/workflows/:id/stages/:sid func (h *WorkflowHandler) DeleteTeamWorkflowStage(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.DeleteStage(c) } // ReorderTeamWorkflowStages reorders stages on a team workflow. // PATCH /api/v1/teams/:teamId/workflows/:id/stages/reorder func (h *WorkflowHandler) ReorderTeamWorkflowStages(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.ReorderStages(c) } // ── Publish / Version ─────────────────────── // PublishTeamWorkflow publishes a team workflow version. // POST /api/v1/teams/:teamId/workflows/:id/publish func (h *WorkflowHandler) PublishTeamWorkflow(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.Publish(c) } // GetTeamWorkflowVersion returns a version snapshot for a team workflow. // GET /api/v1/teams/:teamId/workflows/:id/versions/:version func (h *WorkflowHandler) GetTeamWorkflowVersion(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } h.GetVersion(c) } // CloneTeamWorkflow deep-copies a team workflow. // POST /api/v1/teams/:teamId/workflows/:id/clone func (h *WorkflowHandler) CloneTeamWorkflow(c *gin.Context) { if !h.requireTeamWorkflow(c) { return } c.Set("force_team_id", c.Param("teamId")) h.Clone(c) }