package server import ( "encoding/json" "log/slog" "net/http" "strings" "git.g3e.fr/H6N/account/internal/auth" "git.g3e.fr/H6N/account/internal/config" "git.g3e.fr/H6N/account/internal/handler" "git.g3e.fr/H6N/account/internal/pipeline" "git.g3e.fr/H6N/account/internal/snapshot" "git.g3e.fr/H6N/account/internal/store" "github.com/jackc/pgx/v5/pgxpool" ) type Server struct { mux *http.ServeMux pool *pgxpool.Pool logger *slog.Logger cfg *config.Config } func New(cfg *config.Config, pool *pgxpool.Pool, logger *slog.Logger) *Server { s := &Server{ mux: http.NewServeMux(), pool: pool, logger: logger, cfg: cfg, } s.routes() return s } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { s.mux.ServeHTTP(w, r) return } auth.Middleware(s.mux).ServeHTTP(w, r) })).ServeHTTP(w, r) } func (s *Server) corsMiddleware(next http.Handler) http.Handler { allowedOrigins := strings.Split(s.cfg.CORSAllowedOrigins, ",") wildcard := len(allowedOrigins) == 1 && strings.TrimSpace(allowedOrigins[0]) == "*" return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" { allowed := wildcard if !allowed { for _, o := range allowedOrigins { if strings.TrimSpace(o) == origin { allowed = true break } } } if allowed { if wildcard { w.Header().Set("Access-Control-Allow-Origin", "*") } else { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Add("Vary", "Origin") } w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Owner-ID") w.Header().Set("Access-Control-Max-Age", "86400") } } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Server) routes() { st := store.New(s.pool) horizonDays := 30 var pipelineCfg pipeline.Config if s.cfg != nil { horizonDays = s.cfg.SnapshotHorizonDays pipelineCfg = pipeline.Config{ OpenFIGIKey: s.cfg.OpenFIGIKey, CoinGeckoKey: s.cfg.CoinGeckoKey, } } pl := pipeline.New(st, pipelineCfg, s.logger) eng := snapshot.New(st, s.logger, horizonDays).WithBackfiller(pl) handler.NewInstrumentHandler(st).RegisterRoutes(s.mux) handler.NewAccountHandler(st).RegisterRoutes(s.mux) handler.NewTransactionHandler(st).RegisterRoutes(s.mux) handler.NewSnapshotHandler(eng).RegisterRoutes(s.mux) s.mux.HandleFunc("GET /health", s.handleHealth) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { if err := s.pool.Ping(r.Context()); err != nil { s.logger.Error("health check failed", "error", err) http.Error(w, "database unavailable", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }