package handlers import ( "encoding/json" "net/http" "strings" "github.com/gin-gonic/gin" "armature/store" "armature/workflow" ) // ── Public Workflow Handlers ─────── // WorkflowPublicHandler manages unauthenticated workflow entry endpoints. type WorkflowPublicHandler struct { engine *workflow.Engine stores store.Stores } // NewWorkflowPublicHandler creates a public workflow handler. func NewWorkflowPublicHandler(engine *workflow.Engine, stores store.Stores) *WorkflowPublicHandler { return &WorkflowPublicHandler{engine: engine, stores: stores} } // publicInstanceResponse strips internal metadata from an instance for public consumption. type publicInstanceResponse struct { ID string `json:"id"` WorkflowID string `json:"workflow_id"` CurrentStage string `json:"current_stage"` StageData json.RawMessage `json:"stage_data"` Status string `json:"status"` EntryToken *string `json:"entry_token,omitempty"` CreatedAt any `json:"created_at"` UpdatedAt any `json:"updated_at"` } // StartBySlug resolves a scope+slug to a workflow and starts a public instance. // POST /api/v1/workflow-entry/:scope/:slug // Called by the workflow landing page (/w/:scope/:slug). func (h *WorkflowPublicHandler) StartBySlug(c *gin.Context) { scope := c.Param("scope") slug := c.Param("slug") var teamID *string if scope != "global" { teamID = &scope } wf, err := h.stores.Workflows.GetBySlug(c.Request.Context(), teamID, slug) if err != nil || wf == nil { c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"}) return } var body struct { Data json.RawMessage `json:"data"` } if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" { body.Data = json.RawMessage(`{}`) } if len(body.Data) == 0 { body.Data = json.RawMessage(`{}`) } inst, err := h.engine.StartPublic(c.Request.Context(), wf.ID, body.Data) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Return redirect for the landing page JS redirectTo := "/w/" + inst.ID if inst.EntryToken != nil { redirectTo = "/w/" + inst.ID + "?token=" + *inst.EntryToken } c.JSON(http.StatusCreated, gin.H{ "id": inst.ID, "redirect_to": redirectTo, }) } // StartPublic creates an anonymous workflow instance. // POST /api/v1/public/workflows/:id/start func (h *WorkflowPublicHandler) StartPublic(c *gin.Context) { workflowID := c.Param("id") var body struct { Data json.RawMessage `json:"data"` } if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return } if len(body.Data) == 0 { body.Data = json.RawMessage(`{}`) } inst, err := h.engine.StartPublic(c.Request.Context(), workflowID, body.Data) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, publicInstanceResponse{ ID: inst.ID, WorkflowID: inst.WorkflowID, CurrentStage: inst.CurrentStage, StageData: inst.StageData, Status: inst.Status, EntryToken: inst.EntryToken, CreatedAt: inst.CreatedAt, UpdatedAt: inst.UpdatedAt, }) } // ResumePublic returns an active instance by entry token. // GET /api/v1/public/workflows/resume/:token func (h *WorkflowPublicHandler) ResumePublic(c *gin.Context) { token := c.Param("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing token"}) return } inst, err := h.engine.ResumePublic(c.Request.Context(), token) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, publicInstanceResponse{ ID: inst.ID, WorkflowID: inst.WorkflowID, CurrentStage: inst.CurrentStage, StageData: inst.StageData, Status: inst.Status, EntryToken: inst.EntryToken, CreatedAt: inst.CreatedAt, UpdatedAt: inst.UpdatedAt, }) } // AdvancePublic advances a public instance by entry token. // POST /api/v1/public/workflows/advance/:token func (h *WorkflowPublicHandler) AdvancePublic(c *gin.Context) { token := c.Param("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing token"}) return } var body struct { Data json.RawMessage `json:"data"` } if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return } if len(body.Data) == 0 { body.Data = json.RawMessage(`{}`) } inst, err := h.engine.AdvancePublic(c.Request.Context(), token, body.Data) if err != nil { if strings.Contains(err.Error(), "requires authenticated access") { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, publicInstanceResponse{ ID: inst.ID, WorkflowID: inst.WorkflowID, CurrentStage: inst.CurrentStage, StageData: inst.StageData, Status: inst.Status, EntryToken: inst.EntryToken, CreatedAt: inst.CreatedAt, UpdatedAt: inst.UpdatedAt, }) }