154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
|
"git.gobha.me/xcaliber/chat-switchboard/store"
|
|
)
|
|
|
|
// ── Workflow Entry Handler ──────────────────
|
|
// Handles the visitor-facing workflow start flow.
|
|
// Landing page rendering is in pages/pages.go (RenderWorkflowLanding).
|
|
|
|
type WorkflowEntryHandler struct {
|
|
stores store.Stores
|
|
}
|
|
|
|
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) {
|
|
ctx := c.Request.Context()
|
|
scope := c.Param("scope")
|
|
slug := c.Param("slug")
|
|
|
|
teamID := resolveTeamScope(ctx, h.stores.Teams, scope)
|
|
|
|
wf, err := h.stores.Workflows.GetBySlug(ctx, teamID, slug)
|
|
if err != nil || wf == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"})
|
|
return
|
|
}
|
|
if !wf.IsActive {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow not active"})
|
|
return
|
|
}
|
|
if wf.EntryMode != "public_link" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "workflow requires authentication"})
|
|
return
|
|
}
|
|
|
|
ver, err := h.stores.Workflows.GetLatestVersion(ctx, wf.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow has no published version"})
|
|
return
|
|
}
|
|
|
|
stages, err := h.stores.Workflows.ListStages(ctx, wf.ID)
|
|
if err != nil || len(stages) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow has no stages"})
|
|
return
|
|
}
|
|
|
|
// Create the workflow channel (owned by workflow creator)
|
|
ch := &models.Channel{
|
|
UserID: wf.CreatedBy,
|
|
Title: wf.Name,
|
|
Description: wf.Description,
|
|
Type: "workflow",
|
|
TeamID: wf.TeamID,
|
|
}
|
|
if err := h.stores.Channels.Create(ctx, ch); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
|
|
return
|
|
}
|
|
|
|
// Set workflow columns
|
|
err = h.stores.Channels.SetWorkflowInstance(ctx, ch.ID, wf.ID, ver.VersionNumber, []byte("{}"), "active")
|
|
if err != nil {
|
|
log.Printf("Failed to set workflow columns: %v", err)
|
|
}
|
|
// Enable anonymous access + auto AI mode for visitor entry
|
|
_ = h.stores.Channels.Update(ctx, ch.ID, map[string]interface{}{
|
|
"allow_anonymous": true,
|
|
"ai_mode": "auto",
|
|
})
|
|
|
|
// Create anonymous session
|
|
sessionToken := store.NewID()
|
|
visitorCount, _ := h.stores.Sessions.CountForChannel(ctx, ch.ID)
|
|
displayName := "Visitor"
|
|
if visitorCount > 0 {
|
|
displayName = fmt.Sprintf("Visitor %d", visitorCount+1)
|
|
}
|
|
sess := &models.SessionParticipant{
|
|
SessionToken: sessionToken,
|
|
ChannelID: ch.ID,
|
|
DisplayName: displayName,
|
|
}
|
|
if err := h.stores.Sessions.Create(ctx, sess); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create session"})
|
|
return
|
|
}
|
|
|
|
// Add session as channel participant
|
|
_ = h.stores.Channels.AddParticipant(ctx, &models.ChannelParticipant{
|
|
ChannelID: ch.ID,
|
|
ParticipantType: "session",
|
|
ParticipantID: sess.ID,
|
|
Role: "visitor",
|
|
})
|
|
|
|
// Bind stage 0 persona
|
|
if stages[0].PersonaID != nil {
|
|
_ = h.stores.Channels.AddParticipant(ctx, &models.ChannelParticipant{
|
|
ChannelID: ch.ID,
|
|
ParticipantType: "persona",
|
|
ParticipantID: *stages[0].PersonaID,
|
|
Role: "member",
|
|
})
|
|
}
|
|
|
|
// Set session cookie (30 day expiry, matching v0.24.3)
|
|
c.SetCookie("sb_session", sessionToken, 30*24*60*60, "/", "", false, true)
|
|
|
|
// Redirect to workflow page — visitor lands on existing /w/:channelId
|
|
stageMode := stages[0].StageMode
|
|
if stageMode == "" {
|
|
stageMode = "chat_only"
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"channel_id": ch.ID,
|
|
"session_id": sess.ID,
|
|
"redirect_to": "/w/" + ch.ID,
|
|
"stage_mode": stageMode,
|
|
})
|
|
}
|