package main import ( "context" "database/sql" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/labstack/echo/v4" "git.ma-al.com/goc_marek/ps_shop/internal/assets" "git.ma-al.com/goc_marek/ps_shop/internal/http/handlers" appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware" httpproxy "git.ma-al.com/goc_marek/ps_shop/internal/http/proxy" pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config" pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes" pssession "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/session" "git.ma-al.com/goc_marek/ps_shop/internal/render" "git.ma-al.com/goc_marek/ps_shop/internal/store" ) func main() { if err := run(); err != nil { slog.Error("fatal", "error", err) os.Exit(1) } } func run() error { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) slog.SetDefault(logger) cfg, err := psconfig.Load() if err != nil { return fmt.Errorf("load config: %w", err) } appStore, err := store.Open(cfg.PrestaShopDBDialect, cfg.AppDBDSN) if err != nil { return fmt.Errorf("open app db: %w", err) } defer closeDB("app db", appStore) prestaDB, err := store.OpenPresta(cfg.PrestaShopDBDialect, cfg.PrestaShopDBDSN) if err != nil { return fmt.Errorf("open prestashop db: %w", err) } defer closeDB("prestashop db", prestaDB) assetManifest, err := assets.LoadManifest(cfg.AssetManifestPath) if err != nil { return fmt.Errorf("load asset manifest: %w", err) } cookieCodec, err := pscookie.NewCodec(cfg.CookieConfig()) if err != nil { return fmt.Errorf("create cookie codec: %w", err) } productService := pscatalog.NewService(prestaDB, cfg.PrestaShopTablePrefix) customerService := pscustomer.NewService(prestaDB, cfg.PrestaShopTablePrefix) cartService := pscart.NewService(prestaDB, cfg.PrestaShopTablePrefix) routeService := psroutes.NewService(prestaDB, cfg.PrestaShopTablePrefix) sessionService := pssession.NewService( prestaDB, cfg.PrestaShopTablePrefix, cfg.PrestaShopVersion, cfg.PrestaShopCookieName, cfg.DomainCookie, ) productRoute, err := routeService.LoadProductRoute(context.Background()) if err != nil { return fmt.Errorf("load product route rule: %w", err) } categoryRoute, err := routeService.LoadCategoryRoute(context.Background()) if err != nil { return fmt.Errorf("load category route rule: %w", err) } productHandler := handlers.NewProductHandler( productService, customerService, cartService, render.New(assetManifest), cfg, productRoute, categoryRoute, ) categoryHandler := handlers.NewCategoryHandler( productService, customerService, cartService, render.New(assetManifest), cfg, productRoute, categoryRoute, ) cartHandler := handlers.NewCartHandler(cartService, cookieCodec, sessionService) cartPageHandler := handlers.NewCartPageHandler( productService, customerService, cartService, render.New(assetManifest), cfg, productRoute, categoryRoute, ) proxyHandler, err := httpproxy.New(cfg.PrestaShopProxyTarget) if err != nil { return fmt.Errorf("create reverse proxy: %w", err) } e := echo.New() e.HideBanner = true e.HidePort = true e.HTTPErrorHandler = appmiddleware.HTTPErrorHandler(logger) e.Use( appmiddleware.RequestID(), appmiddleware.RealIP(), appmiddleware.AccessLog(logger), appmiddleware.Recover(logger), appmiddleware.Session(cfg, cookieCodec, sessionService, productService, psroutes.CombineMatchers(productRoute, categoryRoute)), ) e.Static("/dist", "web/dist") e.GET("/healthz", handlers.Healthz()) e.GET("/readyz", handlers.Readyz(appStore, prestaDB, cfg.PrestaShopProxyTarget)) e.Match([]string{http.MethodGet, http.MethodPost}, "/debug/cookie/decode", handlers.DecodeCookie(cookieCodec)) e.GET("/cart", cartPageHandler.Show) e.GET("/:lang/cart", cartPageHandler.Show) e.Match([]string{http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete}, "/cart", cartHandler.Handle) e.Match([]string{http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete}, "/:lang/cart", cartHandler.Handle) e.GET("/*", func(c echo.Context) error { productMatch, productOK := productRoute.MatchInfo(c.Request().URL.Path) categoryMatch, categoryOK := categoryRoute.MatchInfo(c.Request().URL.Path) productSlug := "" productID := int64(0) if productOK { productSlug = productMatch.Slug productID = productMatch.ID } categorySlug := "" categoryID := int64(0) if categoryOK { categorySlug = categoryMatch.Slug categoryID = categoryMatch.ID } slog.Info("route dispatch probe", "method", c.Request().Method, "path", c.Request().URL.Path, "product_match", productOK, "product_id", productID, "product_slug", productSlug, "category_match", categoryOK, "category_id", categoryID, "category_slug", categorySlug, ) if !productOK { if categoryOK { slog.Info("route dispatch", "owner", "go-category", "method", c.Request().Method, "path", c.Request().URL.Path, "slug", categorySlug, "id", categoryID) handlers.SetCategoryID(c, categoryID) handlers.SetCategorySlug(c, categorySlug) return categoryHandler.Show(c) } slog.Info("route dispatch", "owner", "prestashop", "method", c.Request().Method, "path", c.Request().URL.Path) return proxyHandler.Handle(c) } slog.Info("route dispatch", "owner", "go", "method", c.Request().Method, "path", c.Request().URL.Path, "id", productID, "slug", productSlug) handlers.SetProductID(c, productID) handlers.SetProductSlug(c, productSlug) return productHandler.Show(c) }) e.Match([]string{ http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions, http.MethodConnect, http.MethodTrace, }, "/*", func(c echo.Context) error { slog.Info("route dispatch", "owner", "prestashop", "method", c.Request().Method, "path", c.Request().URL.Path) return proxyHandler.Handle(c) }) server := &http.Server{ Addr: cfg.AppAddr, Handler: e, ReadHeaderTimeout: 5 * time.Second, } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() shutdownErrCh := make(chan error, 1) go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := e.Shutdown(shutdownCtx) if errors.Is(err, http.ErrServerClosed) { err = nil } shutdownErrCh <- err }() slog.Info("starting server", "addr", cfg.AppAddr, "proxy_target", cfg.PrestaShopProxyTarget) if err := e.StartServer(server); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } if ctx.Err() != nil { if err := <-shutdownErrCh; err != nil { return fmt.Errorf("shutdown server: %w", err) } } return nil } func closeDB(name string, db interface{ DB() (*sql.DB, error) }) { sqlDB, err := db.DB() if err != nil { slog.Error("resolve db handle", "name", name, "error", err) return } if err := sqlDB.Close(); err != nil { slog.Error("close db", "name", name, "error", err) } }