package httpx import ( "context" "net/http" "strings" "time" "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" "royal-pop-backend/internal/config" "royal-pop-backend/internal/database" "royal-pop-backend/internal/inventory" "royal-pop-backend/internal/mailer" "royal-pop-backend/internal/orders" ) type RouterConfig struct { Config *config.Config DB *database.DB Orders *orders.Store Stock *inventory.Store Mailer *mailer.ResendMailer } func NewRouter(cfg RouterConfig) http.Handler { router := chi.NewRouter() router.Use(chimiddleware.RealIP) router.Use(chimiddleware.RequestID) router.Use(chimiddleware.Recoverer) router.Use(chimiddleware.Timeout(30 * time.Second)) router.Use(corsMiddleware(cfg.Config.AllowedOrigins)) router.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { databaseStatus := "disabled" if cfg.DB != nil { healthCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := cfg.DB.Health(healthCtx); err != nil { WriteJSON(w, http.StatusServiceUnavailable, map[string]any{ "service": cfg.Config.AppName, "status": "degraded", "database": "unreachable", }) return } databaseStatus = "ok" } WriteJSON(w, http.StatusOK, map[string]any{ "service": cfg.Config.AppName, "status": "ok", "database": databaseStatus, }) }) router.Post("/webhooks/stripe", handleStripeWebhook(cfg.Config, cfg.Orders, cfg.Mailer)) router.Route("/v1", func(api chi.Router) { api.Get("/inventory", handleListPublicInventory(cfg.Stock)) api.Get("/storefront/pricing", handleGetStorefrontPricing(cfg.Config)) api.Get("/", func(w http.ResponseWriter, _ *http.Request) { WriteJSON(w, http.StatusOK, map[string]any{ "service": cfg.Config.AppName, "status": "scaffolded", "version": "v1", }) }) api.Post("/checkout/payment-intent", handleCreatePaymentIntent(cfg.Config, cfg.Orders)) api.Get("/checkout/payment-intent/{paymentIntentID}", handleGetPaymentIntentStatus(cfg.Config, cfg.Orders)) api.Get("/client/session", handleGetClientSession(cfg.Config)) api.Post("/client/login", handleClientLogin(cfg.Config)) api.Post("/client/logout", handleClientLogout(cfg.Config)) api.Group(func(client chi.Router) { client.Use(requireClientSession(cfg.Config)) client.Get("/client/orders", handleListClientOrders(cfg.Orders)) client.Get("/client/orders/{orderID}", handleGetClientOrder(cfg.Orders)) client.Patch("/client/orders/{orderID}", handleUpdateClientOrder(cfg.Orders, cfg.Mailer)) client.Get("/client/stock", handleListClientStock(cfg.Stock)) client.Put("/client/stock", handleUpsertClientStock(cfg.Stock)) client.Post("/client/reseed", handleClientReseed(cfg.DB)) }) }) router.NotFound(func(w http.ResponseWriter, _ *http.Request) { WriteError(w, http.StatusNotFound, "not_found", "The requested endpoint does not exist.") }) return router } func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler { normalized := make([]string, 0, len(allowedOrigins)) for _, origin := range allowedOrigins { trimmed := strings.TrimSpace(origin) if trimmed == "" { continue } normalized = append(normalized, trimmed) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" { for _, allowedOrigin := range normalized { if allowedOrigin == "*" || strings.EqualFold(allowedOrigin, origin) { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Credentials", "true") break } } } w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, OPTIONS") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } }