package main import ( "context" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" "boc/auth" "boc/config" "boc/db" "boc/handlers" "boc/ledger" "boc/middleware" "boc/store" ) 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) // 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") } } // HS256 fallback for local dev _ = auth.NewService(database, cfg.JWTSecret) ledgerH := ledger.NewHandler() 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) 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) }) 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("/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":[]}}`)) }) // Ledger (proxy to aamos-ledger) 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) }) 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") } }