package handlers import ( "encoding/json" "net/http" "strings" "github.com/gin-gonic/gin" "switchboard-core/store" "switchboard-core/workflow" ) // ── Public Workflow Handlers (v0.3.3) ─────── // 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"` } // 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, }) }