Files
ps_shop/cmd/proxy/main.go
T
2026-05-14 01:48:15 +02:00

243 lines
7.3 KiB
Go

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