package main import ( "context" "encoding/json" "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" "database/sql" "boc/automation" "boc/briefing" "boc/config" "boc/db" "boc/employee" "boc/handlers" "boc/ledger" "boc/middleware" "boc/sms" "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() financeV2H := handlers.NewFinanceHandlerV2() // Briefing engine (legacy) briefingEngine := briefing.NewBriefingEngine(database) briefingH := handlers.NewBriefingHandler(briefingEngine) // Real Briefing engine (med riktig data från BOC + Ledger) ledgerDB, ledgerErr := sql.Open("postgres", cfg.LedgerDBURL) if ledgerErr != nil { logger.Fatal().Err(ledgerErr).Msg("Failed to connect to ledger database") } defer ledgerDB.Close() realBriefingH := handlers.NewRealBriefingHandler(database, ledgerDB) // Journal handler (totaljournal för drill-down) journalH := handlers.NewJournalHandler(ledgerDB) // Tenant handler (multi-tenancy) tenantH := handlers.NewTenantHandler(database) // Employee Lifecycle handler employeeH := employee.NewHandler(database) // SMS / 46elks integration elk46Client := sms.NewClient(nil) var smsStore sms.VerificationStore if cfg.RedisAddr != "" { smsStore = sms.NewRedisStore(cfg.RedisAddr) logger.Info().Str("redis", cfg.RedisAddr).Msg("SMS Redis store initialized") } else { // Fallback: in-memory store (endast för dev) logger.Warn().Msg("No Redis configured, SMS verification will not persist across restarts") } var twoFactor *sms.TwoFactorAuth var notifier *sms.NotificationService if elk46Client.IsConfigured() { if smsStore != nil { twoFactor = sms.NewTwoFactorAuth(elk46Client, smsStore) } notifier = sms.NewNotificationService(elk46Client) logger.Info().Msg("46elks SMS integration initialized") } else { logger.Warn().Msg("46elks not configured (set ELK46_USERNAME and ELK46_PASSWORD)") } smsH := handlers.NewSMSHandler(twoFactor, notifier) // Automation engine autoEngine := automation.NewEngine(database, logger) autoH := handlers.NewAutomationHandler(database, autoEngine) // Auth: JWTService med förbättrad validering jwtService := auth.NewJWTService(cfg.JWTSecret, "boc-auth", "boc") _ = jwtService // För utveckling: använd öppen auth authMiddleware := middleware.APIKeyAuth("") logger.Info().Msg("Development auth initialized (open access)") // 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("/api/v1/health", handlers.NewHealthHandler()) r.Get("/metrics", promhttp.Handler().ServeHTTP) r.Get("/debug/token", handlers.DebugTokenHandler(cfg.JWTSecret)) // Auth endpoints (no auth required) r.Post("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } // Generera token direkt (förenklad för nu) token, err := jwtService.GenerateToken("3847477b-3d56-4975-9157-ae8f9ce52aa7", req.Email, "admin") if err != nil { http.Error(w, `{"error":"token generation failed"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "ok": true, "token": token, "token_type": "Bearer", "expires_in": 2592000, // 30 dagar "algorithm": "HS256", "user": map[string]string{ "id": "3847477b-3d56-4975-9157-ae8f9ce52aa7", "email": req.Email, "name": "Erik Svensson", "role": "admin", }, }) }) // 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 { // För utveckling: returnera default user w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"user":{"sub":"3847477b-3d56-4975-9157-ae8f9ce52aa7","email":"erik@landvex.com","roles":["admin"]}}`)) 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", financeV2H.GetBalanceSheet) r.Get("/api/v1/finance/income", financeV2H.GetIncomeStatement) r.Get("/api/v1/finance/moms", financeV2H.GetMomsReport) r.Get("/api/v1/finance/accounts", financeV2H.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) r.Get("/api/v1/finance/transactions", ledgerH.GetTransactions) // 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/templates", legalH.GetContractTemplates) r.Get("/api/v1/legal/templates/{type}", legalH.GetContractTemplate) r.Get("/api/v1/legal/product-links", legalH.GetProductContractLinks) // 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) // Briefing (Intelligent Daily Briefing Engine) r.Get("/api/v1/briefing/daily", briefingH.GetDailyBriefing) r.Get("/api/v1/briefing/priority", briefingH.GetPriority) r.Get("/api/v1/briefing/alerts", briefingH.GetAlerts) r.Get("/api/v1/briefing/recommendations", briefingH.GetRecommendations) r.Get("/api/v1/briefing/work-plan", briefingH.GetWorkPlan) // Real Briefing (med riktig data från BOC + Ledger) r.Get("/api/v1/briefing/real", realBriefingH.GetRealBriefing) // Journal (Totaljournal för drill-down) r.Get("/api/v1/journal/entries", journalH.GetJournalEntries) r.Get("/api/v1/journal/entries/{id}", journalH.GetJournalEntry) r.Get("/api/v1/journal/accounts/{code}/transactions", journalH.GetAccountTransactions) r.Post("/api/v1/journal/drill-down", journalH.PostDrillDown) // Multi-Tenancy r.Get("/api/v1/tenants", tenantH.ListTenants) r.Get("/api/v1/tenants/current", tenantH.GetTenant) r.Get("/api/v1/tenants/hierarchy", tenantH.GetTenantHierarchy) r.Post("/api/v1/tenants/switch", tenantH.SwitchTenant) r.Get("/api/v1/tenants/summary", tenantH.GetTenantSummary) // Employee Lifecycle r.Get("/api/v1/employees", employeeH.ListEmployees) r.Post("/api/v1/employees", employeeH.CreateEmployee) r.Get("/api/v1/employees/{id}", employeeH.GetEmployee) r.Put("/api/v1/employees/{id}", employeeH.UpdateEmployee) r.Get("/api/v1/employees/{id}/timeline", employeeH.GetTimeline) r.Post("/api/v1/employees/{id}/timeline", employeeH.AddTimelineEvent) r.Get("/api/v1/employees/{id}/competences", employeeH.GetCompetences) r.Post("/api/v1/employees/{id}/competences", employeeH.AddCompetence) r.Get("/api/v1/employees/{id}/documents", employeeH.GetDocuments) r.Get("/api/v1/employees/{id}/trainings", employeeH.GetTrainings) r.Get("/api/v1/employees/{id}/tasks", employeeH.GetTasks) r.Get("/api/v1/employees/{id}/performance", employeeH.GetPerformance) // SMS / 46elks r.Get("/api/v1/sms/status", smsH.SMSStatus) r.Post("/api/v1/sms/verify/send", smsH.SendVerificationCode) r.Post("/api/v1/sms/verify/check", smsH.VerifyCode) r.Post("/api/v1/sms/notify", smsH.SendNotification) // Legal (Contracts) r.Get("/api/v1/legal/contracts", legalH.ListContracts) r.Get("/api/v1/legal/contracts/{id}", legalH.GetContract) r.Post("/api/v1/legal/contracts", legalH.CreateContract) r.Put("/api/v1/legal/contracts/{id}", legalH.UpdateContract) r.Get("/api/v1/legal/templates", legalH.GetContractTemplates) r.Get("/api/v1/legal/templates/{type}", legalH.GetContractTemplate) r.Get("/api/v1/legal/product-links", legalH.GetProductContractLinks) // 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") } }