Files
gochat/internal/app/bootstrap.go
T

921 lines
49 KiB
Go

package app
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/auth"
"github.com/gochat/gochat/internal/autoassignment"
"github.com/gochat/gochat/internal/automation"
"github.com/gochat/gochat/internal/campaign"
"github.com/gochat/gochat/internal/canned"
"github.com/gochat/gochat/internal/channel"
emailchannel "github.com/gochat/gochat/internal/channel/email"
facebookchannel "github.com/gochat/gochat/internal/channel/facebook"
googlechannel "github.com/gochat/gochat/internal/channel/google"
linechannel "github.com/gochat/gochat/internal/channel/line"
microsoftchannel "github.com/gochat/gochat/internal/channel/microsoft"
channelprovider "github.com/gochat/gochat/internal/channel/provider"
telegramchannel "github.com/gochat/gochat/internal/channel/telegram"
tiktokchannel "github.com/gochat/gochat/internal/channel/tiktok"
twiliochannel "github.com/gochat/gochat/internal/channel/twilio"
twitterchannel "github.com/gochat/gochat/internal/channel/twitter"
whatsappchannel "github.com/gochat/gochat/internal/channel/whatsapp"
"github.com/gochat/gochat/internal/config"
"github.com/gochat/gochat/internal/database"
v1 "github.com/gochat/gochat/internal/handler/api/v1"
webhook "github.com/gochat/gochat/internal/handler/webhook"
widget "github.com/gochat/gochat/internal/handler/widget"
ws "github.com/gochat/gochat/internal/handler/ws"
llm "github.com/gochat/gochat/internal/llm"
"github.com/gochat/gochat/internal/middleware"
"github.com/gochat/gochat/internal/pubsub"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/router"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/security"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/worker"
wspkg "github.com/gochat/gochat/internal/ws"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/spf13/viper"
"gorm.io/gorm"
)
// dbProvider wraps a *gorm.DB to satisfy the DBProvider interface
// used by automation, canned, and other service packages.
type dbProvider struct {
db *gorm.DB
}
func (d *dbProvider) DB() *gorm.DB { return d.db }
// Bootstrap initializes all application dependencies in order.
// Pattern follows Chatwoot's config/initializers/ + config/application.rb boot sequence:
// 1. Load & validate config
// 2. Init logger
// 3. Connect database
// 4. Connect Redis
// 5. Wire auth services (JWT, OAuth, MFA, refresh tokens)
// 6. Wire repositories -> services -> handlers (dependency injection)
// 7. Setup HTTP router + middleware chain
func Bootstrap(env string) (*App, error) {
applogger.L().Info("Bootstrapping GoChat application...")
// Step 1: Load configuration with environment overlay support
cfg, err := config.LoadWithEnv(env)
if err != nil {
return nil, fmt.Errorf("config load failed: %w", err)
}
// Step 2: Validate configuration
if err := config.Validate(cfg); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
// Step 3: Initialize logger (ref: Chatwoot config/initializers/logging.rb)
if err := applogger.Init(applogger.Config{
Level: cfg.Log.Level,
Format: cfg.Log.Format,
Output: "stdout",
ErrorOutput: "stderr",
}); err != nil {
return nil, fmt.Errorf("logger init failed: %w", err)
}
applogger.L().Infof("Configuration loaded (env=%s, mode=%s)", env, cfg.Server.Mode)
// Step 4: Connect to PostgreSQL (ref: Chatwoot config/database.yml)
db, err := NewDatabase(&cfg.Database, cfg.Log.Level)
if err != nil {
return nil, fmt.Errorf("database init failed: %w", err)
}
// Step 4b: Run database migrations if enabled (ref: Chatwoot ActiveRecord migration pattern)
if cfg.Database.RunMigrations {
applogger.L().Info("Running database migrations...")
if err := database.RunMigrations(cfg.Database.MigrateDSN(), cfg.Database.GetMigrationsPath()); err != nil {
return nil, fmt.Errorf("database migrations failed: %w", err)
}
applogger.L().Info("Database migrations completed successfully")
}
workerPool := worker.NewWorkerPool(db)
automation.RegisterActionDeliveryJobs(workerPool, &dbProvider{db: db})
// Step 5: Connect to Redis (ref: Chatwoot config/cable.yml)
rdb, err := NewRedisClient(&cfg.Redis)
if err != nil {
return nil, fmt.Errorf("redis init failed: %w", err)
}
// Step 5b: Initialize PubSub — Redis-backed when Redis is available, in-memory fallback
var ps pubsub.PubSub
redisPubSub, redisErr := pubsub.NewRedisPubSub(rdb)
if redisErr != nil {
applogger.L().Warnf("Redis PubSub init failed, falling back to in-memory: %v", redisErr)
ps = pubsub.NewInMemoryPubSub()
} else {
ps = redisPubSub
applogger.L().Info("Redis PubSub initialized (watermill-redisstream)")
}
// Step 6: Wire auth infrastructure (P2E §1)
jwtService := auth.NewJWTService(&cfg.JWT)
refreshStore := auth.NewRefreshTokenStore(rdb, &cfg.JWT)
sessionStore := auth.NewSessionStore(&cfg.Session) // session management (ref: Chatwoot Devise sessions)
oauthService := auth.NewOAuthService(db, cfg)
mfaService := auth.NewMFAService(db)
webhookRegistry := auth.NewWebhookTokenRegistry()
// Step 6b: Wire SAML service (if enabled)
var samlService *auth.SAMLService
if cfg.SAML.Enabled {
samlSvc, err := auth.NewSAMLService(&cfg.SAML, rdb, db)
if err != nil {
return nil, fmt.Errorf("saml service init failed: %w", err)
}
samlService = samlSvc
applogger.L().Info("SAML service initialized (enabled)")
} else {
samlService, _ = auth.NewSAMLService(&cfg.SAML, nil, nil)
applogger.L().Info("SAML service initialized (disabled)")
}
// Step 6c: Wire SSO session store (M13 — SSO session tracking + SLO support)
ssoSessionStore := auth.NewSSOSessionStore(rdb, 24*time.Hour)
// Step 6d: Wire LDAP service (if enabled)
var ldapService *auth.LDAPService
if cfg.LDAP.Enabled {
ldapSvc, err := auth.NewLDAPService(&cfg.LDAP, rdb, db)
if err != nil {
return nil, fmt.Errorf("ldap service init failed: %w", err)
}
ldapService = ldapSvc
applogger.L().Info("LDAP service initialized (enabled)")
} else {
ldapService, _ = auth.NewLDAPService(&cfg.LDAP, nil, nil)
applogger.L().Info("LDAP service initialized (disabled)")
}
// Step 6e: Wire OIDC service (if enabled)
var oidcService *auth.OIDCService
if cfg.OIDC.Enabled {
oidcSvc, err := auth.NewOIDCService(&cfg.OIDC, rdb, db)
if err != nil {
return nil, fmt.Errorf("oidc service init failed: %w", err)
}
oidcService = oidcSvc
applogger.L().Info("OIDC service initialized (enabled)")
} else {
oidcService, _ = auth.NewOIDCService(&cfg.OIDC, nil, nil)
applogger.L().Info("OIDC service initialized (disabled)")
}
// Step 6f: Wire unified SSO middleware (M13 — provider routing + post-auth processing)
ssoMiddleware := auth.NewSSOMiddleware(db, rdb, cfg, samlService, ldapService, oidcService)
// Step 7: Wire repositories (data access layer)
accountRepo := repository.NewAccountRepo(db)
userRepo := repository.NewUserRepo(db)
contactRepo := repository.NewContactRepo(db)
contactNoteRepo := repository.NewContactNoteRepo(db)
contactInboxRepo := repository.NewContactInboxRepo(db)
conversationRepo := repository.NewConversationRepo(db)
inboxRepo := repository.NewInboxRepo(db)
inboxMemberRepo := repository.NewInboxMemberRepo(db)
messageRepo := repository.NewMessageRepo(db)
notificationRepo := repository.NewNotificationRepo(db)
notificationPrefRepo := repository.NewNotificationPreferenceRepo(db)
pushTokenRepo := repository.NewPushTokenRepo(db)
notificationSubscriptionRepo := repository.NewNotificationSubscriptionRepo(db)
webhookSubRepo := repository.NewWebhookSubscriptionRepo(db)
conversationParticipantRepo := repository.NewConversationParticipantRepo(db)
draftMessageRepo := repository.NewDraftMessageRepo(db)
// M11: Pre-chat form repo (needed by WidgetService)
preChatFormRepo := repository.NewPreChatFormRepo(db)
// Captain repos (P10 M10 — Captain AI + Copilot)
captainAssistantRepo := repository.NewCaptainAssistantRepo(db)
captainDocumentRepo := repository.NewCaptainDocumentRepo(db)
captainScenarioRepo := repository.NewCaptainScenarioRepo(db)
captainInboxRepo := repository.NewCaptainInboxRepo(db)
captainCustomToolRepo := repository.NewCaptainCustomToolRepo(db)
captainAssistantResponseRepo := repository.NewCaptainAssistantResponseRepo(db)
captainPreferenceRepo := repository.NewCaptainPreferenceRepo(db)
copilotThreadRepo := repository.NewCopilotThreadRepo(db)
copilotMessageRepo := repository.NewCopilotMessageRepo(db)
copilotSuggestionRepo := repository.NewCopilotSuggestionRepo(db)
// Team repos (P5 — Teams + Team Members)
teamRepo := repository.NewTeamRepo(db)
teamMemberRepo := repository.NewTeamMemberRepo(db)
// Analytics repos (P11 — Reports/Analytics)
reportingEventRepo := repository.NewReportingEventRepo(db)
reportingEventsRollupRepo := repository.NewReportingEventsRollupRepo(db)
dashboardAppRepo := repository.NewDashboardAppRepo(db)
platformAppRepo := repository.NewPlatformAppRepo(db)
accessTokenRepo := repository.NewAccessTokenRepo(db)
permissibleRepo := repository.NewPermissibleRepo(db)
// M12: AgentBot repos (AgentBot + AgentBotInbox)
agentBotRepo := repository.NewAgentBotRepo(db)
agentBotInboxRepo := repository.NewAgentBotInboxRepo(db)
// Platform: InstallationConfig repo (global key-value config for super-admin)
installationConfigRepo := repository.NewInstallationConfigRepo(db)
// Banner repo (Platform announcement banners)
bannerRepo := repository.NewBannerRepo(db)
// WidgetTest repo (Platform read-only, widget/UI integration testing)
widgetTestRepo := repository.NewWidgetTestRepo(db)
// EmailChannelMigration repo (account-scoped email channel migration)
emailChannelMigrationRepo := repository.NewEmailChannelMigrationRepo(db)
// Knowledge Base repos (M9 — Help Center / Knowledge Base)
portalRepo := repository.NewPortalRepo(db)
categoryRepo := repository.NewCategoryRepo(db)
relatedCategoryRepo := repository.NewRelatedCategoryRepo(db)
articleRepo := repository.NewArticleRepo(db)
folderRepo := repository.NewFolderRepo(db)
portalMemberRepo := repository.NewPortalMemberRepo(db)
// Campaign + AutoAssignment repos
campaignRepo := repository.NewCampaignRepo(db)
assignmentPolicyRepo := repository.NewAssignmentPolicyRepo(db)
inboxAssignmentPolicyRepo := repository.NewInboxAssignmentPolicyRepo(db)
// SLA Policy repos (M11 — SLA Policy + AppliedSLA + SlaEvent + SlaPolicyInbox)
slaPolicyRepo := repository.NewSlaPolicyRepo(db)
appliedSlaRepo := repository.NewAppliedSlaRepo(db)
slaEventRepo := repository.NewSlaEventRepo(db)
slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db)
// Assignment Policy V2 repo (M11 — enhanced with Type + inbox join)
assignmentPolicyV2Repo := repository.NewAssignmentPolicyV2Repo(db)
assignmentPolicyInboxRepo := repository.NewAssignmentPolicyInboxRepo(db)
// Tag/Label repos (M2 — Label+Tag system)
tagRepo := repository.NewTagRepo(db)
convLabelRepo := repository.NewConversationLabelRepo(db)
// S6: WorkingHour repos (out-of-office / business hours)
workingHourRepo := repository.NewWorkingHourRepo(db)
// Search repo (M14 — Global Search + Advanced Filter)
searchRepo := repository.NewSearchRepo(db)
// G4: Company repo (CRUD + search + nested contacts/conversations/notes)
companyRepo := repository.NewCompanyRepo(db)
// G16: Third-party integration repos (IntegrationHook + IntegrationApp)
integrationHookRepo := repository.NewIntegrationHookRepo(db)
integrationAppRepo := repository.NewIntegrationAppRepo(db)
// M13 repos: SAML IdP config + Account SAML settings (enterprise SSO)
samlIdPConfigRepo := repository.NewSAMLIdPConfigRepo(db)
accountSamlSettingsRepo := repository.NewAccountSamlSettingsRepo(db)
_ = samlIdPConfigRepo // used by SAML service later
// M13 repos: LDAP + OIDC account settings (enterprise SSO)
accountLDAPSettingsRepo := repository.NewAccountLDAPSettingsRepo(db)
accountOIDCSettingsRepo := repository.NewAccountOIDCSettingsRepo(db)
_ = accountLDAPSettingsRepo // used by LDAP handler later
_ = accountOIDCSettingsRepo // used by OIDC handler later
// Custom attribute definition + custom filter repos
customAttributeDefinitionRepo := repository.NewCustomAttributeDefinitionRepo(db)
customFilterRepo := repository.NewCustomFilterRepo(db)
// Lane C: CSAT template + Inbox limit repos
csatTemplateRepo := repository.NewCsatTemplateRepo(db)
inboxLimitRepo := repository.NewInboxLimitRepo(db)
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy repos
auditRepo := repository.NewAuditRepo(db)
customRoleRepo := repository.NewCustomRoleRepo(db)
agentCapacityPolicyRepo := repository.NewAgentCapacityPolicyRepo(db)
// Step 8: Wire services (business logic layer)
authService := service.NewAuthService(db, jwtService, refreshStore, oauthService, mfaService)
accountService := service.NewAccountService(accountRepo)
contactInboxService := service.NewContactInboxService(contactInboxRepo)
noteRepo := repository.NewNoteRepo(db)
contactNoteService := service.NewContactNoteService(contactRepo, contactNoteRepo)
contactService := service.NewContactService(contactRepo, contactInboxService, noteRepo)
contactService.SetContactExportMailer(service.NewEnvContactExportMailer())
// G4: Company service (depends on companyRepo, contactRepo, conversationRepo for nested queries)
companyService := service.NewCompanyService(companyRepo, contactRepo, conversationRepo)
// G16: Third-party integration services
integrationHookService := service.NewIntegrationHookService(integrationHookRepo, integrationAppRepo, nil) // registry created after webhookSignatureService
slackIntegrationService := service.NewSlackIntegrationService(integrationHookRepo)
shopifyIntegrationService := service.NewShopifyIntegrationService(integrationHookRepo)
linearIntegrationService := service.NewLinearIntegrationService(integrationHookRepo)
notionIntegrationService := service.NewNotionIntegrationService(integrationHookRepo)
// Create channel dispatcher for event-driven architecture (ref: Chatwoot Dispatcher)
channelDispatcher := channel.NewDispatcher(workerPool)
// P9: AgentBot rule engine services (declared early so listener can be registered on dispatcher)
botRuleService := automation.NewBotRuleService(&dbProvider{db: db})
botTriggerConfigService := automation.NewBotTriggerConfigService(&dbProvider{db: db})
agentBotRuleListener := automation.NewAgentBotRuleListener(&dbProvider{db: db})
// Step 8b: Wire channel providers (Facebook, Instagram, Telegram, Twitter, Microsoft, Google)
// Reference: Chatwoot registers channel providers in config/initializers/channels.rb
fbProvider := facebookchannel.NewFacebookProvider()
igProvider := facebookchannel.NewInstagramProvider()
tgProvider := channelprovider.NewTelegramProvider()
twProvider := twitterchannel.NewTwitterProvider(twitterchannel.TwitterOAuth2Config{
ClientID: cfg.OAuth.Twitter.ClientID,
ClientSecret: cfg.OAuth.Twitter.ClientSecret,
RedirectURL: cfg.OAuth.Twitter.RedirectURL,
Scopes: cfg.OAuth.Twitter.Scopes,
})
msProvider := microsoftchannel.NewMicrosoftProvider(microsoftchannel.MicrosoftOAuthConfig{
ClientID: cfg.OAuth.Microsoft.ClientID,
ClientSecret: cfg.OAuth.Microsoft.ClientSecret,
TenantID: cfg.OAuth.Microsoft.TenantID,
RedirectURL: cfg.OAuth.Microsoft.RedirectURL,
Scopes: cfg.OAuth.Microsoft.Scopes,
})
goProvider := googlechannel.NewGoogleProvider(googlechannel.GoogleOAuthConfig{
ClientID: cfg.OAuth.Google.ClientID,
ClientSecret: cfg.OAuth.Google.ClientSecret,
RedirectURL: cfg.OAuth.Google.RedirectURL,
Scopes: cfg.OAuth.Google.Scopes,
})
// Register channel providers with the global registry so outgoing messages route correctly.
// The provider/ init() package already registers these, so we use Register() which
// returns an error on duplicates rather than MustRegister() which panics.
if err := channel.Register(fbProvider); err != nil {
applogger.L().Warnf("FB provider already registered: %v", err)
}
if err := channel.Register(igProvider); err != nil {
applogger.L().Warnf("IG provider already registered: %v", err)
}
if err := channel.Register(tgProvider); err != nil {
applogger.L().Warnf("Telegram provider already registered: %v", err)
}
if err := channel.Register(twProvider); err != nil {
applogger.L().Warnf("Twitter provider already registered: %v", err)
}
if err := channel.Register(msProvider); err != nil {
applogger.L().Warnf("Microsoft provider already registered: %v", err)
}
if err := channel.Register(goProvider); err != nil {
applogger.L().Warnf("Google provider already registered: %v", err)
}
// Create Facebook channel repository and service (for listener DB access)
fbRepo := facebookchannel.NewRepository(db)
fbService := facebookchannel.NewService(fbRepo)
// Create Facebook/Instagram event listeners for incoming event processing
// (delegates to provider ProcessIncoming, handles contact/inbox resolution)
fbListener := facebookchannel.NewFacebookEventListener(db, fbService, fbRepo, fbProvider)
igListener := facebookchannel.NewInstagramEventListener(db, fbService, fbRepo, igProvider)
// Register listeners on the channel dispatcher (keyed by Name(), not channel type)
channelDispatcher.Register(fbListener)
channelDispatcher.Register(igListener)
// P9: Register AgentBot rule listener on the dispatcher
channelDispatcher.Register(agentBotRuleListener)
// M6: Register automation rule listener on the dispatcher for event-triggered automation
automation.RegisterAutomationRuleListenerWithWorker(channelDispatcher, &dbProvider{db: db}, workerPool)
// M6: Register CSAT survey listener on the dispatcher for conversation.resolved events
channelDispatcher.Register(automation.NewCsatSurveyListenerWithWorker(&dbProvider{db: db}, workerPool))
// Create Facebook webhook handler (Gin HTTP handler for FB/IG webhook endpoints)
facebookWebhookHandler := webhook.NewFacebookWebhookHandler(fbProvider, igProvider, db, channelDispatcher)
shopifyWebhookHandler := webhook.NewShopifyWebhookHandler(db, "")
// Create Telegram webhook handler (Gin HTTP handler for Telegram webhook endpoint)
telWebhook := telegramchannel.NewWebhookHandler(tgProvider)
telegramWebhookHandler := webhook.NewTelegramWebhookHandler(tgProvider, telWebhook, db, channelDispatcher)
// Step 8c: Wire WhatsApp channel provider (Cloud API / 360dialog)
// Reference: Chatwoot registers WhatsApp channel providers in config/initializers/channels.rb
// WhatsAppProvider requires service/repo/pipeline deps (unlike FB/IG/TG which are self-contained),
// so we cannot use init() registration — must wire here in bootstrap.
waRepo := whatsappchannel.NewRepository(db)
waService := whatsappchannel.NewWhatsAppService(waRepo)
waPipeline := whatsappchannel.NewIncomingPipeline(waService)
waProvider := whatsappchannel.NewWhatsAppProvider(waService, waRepo, waPipeline)
// Register WhatsApp provider with the global registry for outgoing message routing.
if err := channel.Register(waProvider); err != nil {
applogger.L().Warnf("WhatsApp provider already registered: %v", err)
}
// Create WhatsApp webhook handler (Gin HTTP handler for WA webhook endpoints)
waWebhook := whatsappchannel.NewWebhookHandler(waProvider)
whatsappWebhookHandler := webhook.NewWhatsAppWebhookHandler(waProvider, waWebhook, db, channelDispatcher)
// Step 8d: Wire TikTok channel provider (Business API)
// TikTok requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
ttRepo := tiktokchannel.NewRepository(db)
ttService := tiktokchannel.NewTikTokService(ttRepo)
ttPipeline := tiktokchannel.NewIncomingProcessor(ttService, ttRepo)
ttProvider := tiktokchannel.NewTikTokProvider(ttService, ttRepo, ttPipeline)
// Register TikTok provider with the global registry for outgoing message routing.
if err := channel.Register(ttProvider); err != nil {
applogger.L().Warnf("TikTok provider already registered: %v", err)
}
// Create TikTok webhook handler (Gin HTTP handler for TikTok webhook endpoints)
ttWebhook := tiktokchannel.NewWebhookHandler(ttService, ttPipeline)
tiktokWebhookHandler := webhook.NewTikTokWebhookHandler(ttWebhook, ttPipeline, db, channelDispatcher)
// Step 8e: Wire LINE channel provider (Messaging API)
// LINE requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
lineRepo := linechannel.NewRepository(db)
lineService := linechannel.NewLineService(lineRepo)
linePipeline := linechannel.NewIncomingProcessor(lineService)
lineProvider := linechannel.NewLineProvider(lineService, lineRepo, linePipeline)
// Register LINE provider with the global registry for outgoing message routing.
if err := channel.Register(lineProvider); err != nil {
applogger.L().Warnf("LINE provider already registered: %v", err)
}
// Create LINE webhook handler (Gin HTTP handler for LINE webhook endpoints)
lineWebhook := linechannel.NewWebhookHandler(linePipeline, lineService)
lineWebhookHandler := webhook.NewLineWebhookHandler(lineWebhook, linePipeline, lineService, db, channelDispatcher)
// Step 8f: Wire Twilio SMS channel provider (Twilio SMS API)
// Twilio SMS requires service/repo/pipeline deps (like WhatsApp), so wire here in bootstrap.
twilioRepo := twiliochannel.NewRepository(db)
twilioService := twiliochannel.NewTwilioService(twilioRepo)
twilioPipeline := twiliochannel.NewIncomingProcessor(twilioService)
twilioProvider := twiliochannel.NewTwilioProvider(twilioService, twilioRepo, twilioPipeline)
// Register Twilio SMS provider with the global registry for outgoing message routing.
if err := channel.Register(twilioProvider); err != nil {
applogger.L().Warnf("Twilio SMS provider already registered: %v", err)
}
// Create Twilio webhook handler (Gin HTTP handler for Twilio SMS webhook endpoints)
twilioWebhook := twiliochannel.NewWebhookHandler(twilioPipeline, twilioService)
twilioWebhookHandler := webhook.NewTwilioWebhookHandler(twilioWebhook, db, channelDispatcher)
// Step 8g: Wire Email channel provider (IMAP/SMTP)
// Email requires service/repo/pipeline deps, so wire here in bootstrap.
emailRepo := emailchannel.NewRepository(db)
emailSvc := emailchannel.NewService()
emailProvider := emailchannel.NewEmailProvider(emailSvc, emailRepo, nil)
emailPipeline := emailchannel.NewIncomingProcessor(emailProvider)
emailProvider.SetPipeline(emailPipeline)
// Register Email provider with the global registry for outgoing message routing.
if err := channel.Register(emailProvider); err != nil {
applogger.L().Warnf("Email provider already registered: %v", err)
}
// Create Email webhook handler (Gin HTTP handler for Email webhook endpoints)
emailWebhook := emailchannel.NewWebhookHandler()
emailWebhookHandler := webhook.NewEmailWebhookHandler(emailWebhook, emailPipeline, db)
inboxMemberService := service.NewInboxMemberService(inboxMemberRepo)
accountUserRepo := repository.NewAccountUserRepo(db)
conversationService := service.NewConversationService(conversationRepo, messageRepo, channelDispatcher, inboxMemberService, accountUserRepo, teamRepo, teamMemberRepo)
appliedSlaService := service.NewAppliedSlaService(appliedSlaRepo, slaEventRepo, slaPolicyRepo, conversationRepo)
conversationService.SetAppliedSlaService(appliedSlaService)
conversationParticipantService := service.NewConversationParticipantService(conversationParticipantRepo, conversationRepo)
draftMessageService := service.NewDraftMessageService(draftMessageRepo, conversationRepo)
inboxService := service.NewInboxService(inboxRepo, agentBotInboxRepo, agentBotRepo, campaignRepo, webhookSubRepo, waService, waRepo)
igRepo := repository.NewChannelInstagramRepo(db)
igService := service.NewChannelInstagramService(igRepo, igProvider)
fbChannelRepo := repository.NewChannelFacebookRepo(db)
fbChannelService := service.NewChannelFacebookService(fbChannelRepo)
twRepo := repository.NewChannelTwitterRepo(db)
twService := service.NewChannelTwitterService(twRepo)
msRepo := repository.NewChannelMicrosoftRepo(db)
msService := service.NewChannelMicrosoftService(msRepo)
goRepo := repository.NewChannelGoogleRepo(db)
goService := service.NewChannelGoogleService(goRepo)
// Step 8h: Wire channel CRUD service/repo layers for TikTok, LINE, Twilio SMS, Email
// These service/repo pairs are for the v1 API channel handlers (CRUD operations),
// distinct from the channel-internal service/repo (which handle message pipeline processing).
ttChannelRepo := repository.NewChannelTikTokRepo(db)
ttChannelSvc := service.NewChannelTikTokService(ttChannelRepo)
lineChannelRepo := repository.NewChannelLINERepo(db)
lineChannelSvc := service.NewChannelLINEService(lineChannelRepo)
twilioSMSRepo := repository.NewChannelTwilioSMSRepo(db)
twilioSMSSvc := service.NewChannelTwilioSMSService(twilioSMSRepo)
emailChannelRepo := repository.NewChannelEmailRepo(db)
emailChannelSvc := service.NewChannelEmailService(emailChannelRepo)
// LLM provider for Captain AI + Copilot features (must be created before services that depend on it)
llmProvider := llm.NewOpenAIProvider(llm.OpenAIProviderConfig{
APIKey: cfg.Captain.LLMAPIKey,
Model: cfg.Captain.LLMModel,
BaseURL: cfg.Captain.LLMBaseURL,
EmbedModel: cfg.Captain.EmbeddingModel,
})
messageService := service.NewMessageService(messageRepo, channelDispatcher, llmProvider)
notificationService := service.NewNotificationService(db, notificationRepo, notificationPrefRepo)
// M2 — Tag+Label services
tagService := service.NewTagService(tagRepo)
labelService := service.NewLabelService(convLabelRepo, tagRepo)
// P4 Notification+Webhook services
pushSubscriptionService := service.NewPushSubscriptionService(pushTokenRepo)
notificationSubscriptionService := service.NewNotificationSubscriptionService(notificationSubscriptionRepo)
pushDeliveryService := service.NewPushDeliveryService(pushTokenRepo, cfg.Push.VapidPublicKey, cfg.Push.VapidPrivateKey, cfg.Push.VapidSubject)
webhookSubscriptionService := service.NewWebhookSubscriptionService(webhookSubRepo)
webhookDeliveryService := service.NewWebhookDeliveryService(webhookSubRepo)
webhookSignatureService := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
webhookProcessorRegistry := service.NewWebhookProcessorRegistry(webhookSignatureService, integrationHookRepo, accountRepo)
// Wire registry into integrationHookService (created earlier with nil)
integrationHookService.SetRegistry(webhookProcessorRegistry)
_ = pushDeliveryService
_ = webhookDeliveryService
_ = webhookSignatureService
// Notification delivery pipeline — subscribes to EventBus topics and delivers via push/email/webhook (P4 M8)
notificationDeliverySvc, err := service.NewNotificationDeliveryService(
notificationService,
pushDeliveryService,
webhookDeliveryService,
webhookSignatureService,
notificationPrefRepo,
pushTokenRepo,
webhookSubRepo,
rdb,
)
if err != nil {
return nil, fmt.Errorf("failed to init notification delivery service: %w", err)
}
// Captain services (P10 M10 — Captain AI + Copilot)
captainAssistantService := service.NewCaptainAssistantService(captainAssistantRepo, captainInboxRepo, captainDocumentRepo, captainAssistantResponseRepo, llmProvider)
captainDocumentService := service.NewCaptainDocumentService(captainDocumentRepo, llmProvider, captainAssistantRepo)
captainScenarioService := service.NewCaptainScenarioService(captainScenarioRepo, captainAssistantRepo)
captainCustomToolService := service.NewCaptainCustomToolService(captainCustomToolRepo)
copilotService := service.NewCopilotService(copilotThreadRepo, copilotMessageRepo, copilotSuggestionRepo, llmProvider, captainAssistantRepo)
copilotContextService := service.NewCopilotContextService(messageRepo, conversationRepo, contactRepo, llmProvider)
captainTaskService := service.NewCaptainTaskService(captainAssistantRepo, captainAssistantResponseRepo, captainCustomToolRepo, conversationRepo, messageRepo, llmProvider, copilotContextService, copilotSuggestionRepo)
conversationInsightService := service.NewConversationInsightService(conversationRepo, messageRepo, captainAssistantRepo, llmProvider)
captainPreferenceService := service.NewCaptainPreferenceService(captainPreferenceRepo, accountRepo)
captainTaskExtendedService := service.NewCaptainTaskExtendedService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider, copilotSuggestionRepo)
captainAssistantResponseService := service.NewCaptainAssistantResponseService(captainAssistantRepo, captainAssistantResponseRepo, conversationRepo, messageRepo, captainPreferenceRepo, llmProvider)
captainBulkActionService := service.NewCaptainBulkActionService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider, captainTaskExtendedService, captainAssistantResponseService)
captainBulkActionService.SetCaptainResourceRepos(captainAssistantResponseRepo, captainDocumentRepo)
// Analytics services (P11 — Reports/Analytics)
analyticsService := service.NewAnalyticsService(reportingEventRepo, reportingEventsRollupRepo)
summaryReportService := service.NewSummaryReportService(reportingEventsRollupRepo)
dashboardAppService := service.NewDashboardAppService(dashboardAppRepo)
platformAppService := service.NewPlatformAppService(platformAppRepo, accessTokenRepo, permissibleRepo)
platformUserService := service.NewPlatformUserService(userRepo, permissibleRepo)
// M12: AgentBot services (AgentBot + AgentBotInbox + Listener)
agentBotService := service.NewAgentBotService(agentBotRepo)
agentBotInboxService := service.NewAgentBotInboxService(agentBotInboxRepo, agentBotRepo)
agentBotListener := service.NewAgentBotListener(agentBotInboxRepo, agentBotRepo)
// Platform: InstallationConfig service (global key-value config for super-admin)
installationConfigService := service.NewInstallationConfigService(installationConfigRepo)
// Banner service (Platform CRUD + account read-only)
bannerService := service.NewBannerService(bannerRepo)
// WidgetTest service (Platform read-only, widget/UI integration testing)
widgetTestService := service.NewWidgetTestService(widgetTestRepo)
// EmailChannelMigration service (account-scoped create-only)
emailChannelMigrationService := service.NewEmailChannelMigrationService(emailChannelMigrationRepo)
// Knowledge Base services (M9 — Help Center / Knowledge Base)
portalService := service.NewPortalService(portalRepo)
categoryService := service.NewCategoryService(categoryRepo, relatedCategoryRepo)
articleService := service.NewArticleService(articleRepo)
folderService := service.NewFolderService(folderRepo)
portalMemberService := service.NewPortalMemberService(portalMemberRepo)
// Automation services (P6 — Automation Rules + Macros + CSAT)
automationRuleService := automation.NewAutomationRuleServiceWithWorker(&dbProvider{db: db}, workerPool)
macroService := automation.NewMacroServiceWithWorker(&dbProvider{db: db}, workerPool)
csatSurveyService := automation.NewCsatSurveyService(&dbProvider{db: db})
cannedResponseService := canned.NewCannedResponseService(&dbProvider{db: db})
// Team + Profile services (P5 — Teams + Team Members + User Profiles)
teamService := service.NewTeamService(teamRepo, teamMemberRepo, db)
profileService := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo)
// Campaign + AutoAssignment services
campaignInternalSvc := campaign.NewCampaignService(db)
campaignService := service.NewCampaignService(campaignInternalSvc, campaignRepo)
assignmentInternalSvc := autoassignment.NewAssignmentService(db, rdb)
assignmentPolicyService := service.NewAssignmentPolicyService(assignmentPolicyRepo, inboxAssignmentPolicyRepo, assignmentInternalSvc)
// SLA Policy service (M11 — SLA Policy + AppliedSLA + SlaEvent + inbox associations)
slaPolicyService := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo)
// Assignment Policy V2 service (M11 — enhanced with Type + inbox join)
assignmentPolicyV2Service := service.NewAssignmentPolicyV2Service(assignmentPolicyV2Repo, assignmentPolicyInboxRepo)
// Search service (M14 — Global Search + Advanced Filter)
searchEngine, err := search.NewSearchEngine(search.EngineConfig{
Engine: cfg.Search.Engine,
Host: cfg.Search.Host,
APIKey: cfg.Search.APIKey,
IndexPrefix: cfg.Search.IndexPrefix,
TimeoutSeconds: cfg.Search.TimeoutSeconds,
}, searchRepo)
if err != nil {
return nil, fmt.Errorf("search engine init failed: %w", err)
}
searchService := search.NewSearchServiceWithEngine(searchEngine, searchRepo)
searchIndexer := service.NewDurableSearchIndexer(db, workerPool, searchService)
conversationService.SetSearchIndexer(searchIndexer)
messageService.SetSearchIndexer(searchIndexer)
contactService.SetSearchIndexer(searchIndexer)
contactService.SetSearchReader(searchService)
companyService.SetSearchIndexer(searchIndexer)
companyService.SetSearchReader(searchService)
articleService.SetSearchIndexer(searchIndexer)
// Custom attribute definition + custom filter + custom attribute value services
customAttributeDefinitionService := service.NewCustomAttributeDefinitionService(customAttributeDefinitionRepo)
customFilterService := service.NewCustomFilterService(customFilterRepo)
customAttributeValueService := service.NewCustomAttributeValueService(customAttributeDefinitionRepo, conversationRepo, contactRepo)
// Lane C: CSAT template + Inbox limit services
csatTemplateService := service.NewCsatTemplateService(csatTemplateRepo)
inboxLimitService := service.NewInboxLimitService(inboxLimitRepo)
// S6: WorkingHour service (out-of-office / business hours per inbox)
workingHourService := service.NewWorkingHourService(workingHourRepo, inboxRepo, accountRepo)
// Lane B: AssignableAgent service (find agents available for assignment)
assignableAgentService := service.NewAssignableAgentService(inboxMemberRepo, userRepo, accountRepo, conversationRepo)
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy, CsatMetrics services
auditService := service.NewAuditService(auditRepo)
customRoleService := service.NewCustomRoleService(customRoleRepo)
agentCapacityPolicyService := service.NewAgentCapacityPolicyService(agentCapacityPolicyRepo)
csatMetricsService := service.NewCsatMetricsService(&dbProvider{db: db})
// Step 11 (early): Create WebSocket hub (ref: Chatwoot ActionCable)
// Must be created before handlers so the hubTypingAdapter can reference it.
wsHub := ws.NewHubSimple()
// Widget service + handler (M11 — WebWidget channel completion)
// hubTypingAdapter delegates typing events to the WS hub's direct broadcast,
// avoiding Redis dependency (the full TypingTracker requires Redis).
widgetTypingAdapter := &hubTypingAdapter{hub: wsHub}
widgetThemeConfigRepo := repository.NewWidgetThemeConfigRepo(db)
widgetFileUploadRepo := repository.NewWidgetFileUploadRepo(db)
widgetOfflineMessageRepo := repository.NewWidgetOfflineMessageRepo(db)
widgetService := service.NewWidgetService(inboxRepo, contactRepo, contactInboxRepo, conversationRepo, messageRepo, widgetTypingAdapter, widgetThemeConfigRepo, preChatFormRepo, widgetFileUploadRepo, widgetOfflineMessageRepo, inboxMemberRepo, tagRepo, campaignRepo)
widgetHandler := widget.NewHandler(widgetService)
// Upload: DirectUpload repo + service + handler (account-level + widget direct uploads)
directUploadRepo := repository.NewDirectUploadRepo(db)
uploadService := service.NewUploadService(directUploadRepo, cfg).WithWidgetAuth(inboxRepo, contactInboxRepo)
uploadHandler := v1.NewUploadHandler(uploadService)
// Step 9: Wire handlers (HTTP presentation layer)
contactMergeRepo := repository.NewContactMergeRepo(db)
contactMergeService := service.NewContactMergeService(contactMergeRepo, db)
handlers := &router.Handlers{
Auth: v1.NewAuthHandler(authService, oauthService, profileService),
MFA: v1.NewMFAHandler(mfaService),
SAML: v1.NewSAMLHandler(samlService, jwtService, refreshStore, ssoSessionStore, &cfg.SAML),
Account: v1.NewAccountHandler(accountService),
Contact: v1.NewContactHandler(contactService, contactInboxService, contactMergeService, contactNoteService, conversationService),
Conversation: v1.NewConversationHandler(conversationService, messageService).WithAuditService(auditService),
Inbox: v1.NewInboxHandler(inboxService).WithAuditService(auditService),
InboxMember: v1.NewInboxMemberHandler(inboxMemberService),
WebWidget: v1.NewWebWidgetHandler(inboxService),
WebWidgetTheme: v1.NewWebWidgetThemeHandler(widgetService, inboxService),
WebWidgetPreChat: v1.NewWebWidgetPreChatHandler(widgetService, inboxService),
WebWidgetOffline: v1.NewWebWidgetOfflineHandler(widgetService, inboxService),
InstagramChannel: v1.NewInstagramChannelHandler(igService, igProvider, inboxService, igRepo),
FacebookChannel: v1.NewFacebookChannelHandler(fbChannelService, fbProvider, inboxService, fbChannelRepo),
TwitterChannel: v1.NewTwitterChannelHandler(twService, twProvider, inboxService, twRepo),
MicrosoftChannel: v1.NewMicrosoftChannelHandler(msService, msProvider, inboxService, msRepo),
GoogleChannel: v1.NewGoogleChannelHandler(goService, goProvider, inboxService, goRepo),
TikTokChannel: v1.NewTikTokChannelHandler(ttChannelSvc, ttProvider, inboxService, ttChannelRepo),
LINEChannel: v1.NewLINEChannelHandler(lineChannelSvc, lineProvider, inboxService, lineChannelRepo),
TwilioSMSChannel: v1.NewTwilioChannelHandler(twilioSMSSvc, inboxService, twilioSMSRepo),
EmailChannel: v1.NewEmailChannelHandler(emailChannelSvc, inboxService, emailChannelRepo),
EmailWebhook: emailWebhookHandler,
Message: v1.NewMessageHandler(messageService),
Profile: v1.NewProfileHandler(profileService),
Notification: v1.NewNotificationHandler(notificationService),
PlatformApp: v1.NewPlatformAppHandler(platformAppService),
Team: v1.NewTeamHandler(teamService),
CaptainAssistant: v1.NewCaptainAssistantHandler(captainAssistantService),
CaptainDocument: v1.NewCaptainDocumentHandler(captainDocumentService),
CaptainScenario: v1.NewCaptainScenarioHandler(captainScenarioService),
CaptainCustomTool: v1.NewCaptainCustomToolHandler(captainCustomToolService),
CaptainTask: v1.NewCaptainTaskHandler(captainTaskService),
CaptainPreference: v1.NewCaptainPreferenceHandler(captainPreferenceService),
CaptainTaskExtended: v1.NewCaptainTaskExtendedHandler(captainTaskExtendedService),
CaptainAssistantResponse: v1.NewCaptainAssistantResponseHandler(captainAssistantResponseService),
CaptainBulkAction: v1.NewCaptainBulkActionHandler(captainBulkActionService),
Copilot: v1.NewCopilotHandler(copilotService),
Analytics: v1.NewAnalyticsHandler(analyticsService),
LiveReport: v1.NewLiveReportHandler(analyticsService),
DashboardApp: v1.NewDashboardAppHandler(dashboardAppService),
Portal: v1.NewPortalHandler(portalService),
Category: v1.NewCategoryHandler(categoryService),
Article: v1.NewArticleHandler(articleService),
Folder: v1.NewFolderHandler(folderService),
PortalMember: v1.NewPortalMemberHandler(portalMemberService),
AutomationRule: v1.NewAutomationRuleHandler(automationRuleService).WithAuditService(auditService),
Macro: v1.NewMacroHandler(macroService).WithAuditService(auditService),
CsatSurvey: v1.NewCsatSurveyHandler(csatSurveyService).WithAuditService(auditService),
CannedResponse: v1.NewCannedResponseHandler(cannedResponseService),
PushSubscription: v1.NewPushSubscriptionHandler(pushSubscriptionService),
NotificationSubscription: v1.NewNotificationSubscriptionHandler(notificationSubscriptionService),
WebhookSubscription: v1.NewWebhookSubscriptionHandler(webhookSubscriptionService),
TelegramWebhook: telegramWebhookHandler,
FacebookWebhook: facebookWebhookHandler,
WhatsAppWebhook: whatsappWebhookHandler,
TikTokWebhook: tiktokWebhookHandler,
LineWebhook: lineWebhookHandler,
TwilioWebhook: twilioWebhookHandler,
ShopifyWebhook: shopifyWebhookHandler,
Label: v1.NewLabelHandler(tagService, labelService),
Campaign: v1.NewCampaignHandler(campaignService),
AssignmentPolicy: v1.NewAssignmentPolicyHandler(assignmentPolicyService),
SlaPolicy: v1.NewSlaPolicyHandler(slaPolicyService).WithAuditService(auditService),
AssignmentPolicyV2: v1.NewAssignmentPolicyV2Handler(assignmentPolicyV2Service),
// Enterprise: AuditLog, CustomRole, AgentCapacityPolicy, CsatMetrics handlers
Audit: v1.NewAuditHandler(auditService),
CustomRole: v1.NewCustomRoleHandler(customRoleService).WithAuditService(auditService),
AgentCapacity: v1.NewAgentCapacityHandler(agentCapacityPolicyService).WithAuditService(auditService),
CsatMetrics: v1.NewCsatMetricsHandler(csatMetricsService),
Search: v1.NewSearchHandler(searchService),
Widget: widgetHandler,
// M13: SSO/SAML enterprise authentication handlers
AccountSamlSettings: v1.NewAccountSamlSettingsHandler(accountSamlSettingsRepo),
SSOSession: v1.NewSSOSessionHandler(ssoSessionStore),
// M13: LDAP/OIDC enterprise authentication handlers
LDAP: v1.NewLDAPHandler(ldapService, ssoMiddleware, jwtService, refreshStore, ssoSessionStore, &cfg.LDAP, db),
OIDC: v1.NewOIDCHandler(oidcService, ssoMiddleware, jwtService, refreshStore, &cfg.OIDC),
SSOMiddleware: ssoMiddleware,
// M12: AgentBot handlers (platform-level + account-level bots)
AgentBot: v1.NewAgentBotHandler(agentBotService),
InstallationConfig: v1.NewInstallationConfigHandler(installationConfigService),
WidgetTest: v1.NewWidgetTestHandler(widgetTestService),
AgentBotInbox: v1.NewAgentBotInboxHandler(agentBotInboxService),
// P9: AgentBot rule engine + trigger config handlers
BotRule: v1.NewBotRuleHandler(botRuleService),
BotTriggerConfig: v1.NewBotTriggerConfigHandler(botTriggerConfigService),
// M12: SSE streaming + conversation insight handlers
SSEStream: v1.NewSSEStreamHandler(copilotService, llmProvider),
ConversationInsight: v1.NewConversationInsightHandler(conversationInsightService),
// M4 G3: ContactInbox filter handler (account-scope)
ContactInboxFilter: v1.NewContactInboxHandler(contactInboxService),
// Conversation participant + draft message handlers
ConversationParticipant: v1.NewConversationParticipantHandler(conversationParticipantService),
DraftMessage: v1.NewDraftMessageHandler(draftMessageService),
// G4: Companies module (CRUD + search + nested contacts/conversations/notes)
Company: v1.NewCompanyHandler(companyService),
// Custom attributes + custom filters
CustomAttributeDefinition: v1.NewCustomAttributeDefinitionHandler(customAttributeDefinitionService),
CustomAttributeValue: v1.NewCustomAttributeValueHandler(customAttributeValueService),
CustomFilter: v1.NewCustomFilterHandler(customFilterService),
// G16: Third-party integration handlers (IntegrationHook CRUD + Slack/Shopify/Linear/Notion)
IntegrationHook: v1.NewIntegrationHookHandler(integrationHookService),
SlackIntegration: v1.NewSlackIntegrationHandler(slackIntegrationService),
ShopifyIntegration: v1.NewShopifyIntegrationHandler(shopifyIntegrationService),
LinearIntegration: v1.NewLinearIntegrationHandler(linearIntegrationService),
NotionIntegration: v1.NewNotionIntegrationHandler(notionIntegrationService),
PlatformUserSSO: v1.NewPlatformUserSSOHandler(),
// Platform API AccessToken-authenticated handlers
PlatformUser: v1.NewPlatformUserHandler(platformUserService),
PlatformAccount: v1.NewPlatformAccountHandler(accountRepo, permissibleRepo, accountService),
PlatformAgentBot: v1.NewPlatformAgentBotHandler(agentBotRepo, permissibleRepo),
PlatformAccountUser: v1.NewPlatformAccountUserHandler(accountRepo, userRepo, permissibleRepo),
Upload: uploadHandler,
// Lane B: AssignableAgent handler (find agents available for assignment)
AssignableAgent: v1.NewAssignableAgentHandler(assignableAgentService),
AgentBulk: v1.NewAgentBulkHandler(conversationService),
BulkAction: v1.NewBulkActionHandler(conversationService, contactService),
// Lane C: CSAT template (singular per inbox) + Inbox limits
InboxCsatTemplate: v1.NewInboxCsatTemplateHandler(csatTemplateService),
InboxLimit: v1.NewInboxLimitHandler(inboxLimitService),
WorkingHour: v1.NewWorkingHourHandler(workingHourService),
// Banner handler (Platform CRUD + account read-only)
Banner: v1.NewBannerHandler(bannerService),
// EmailChannelMigration handler (account-scoped create-only)
EmailChannelMigration: v1.NewEmailChannelMigrationHandler(emailChannelMigrationService),
// SummaryReport handler (read-only reporting resource — agent/team/inbox/label summaries)
SummaryReport: v1.NewSummaryReportHandler(summaryReportService),
}
_ = agentBotListener // M12: subscribed to message events via service calls
// Step 10: Setup Gin router + middleware chain
// (ref: Chatwoot Rails middleware stack in config/application.rb)
gin.SetMode(cfg.Server.Mode)
engine := gin.New()
// Global middleware — applies to ALL routes
corsMiddleware := middleware.CORS(middleware.CORSConfigFromAppConfig(cfg))
engine.Use(middleware.Recovery()) // panic recovery
engine.Use(middleware.RequestLogger()) // structured request logging
engine.Use(middleware.RateLimit(cfg, rdb)) // rate limiting (ref: Chatwoot rack-attack)
engine.Use(corsMiddleware) // CORS with configurable whitelist
engine.Use(middleware.SecurityHeaders(middleware.DefaultSecurityHeadersConfig())) // security headers (ref: P14 deliverable #11)
// CSRF protection — double-submit cookie pattern (ref: OWASP CSRF Prevention)
// Applies globally; safe methods (GET/HEAD/OPTIONS) auto-set token cookie,
// unsafe methods validate X-CSRF-Token header against cookie value.
csrfCfg := middleware.CSRFConfigFromAppConfig(cfg)
engine.Use(middleware.CSRF(csrfCfg))
// Session middleware — loads and refreshes user sessions (ref: Chatwoot Devise sessions)
// Complementary to AuthMiddleware: JWT is primary auth, session provides continuity + force logout.
if cfg.Session.Enabled {
sessionMwCfg := middleware.SessionMiddlewareConfigFromAppConfig(cfg)
engine.Use(middleware.SessionMiddleware(sessionStore, sessionMwCfg))
}
// Step 11: WebSocket hub already created above (before handler wiring)
// so handlers can reference it via the hubTypingAdapter.
// Create WS authenticator for dual JWT + pubsub_token auth
wsAuthenticator := wspkg.NewWSAuthenticator(jwtService, contactInboxRepo)
// Register all routes with handlers + WS hub + authenticator
router.RegisterRoutes(engine, jwtService, refreshStore, webhookRegistry, handlers, wsHub, wsAuthenticator, &cfg.JWT, middleware.CORSConfigFromAppConfig(cfg), db)
// Step 12: Setup config hot-reloader (watches config file for runtime changes)
reloader, err := config.NewConfigReloader(cfg, env)
if err != nil {
applogger.L().Warnf("Config hot-reloader init failed (non-critical, continuing): %v", err)
} else {
// Register callback to dynamically adjust log level when config changes
reloader.OnChange(func(old, new *config.Config) {
if old.Log.Level != new.Log.Level {
applogger.L().Infof("Log level changed: %s → %s", old.Log.Level, new.Log.Level)
_ = applogger.Init(applogger.Config{Level: new.Log.Level, Format: new.Log.Format, Output: "stdout", ErrorOutput: "stderr"})
}
})
applogger.L().Infof("Config hot-reloader initialized (env=%s, watching %s)", env, viper.ConfigFileUsed())
}
applogger.L().Info("All dependencies wired successfully")
return &App{
config: cfg,
reloader: reloader,
db: db,
pubsub: ps,
engine: engine,
wsHub: wsHub,
notificationDeliverySvc: notificationDeliverySvc,
workerPool: workerPool,
}, nil
}
// hubTypingAdapter implements service.TypingIndicator by broadcasting
// typing_on/typing_off events directly through the WS hub's SendToAccount method.
// This avoids the Redis dependency required by the full ws.TypingTracker.
type hubTypingAdapter struct {
hub *ws.Hub
}
func (a *hubTypingAdapter) SetTypingOn(ctx context.Context, accountID, conversationID uint, performer *wspkg.Performer) error {
msg := &wspkg.WSMessage{
Event: wspkg.EventConversationTypingOn,
Data: map[string]any{"conversation_id": conversationID},
AccountID: accountID,
Performer: performer,
}
data, _ := json.Marshal(msg)
a.hub.SendToAccount(accountID, data)
return nil
}
func (a *hubTypingAdapter) SetTypingOff(ctx context.Context, accountID, conversationID uint, performer *wspkg.Performer) error {
msg := &wspkg.WSMessage{
Event: wspkg.EventConversationTypingOff,
Data: map[string]any{"conversation_id": conversationID},
AccountID: accountID,
Performer: performer,
}
data, _ := json.Marshal(msg)
a.hub.SendToAccount(accountID, data)
return nil
}