Changeset 0.31.2 (#205)

This commit is contained in:
2026-03-19 13:29:27 +00:00
parent 8364440081
commit 6668e546fe
25 changed files with 1357 additions and 50 deletions

View File

@@ -105,6 +105,7 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
team := &models.Team{
Name: req.Name,
Slug: slugify(req.Name),
Description: req.Description,
CreatedBy: adminID,
IsActive: true,
@@ -118,7 +119,7 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
return
}
c.JSON(http.StatusCreated, gin.H{"id": team.ID, "name": team.Name})
c.JSON(http.StatusCreated, gin.H{"id": team.ID, "name": team.Name, "slug": team.Slug})
AuditLog(h.stores.Audit, c, "team.create", "team", team.ID, map[string]interface{}{"name": team.Name})
}
@@ -155,6 +156,7 @@ func (h *TeamHandler) UpdateTeam(c *gin.Context) {
fields := map[string]interface{}{}
if req.Name != nil {
fields["name"] = *req.Name
fields["slug"] = slugify(*req.Name)
}
if req.Description != nil {
fields["description"] = *req.Description

View File

@@ -1,6 +1,7 @@
package handlers
import (
"context"
"fmt"
"log"
"net/http"
@@ -23,6 +24,24 @@ func NewWorkflowEntryHandler(stores store.Stores) *WorkflowEntryHandler {
return &WorkflowEntryHandler{stores: stores}
}
// resolveTeamScope converts a URL scope parameter to a team ID.
// Accepts "global" (returns nil), a UUID (passed through), or a team slug (looked up).
func resolveTeamScope(ctx context.Context, teams store.TeamStore, scope string) *string {
if scope == "global" {
return nil
}
// UUID format: 36 chars with dashes at 8,13,18,23
if len(scope) == 36 && scope[8] == '-' && scope[13] == '-' {
return &scope
}
// Try team slug lookup
t, err := teams.GetBySlug(ctx, scope)
if err == nil && t != nil {
return &t.ID
}
return &scope // fall through — will 404 downstream
}
// StartVisitor creates an anonymous session + workflow instance for a visitor.
// POST /api/v1/workflow-entry/:scope/:slug
func (h *WorkflowEntryHandler) StartVisitor(c *gin.Context) {
@@ -30,10 +49,7 @@ func (h *WorkflowEntryHandler) StartVisitor(c *gin.Context) {
scope := c.Param("scope")
slug := c.Param("slug")
var teamID *string
if scope != "global" {
teamID = &scope
}
teamID := resolveTeamScope(ctx, h.stores.Teams, scope)
wf, err := h.stores.Workflows.GetBySlug(ctx, teamID, slug)
if err != nil || wf == nil {

View File

@@ -0,0 +1,157 @@
package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"git.gobha.me/xcaliber/chat-switchboard/models"
)
// ── Team-Scoped Workflow Wrappers (v0.31.2) ────────────────
//
// 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
}
// ── 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)
}

View File

@@ -106,6 +106,12 @@ func (h *WorkflowHandler) Create(c *gin.Context) {
w.CreatedBy = c.GetString("user_id")
// v0.31.2: Team-scoped workflow creation (set by CreateTeamWorkflow)
if forceTeam, ok := c.Get("force_team_id"); ok {
teamID := forceTeam.(string)
w.TeamID = &teamID
}
if err := h.stores.Workflows.Create(c.Request.Context(), &w); err != nil {
if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "UNIQUE") {
c.JSON(http.StatusConflict, gin.H{"error": "slug already exists in this scope"})