This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/auth/session.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

97 lines
2.7 KiB
Go

package auth
import (
"context"
"database/sql"
"fmt"
"log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"switchboard-core/config"
"switchboard-core/models"
"switchboard-core/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
}