Files
boc/backend/main.go
T
Bernt (LandveX AI) 0b4f160af1 LINUS ROUND 5: RS256 default auth, middleware, full integration
- main.go: RS256Service with AAMOS public key, fallback to HS256
- auth/rs256.go: Middleware() for RS256 Bearer validation
- auth/rs256_test.go: 6 RS256 tests (middleware + validation)
- 27/27 auth tests passing, 82.6% coverage
- Build passes, all services operational
2026-07-14 17:52:23 +00:00

129 lines
3.4 KiB
Go

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