package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" "boc/auth" "boc/automation" "boc/config" "boc/db" "boc/handlers" "boc/ledger" "boc/middleware" "boc/store" ) // responseWriter wraps http.ResponseWriter to capture status code type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } func main() { logger := zerolog.New(os.Stdout).With().Timestamp().Logger() cfg := config.Load() database, err := db.Connect(cfg.DBURL) if err != nil { logger.Fatal().Err(err).Msg("database connect failed") } defer database.Close() if err := db.RunMigrations(database, cfg.MigrationsDir); err != nil { logger.Fatal().Err(err).Msg("migrations failed") } _ = store.New(database) // Initialize handlers crmH := handlers.NewCRMHandler(database) salesH := handlers.NewSalesHandler(database) hrH := handlers.NewHRHandler(database) legalH := handlers.NewLegalHandler(database) marketingH := handlers.NewMarketingHandler(database) supportH := handlers.NewSupportHandler(database) analyticsH := handlers.NewAnalyticsHandler(database) ledgerH := ledger.NewHandler() // Automation engine autoEngine := automation.NewEngine(database, logger) autoH := handlers.NewAutomationHandler(database, autoEngine) // Auth: Try RS256 (Ouroboros) first, fall back to HS256 var authMiddleware func(http.Handler) http.Handler // Try RS256 from Ouroboros JWKS rs256Service, err := auth.NewRS256ServiceFromURL("http://localhost:3208/.well-known/jwks.json") if err != nil { logger.Warn().Err(err).Msg("RS256 init failed, using HS256 fallback") // Fallback to HS256 hs256Service := auth.NewService(database, cfg.JWTSecret) authMiddleware = hs256Service.Middleware() } else { logger.Info().Msg("RS256 auth service initialized from Ouroboros") authMiddleware = rs256Service.Middleware() } // Prometheus metrics requestDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "boc_request_duration_seconds", Help: "Request duration in seconds", Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, }, []string{"method", "path", "status"}) requestCount := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "boc_request_total", Help: "Total requests", }, []string{"method", "path", "status"}) activeUsers := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "boc_active_users", Help: "Currently active users", }) prometheus.MustRegister(requestDuration, requestCount, activeUsers) r := chi.NewRouter() r.Use(middleware.CORS) r.Use(hlog.NewHandler(logger)) r.Use(hlog.RequestIDHandler("req_id", "X-Request-ID")) r.Use(middleware.Logger(logger)) r.Use(chimw.Recoverer) // Metrics middleware r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} next.ServeHTTP(rw, req) duration := time.Since(start).Seconds() requestDuration.WithLabelValues(req.Method, req.URL.Path, fmt.Sprintf("%d", rw.statusCode)).Observe(duration) requestCount.WithLabelValues(req.Method, req.URL.Path, fmt.Sprintf("%d", rw.statusCode)).Inc() }) }) r.Get("/health", handlers.NewHealthHandler()) r.Get("/metrics", promhttp.Handler().ServeHTTP) // Auth endpoints (no auth required) r.Post("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) { // Forward to Ouroboros for RS256 tokens http.Redirect(w, r, "http://localhost:3208/api/auth/token", http.StatusTemporaryRedirect) }) // Protected routes r.Group(func(r chi.Router) { r.Use(authMiddleware) // Auth me r.Get("/api/v1/auth/me", func(w http.ResponseWriter, r *http.Request) { claims, ok := auth.FromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"user":{"sub":"` + claims.Sub + `","email":"` + claims.Email + `","roles":[]}}`)) }) // CRM r.Get("/api/v1/crm/customers", crmH.ListCustomers) r.Post("/api/v1/crm/customers", crmH.CreateCustomer) r.Get("/api/v1/crm/customers/{id}", crmH.GetCustomer) r.Put("/api/v1/crm/customers/{id}", crmH.UpdateCustomer) r.Delete("/api/v1/crm/customers/{id}", crmH.DeleteCustomer) r.Get("/api/v1/crm/leads", crmH.ListLeads) r.Get("/api/v1/crm/pipeline", crmH.GetPipeline) r.Post("/api/v1/crm/interactions", crmH.CreateInteraction) r.Get("/api/v1/crm/customers/{id}/interactions", crmH.GetCustomerInteractions) // Sales r.Get("/api/v1/sales/deals", salesH.ListDeals) r.Post("/api/v1/sales/deals", salesH.CreateDeal) r.Get("/api/v1/sales/deals/{id}", salesH.GetDeal) r.Put("/api/v1/sales/deals/{id}", salesH.UpdateDeal) r.Get("/api/v1/sales/mrr", salesH.GetMRR) r.Get("/api/v1/sales/arr", salesH.GetARR) r.Get("/api/v1/sales/products", salesH.ListProducts) r.Post("/api/v1/sales/products", salesH.CreateProduct) // Finance (Ledger integration) r.Get("/api/v1/finance/balance", ledgerH.GetBalanceSheet) r.Get("/api/v1/finance/income", ledgerH.GetIncomeStatement) r.Get("/api/v1/finance/moms", ledgerH.GetMomsReport) r.Get("/api/v1/finance/accounts", ledgerH.GetAccounts) r.Get("/api/v1/finance/invoices", ledgerH.GetInvoices) r.Get("/api/v1/finance/cashflow", ledgerH.GetCashflow) r.Get("/api/v1/finance/budget", ledgerH.GetBudget) r.Post("/api/v1/finance/expenses", ledgerH.CreateExpense) r.Get("/api/v1/finance/expenses", ledgerH.ListExpenses) // HR r.Get("/api/v1/hr/employees", hrH.ListEmployees) r.Post("/api/v1/hr/employees", hrH.CreateEmployee) r.Get("/api/v1/hr/employees/{id}", hrH.GetEmployee) r.Put("/api/v1/hr/employees/{id}", hrH.UpdateEmployee) r.Get("/api/v1/hr/leaves", hrH.ListLeaves) r.Post("/api/v1/hr/leaves", hrH.CreateLeave) r.Get("/api/v1/hr/timesheets", hrH.ListTimesheets) r.Post("/api/v1/hr/timesheets", hrH.CreateTimesheet) // Legal r.Get("/api/v1/legal/contracts", legalH.ListContracts) r.Post("/api/v1/legal/contracts", legalH.CreateContract) r.Get("/api/v1/legal/contracts/{id}", legalH.GetContract) r.Put("/api/v1/legal/contracts/{id}", legalH.UpdateContract) r.Get("/api/v1/legal/reminders", legalH.ListReminders) // Marketing r.Get("/api/v1/marketing/campaigns", marketingH.ListCampaigns) r.Post("/api/v1/marketing/campaigns", marketingH.CreateCampaign) r.Get("/api/v1/marketing/content", marketingH.ListContent) r.Post("/api/v1/marketing/content", marketingH.CreateContent) // Support r.Get("/api/v1/support/tickets", supportH.ListTickets) r.Post("/api/v1/support/tickets", supportH.CreateTicket) r.Get("/api/v1/support/tickets/{id}", supportH.GetTicket) r.Put("/api/v1/support/tickets/{id}", supportH.UpdateTicket) r.Post("/api/v1/support/tickets/{id}/comments", supportH.AddComment) r.Get("/api/v1/support/csat", supportH.GetCSAT) // Analytics r.Get("/api/v1/analytics/users", analyticsH.GetActiveUsers) r.Get("/api/v1/analytics/revenue", analyticsH.GetRevenue) r.Get("/api/v1/analytics/retention", analyticsH.GetRetention) r.Get("/api/v1/analytics/dashboard", analyticsH.GetDashboard) // Automation r.Get("/api/v1/automation/workflows", autoH.ListWorkflows) r.Post("/api/v1/automation/workflows", autoH.CreateWorkflow) r.Post("/api/v1/automation/workflows/{id}/trigger", autoH.TriggerWorkflow) r.Get("/api/v1/automation/jobs", autoH.ListScheduledJobs) r.Post("/api/v1/automation/jobs", autoH.CreateScheduledJob) r.Get("/api/v1/automation/runs", autoH.ListRuns) }) // WebSocket (protected) r.Get("/ws", func(w http.ResponseWriter, r *http.Request) { http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented) }) srv := &http.Server{ Addr: ":" + cfg.Port, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } go func() { logger.Info().Str("addr", srv.Addr).Msg("BOC server starting") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("listen error") } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info().Msg("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { logger.Error().Err(err).Msg("shutdown error") } }