From 631894084af91d0b2edf6e6b734e98b715a992e4 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 20 Jul 2026 11:40:36 +0100 Subject: [PATCH] feat(api): workflow, step, and run REST endpoints --- server/internal/api/handlers.go | 2 + server/internal/api/workflows.go | 174 +++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 server/internal/api/workflows.go diff --git a/server/internal/api/handlers.go b/server/internal/api/handlers.go index 3dbc507..03b182c 100644 --- a/server/internal/api/handlers.go +++ b/server/internal/api/handlers.go @@ -78,6 +78,8 @@ func RegisterRoutes(r *gin.Engine) { apiGroup.POST("/console/connect", consoleConnect) apiGroup.GET("/console/tunnel", consoleTunnel) + + registerWorkflowRoutes(apiGroup) } } diff --git a/server/internal/api/workflows.go b/server/internal/api/workflows.go new file mode 100644 index 0000000..94e9b04 --- /dev/null +++ b/server/internal/api/workflows.go @@ -0,0 +1,174 @@ +package api + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/mrhid6/vantage/server/internal/models" + "github.com/mrhid6/vantage/server/internal/services" +) + +func registerWorkflowRoutes(g *gin.RouterGroup) { + g.GET("/steps", listSteps) + g.POST("/steps", createStep) + g.PUT("/steps/:id", updateStep) + g.DELETE("/steps/:id", deleteStep) + + g.GET("/workflows", listWorkflows) + g.POST("/workflows", createWorkflow) + g.GET("/workflows/:id", getWorkflow) + g.PUT("/workflows/:id", updateWorkflow) + g.DELETE("/workflows/:id", deleteWorkflow) + g.POST("/workflows/:id/run", runWorkflow) + g.GET("/workflows/:id/runs", listWorkflowRuns) + + g.GET("/runs/:runId", getRun) + g.POST("/runs/:runId/cancel", cancelRun) +} + +func listSteps(c *gin.Context) { + steps, err := services.ListSteps() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, steps) +} + +func createStep(c *gin.Context) { + var s models.WorkflowStep + if err := c.ShouldBindJSON(&s); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + out, err := services.CreateStep(s) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.step_created", actorFromCtx(c), "", out.StepID, fmt.Sprintf("step '%s' created", out.Name)) + c.JSON(http.StatusCreated, out) +} + +func updateStep(c *gin.Context) { + var s models.WorkflowStep + if err := c.ShouldBindJSON(&s); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := services.UpdateStep(c.Param("id"), s); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.step_updated", actorFromCtx(c), "", c.Param("id"), "step updated") + c.JSON(http.StatusOK, gin.H{"updated": true}) +} + +func deleteStep(c *gin.Context) { + if err := services.DeleteStep(c.Param("id")); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.step_deleted", actorFromCtx(c), "", c.Param("id"), "step deleted") + c.JSON(http.StatusOK, gin.H{"deleted": true}) +} + +func listWorkflows(c *gin.Context) { + wfs, err := services.ListWorkflows() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, wfs) +} + +func createWorkflow(c *gin.Context) { + var w models.Workflow + if err := c.ShouldBindJSON(&w); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + out, err := services.CreateWorkflow(w) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.created", actorFromCtx(c), "", out.WorkflowID, fmt.Sprintf("workflow '%s' created", out.Name)) + c.JSON(http.StatusCreated, out) +} + +func getWorkflow(c *gin.Context) { + w, err := services.GetWorkflow(c.Param("id")) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, w) +} + +func updateWorkflow(c *gin.Context) { + var w models.Workflow + if err := c.ShouldBindJSON(&w); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := services.UpdateWorkflow(c.Param("id"), w); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.updated", actorFromCtx(c), "", c.Param("id"), "workflow updated") + c.JSON(http.StatusOK, gin.H{"updated": true}) +} + +func deleteWorkflow(c *gin.Context) { + if err := services.DeleteWorkflow(c.Param("id")); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.deleted", actorFromCtx(c), "", c.Param("id"), "workflow deleted") + c.JSON(http.StatusOK, gin.H{"deleted": true}) +} + +func runWorkflow(c *gin.Context) { + runID, err := services.TriggerWorkflow(c.Param("id"), actorFromCtx(c)) + if err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.run_triggered", actorFromCtx(c), "", c.Param("id"), fmt.Sprintf("run %s triggered", runID)) + c.JSON(http.StatusAccepted, gin.H{"run_id": runID}) +} + +func listWorkflowRuns(c *gin.Context) { + limit := int64(50) + if l := c.Query("limit"); l != "" { + if n, err := strconv.ParseInt(l, 10, 64); err == nil && n > 0 { + limit = n + } + } + runs, err := services.ListRuns(c.Param("id"), limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, runs) +} + +func getRun(c *gin.Context) { + r, err := services.GetRun(c.Param("runId")) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, r) +} + +func cancelRun(c *gin.Context) { + if err := services.CancelRun(c.Param("runId")); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"cancelled": true}) +}