Files
boc/backend/main.go
T

255 lines
8.6 KiB
Go

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")
}
}