0b4f160af1
- 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
129 lines
3.4 KiB
Go
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")
|
|
}
|
|
}
|