286 lines
7.3 KiB
Go
286 lines
7.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware"
|
|
pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart"
|
|
pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"
|
|
)
|
|
|
|
type cartSessionService interface {
|
|
RefreshExpiry(ctx context.Context, session *pscookie.SessionContext) error
|
|
ResolveCookiePath(ctx context.Context, req *http.Request) (string, error)
|
|
}
|
|
|
|
type CartHandler struct {
|
|
carts *pscart.Service
|
|
codec pscookie.Codec
|
|
sessions cartSessionService
|
|
}
|
|
|
|
func NewCartHandler(carts *pscart.Service, codec pscookie.Codec, sessions cartSessionService) *CartHandler {
|
|
return &CartHandler{
|
|
carts: carts,
|
|
codec: codec,
|
|
sessions: sessions,
|
|
}
|
|
}
|
|
|
|
func (h *CartHandler) Handle(c echo.Context) error {
|
|
if h == nil || h.carts == nil || h.codec == nil || h.sessions == nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "cart handler is not initialized")
|
|
}
|
|
|
|
session := appmiddleware.GetSession(c)
|
|
action := cartActionFromRequest(c)
|
|
|
|
input, err := cartMutationInput(c, session)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
}
|
|
|
|
var result *pscart.MutationResult
|
|
switch action {
|
|
case cartActionDelete:
|
|
result, err = h.carts.DeleteProduct(c.Request().Context(), input)
|
|
case cartActionUpdate:
|
|
result, err = h.carts.UpdateProduct(c.Request().Context(), input)
|
|
default:
|
|
result, err = h.carts.AddProduct(c.Request().Context(), input)
|
|
}
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "cart mutation failed: "+err.Error())
|
|
}
|
|
|
|
syncSessionCartID(session, result.CartID)
|
|
if err := h.writeSessionCookie(c, session); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "cart cookie update failed: "+err.Error())
|
|
}
|
|
|
|
if wantsJSON(c.Request()) {
|
|
return c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
return c.Redirect(http.StatusSeeOther, cartRedirectTarget(c.Request()))
|
|
}
|
|
|
|
type cartAction string
|
|
|
|
const (
|
|
cartActionAdd cartAction = "add"
|
|
cartActionUpdate cartAction = "update"
|
|
cartActionDelete cartAction = "delete"
|
|
)
|
|
|
|
func cartActionFromRequest(c echo.Context) cartAction {
|
|
switch c.Request().Method {
|
|
case http.MethodDelete:
|
|
return cartActionDelete
|
|
case http.MethodPut, http.MethodPatch:
|
|
return cartActionUpdate
|
|
}
|
|
|
|
action := strings.ToLower(strings.TrimSpace(c.FormValue("action")))
|
|
switch action {
|
|
case string(cartActionDelete):
|
|
return cartActionDelete
|
|
case string(cartActionUpdate):
|
|
return cartActionUpdate
|
|
}
|
|
|
|
switch {
|
|
case c.FormValue("delete") != "":
|
|
return cartActionDelete
|
|
case c.FormValue("update") != "":
|
|
return cartActionUpdate
|
|
default:
|
|
return cartActionAdd
|
|
}
|
|
}
|
|
|
|
func cartMutationInput(c echo.Context, session *pscookie.SessionContext) (pscart.MutationInput, error) {
|
|
productID, err := formInt64(c, "id_product")
|
|
if err != nil || productID == 0 {
|
|
return pscart.MutationInput{}, fmt.Errorf("valid id_product is required")
|
|
}
|
|
|
|
quantity := int64(1)
|
|
if raw := strings.TrimSpace(c.FormValue("qty")); raw != "" {
|
|
quantity, err = strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || quantity < 0 {
|
|
return pscart.MutationInput{}, fmt.Errorf("valid qty is required")
|
|
}
|
|
}
|
|
|
|
productAttributeID, err := firstFormInt64(c, "id_product_attribute", "ipa")
|
|
if err != nil {
|
|
return pscart.MutationInput{}, fmt.Errorf("valid id_product_attribute is required")
|
|
}
|
|
customizationID, err := firstFormInt64(c, "id_customization", "customization_id")
|
|
if err != nil {
|
|
return pscart.MutationInput{}, fmt.Errorf("valid id_customization is required")
|
|
}
|
|
|
|
return pscart.MutationInput{
|
|
CartID: int64Value(session.CartID),
|
|
ProductID: productID,
|
|
ProductAttributeID: productAttributeID,
|
|
CustomizationID: customizationID,
|
|
Quantity: quantity,
|
|
CustomerID: int64Value(session.CustomerID),
|
|
GuestID: int64Value(session.GuestID),
|
|
LanguageID: int64Value(session.LanguageID),
|
|
CurrencyID: int64Value(session.CurrencyID),
|
|
ShopID: int64Value(session.ShopID),
|
|
}, nil
|
|
}
|
|
|
|
func formInt64(c echo.Context, key string) (int64, error) {
|
|
raw := strings.TrimSpace(c.FormValue(key))
|
|
if raw == "" {
|
|
return 0, nil
|
|
}
|
|
return strconv.ParseInt(raw, 10, 64)
|
|
}
|
|
|
|
func firstFormInt64(c echo.Context, keys ...string) (int64, error) {
|
|
for _, key := range keys {
|
|
value, err := formInt64(c, key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if value != 0 {
|
|
return value, nil
|
|
}
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func syncSessionCartID(session *pscookie.SessionContext, cartID int64) {
|
|
if session == nil || cartID == 0 {
|
|
return
|
|
}
|
|
if session.Values == nil {
|
|
session.Values = map[string]string{}
|
|
}
|
|
|
|
session.CartID = int64Ptr(cartID)
|
|
session.Values["id_cart"] = strconv.FormatInt(cartID, 10)
|
|
session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "id_cart")
|
|
session.OrderedKeys = moveOrderedKeyToEnd(session.OrderedKeys, "checksum")
|
|
session.Plaintext = ""
|
|
session.RawCookie = ""
|
|
}
|
|
|
|
func appendOrderedKeyIfMissing(keys []string, key string) []string {
|
|
for _, existing := range keys {
|
|
if existing == key {
|
|
return keys
|
|
}
|
|
}
|
|
return append(keys, key)
|
|
}
|
|
|
|
func moveOrderedKeyToEnd(keys []string, key string) []string {
|
|
for i, existing := range keys {
|
|
if existing == key {
|
|
keys = append(keys[:i], keys[i+1:]...)
|
|
return append(keys, key)
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func int64Ptr(value int64) *int64 {
|
|
if value == 0 {
|
|
return nil
|
|
}
|
|
v := value
|
|
return &v
|
|
}
|
|
|
|
func int64Value(value *int64) int64 {
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func (h *CartHandler) writeSessionCookie(c echo.Context, session *pscookie.SessionContext) error {
|
|
if err := h.sessions.RefreshExpiry(c.Request().Context(), session); err != nil {
|
|
return err
|
|
}
|
|
|
|
cookiePath, err := h.sessions.ResolveCookiePath(c.Request().Context(), c.Request())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
encoded, err := h.codec.Encode(session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
session.RawCookie = encoded
|
|
writeSessionCookie(c.Request(), c.Response(), session, session.CookieName, encoded, cookiePath)
|
|
return nil
|
|
}
|
|
|
|
func writeSessionCookie(req *http.Request, res *echo.Response, session *pscookie.SessionContext, name, value, path string) {
|
|
maxAge := 1
|
|
if session != nil && session.ExpiresAt != nil {
|
|
maxAge = int(session.ExpiresAt.UTC().Unix())
|
|
}
|
|
if strings.TrimSpace(path) == "" {
|
|
path = "/"
|
|
}
|
|
|
|
header := fmt.Sprintf("%s=%s; path=%s; max-age=%d; HttpOnly; SameSite=Lax", name, value, path, maxAge)
|
|
if requestCookieSecure(req) {
|
|
header += "; Secure"
|
|
}
|
|
res.Header().Add(echo.HeaderSetCookie, header)
|
|
}
|
|
|
|
func requestCookieSecure(req *http.Request) bool {
|
|
if req == nil {
|
|
return false
|
|
}
|
|
if req.TLS != nil {
|
|
return true
|
|
}
|
|
forwarded := req.Header.Get("X-Forwarded-Proto")
|
|
if strings.Contains(forwarded, ",") {
|
|
forwarded = strings.TrimSpace(strings.Split(forwarded, ",")[0])
|
|
}
|
|
return strings.EqualFold(forwarded, "https")
|
|
}
|
|
|
|
func wantsJSON(req *http.Request) bool {
|
|
if req == nil {
|
|
return false
|
|
}
|
|
return strings.Contains(strings.ToLower(req.Header.Get(echo.HeaderAccept)), "application/json")
|
|
}
|
|
|
|
func cartRedirectTarget(req *http.Request) string {
|
|
if req == nil {
|
|
return "/cart?action=show"
|
|
}
|
|
referer := strings.TrimSpace(req.Referer())
|
|
if referer != "" {
|
|
return referer
|
|
}
|
|
path := requestLanguagePrefix(req) + "/cart"
|
|
if path == "/cart" {
|
|
return "/cart?action=show"
|
|
}
|
|
return path + "?action=show"
|
|
}
|