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