package auth import ( "context" "database/sql" "fmt" "log" "github.com/gin-gonic/gin" "github.com/google/uuid" "git.gobha.me/xcaliber/chat-switchboard/config" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) const sessionCookieName = "sb_session" // CreateOrResumeSession looks up or creates a session participant for // an anonymous visitor to a workflow channel. // // Two entry paths: // - Cookie-based (default): random token stored in sb_session cookie // - mTLS-based: cert fingerprint used as stable session identity func CreateOrResumeSession(c *gin.Context, stores store.Stores, channelID string, cfg *config.Config) (*models.SessionParticipant, error) { // Check for existing session cookie token, _ := c.Cookie(sessionCookieName) // mTLS mode: use cert fingerprint as stable token if cfg.AuthMode == "mtls" { fp := c.GetHeader("X-SSL-Client-Fingerprint") if fp != "" { token = "mtls:" + fp } } // Resume existing session if token matches this channel if token != "" { session, err := stores.Sessions.GetByToken(c.Request.Context(), token) if err == nil && session.ChannelID == channelID { return session, nil } // Token exists but for different channel — fall through to create if err != nil && err != sql.ErrNoRows { log.Printf("[auth/session] warn: GetByToken error: %v", err) } } // Create new session token = "sess:" + uuid.New().String() displayName, err := generateVisitorName(c.Request.Context(), stores, channelID) if err != nil { displayName = "Visitor" } session := &models.SessionParticipant{ SessionToken: token, ChannelID: channelID, DisplayName: displayName, } // mTLS mode: store fingerprint for team member visibility if cfg.AuthMode == "mtls" { fp := c.GetHeader("X-SSL-Client-Fingerprint") if fp != "" { session.Fingerprint = fp } } if err := stores.Sessions.Create(c.Request.Context(), session); err != nil { return nil, fmt.Errorf("create session: %w", err) } // Add as channel participant _ = stores.Channels.AddParticipant(c.Request.Context(), &models.ChannelParticipant{ ChannelID: channelID, ParticipantType: "session", ParticipantID: session.ID, Role: "visitor", }) // Set cookie (httponly, secure, 30 day expiry) c.SetCookie(sessionCookieName, token, 60*60*24*30, "/", "", true, true) log.Printf("[auth/session] created session %s for channel %s (%s)", session.ID, channelID, displayName) return session, nil } // generateVisitorName produces "Visitor #N" based on existing session count. func generateVisitorName(ctx context.Context, stores store.Stores, channelID string) (string, error) { count, err := stores.Sessions.CountForChannel(ctx, channelID) if err != nil { return "", err } return fmt.Sprintf("Visitor #%d", count+1), nil }