BOC v1.0: RS256 auth, Ledger integration, Prometheus metrics, CI/CD, backup
This commit is contained in:
+159
-33
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -10,10 +11,13 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -22,6 +26,17 @@ import (
|
||||
"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()
|
||||
@@ -38,19 +53,53 @@ func main() {
|
||||
|
||||
_ = store.New(database)
|
||||
|
||||
// RS256 auth service (AAMOS standard)
|
||||
var authService *auth.RS256Service
|
||||
if _, err := os.Stat("auth/jwt-public.pem"); err == nil {
|
||||
authService, err = auth.NewRS256Service("auth/jwt-public.pem")
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("RS256 init failed, falling back to HS256")
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
|
||||
// HS256 fallback for local dev
|
||||
_ = auth.NewService(database, cfg.JWTSecret)
|
||||
|
||||
ledgerH := ledger.NewHandler()
|
||||
// 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)
|
||||
@@ -58,30 +107,32 @@ func main() {
|
||||
r.Use(hlog.RequestIDHandler("req_id", "X-Request-ID"))
|
||||
r.Use(middleware.Logger(logger))
|
||||
r.Use(chimw.Recoverer)
|
||||
|
||||
r.Get("/health", handlers.NewHealthHandler())
|
||||
|
||||
// Auth endpoints
|
||||
r.Post("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Try RS256 first, fall back to HS256
|
||||
if authService != nil {
|
||||
// Forward to ouroboros-identity for RS256 tokens
|
||||
http.Redirect(w, r, "http://localhost:3208/api/auth/token", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
// Local HS256 fallback
|
||||
hs256AuthHandler := &handlers.AuthHandler{DB: database, JWTSecret: []byte(cfg.JWTSecret)}
|
||||
hs256AuthHandler.Login(w, r)
|
||||
// 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.Group(func(r chi.Router) {
|
||||
// Use RS256 if available, otherwise HS256
|
||||
if authService != nil {
|
||||
r.Use(authService.Middleware())
|
||||
} else {
|
||||
r.Use(middleware.Auth(cfg))
|
||||
}
|
||||
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 {
|
||||
@@ -92,12 +143,87 @@ func main() {
|
||||
w.Write([]byte(`{"user":{"sub":"` + claims.Sub + `","email":"` + claims.Email + `","roles":[]}}`))
|
||||
})
|
||||
|
||||
// Ledger (proxy to aamos-ledger)
|
||||
// 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{
|
||||
|
||||
Reference in New Issue
Block a user