added new endpoint to save product descriptions

This commit is contained in:
Daniel Goc
2026-03-12 12:20:18 +01:00
parent 395c09f7e1
commit 098f559b5f
7 changed files with 159 additions and 23 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
***/node_modules/*** ***/node_modules/***
tmp tmp/
assets/public/dist assets/public/dist
bin/ bin/
i18n/*.json i18n/*.json

View File

@@ -5,6 +5,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/productDescriptionService" "git.ma-al.com/goc_daniel/b2b/app/service/productDescriptionService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@@ -30,7 +31,7 @@ func ProductDescriptionHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductDescriptionHandler() handler := NewProductDescriptionHandler()
r.Get("/get-product-description", handler.GetProductDescription) r.Get("/get-product-description", handler.GetProductDescription)
// r.Get("/get-years", handler.GetYears) r.Post("/save-product-description", handler.SaveProductDescription)
// r.Get("/get-quarters", handler.GetQuarters) // r.Get("/get-quarters", handler.GetQuarters)
// r.Get("/get-issues", handler.GetIssues) // r.Get("/get-issues", handler.GetIssues)
@@ -42,7 +43,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a dfifferent error "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a different error
}) })
} }
@@ -79,3 +80,55 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
return c.JSON(response) return c.JSON(response)
} }
// SaveProductDescription saves the description for a given product ID, in given shop and language
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a different error
})
}
productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute),
})
}
productShopID_attribute := c.Query("productShopID")
productShopID, err := strconv.Atoi(productShopID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute),
})
}
productLangID_attribute := c.Query("productLangID")
productLangID, err := strconv.Atoi(productLangID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute),
})
}
updates := make(map[string]string)
if err := c.Bind().Body(&updates); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
}
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
}
return c.JSON(fiber.Map{
"message": i18n.T_(c, "product_description.successfully_updated_fields"),
})
}

View File

@@ -1,20 +1,20 @@
package model package model
// User represents a user in the system // ProductDescription contains all the information visible on webpage, in given language.
type ProductDescription struct { type ProductDescription struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id"` ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
ShopID uint `gorm:"column:id_shop;primaryKey" json:"shop_id"` ShopID uint `gorm:"column:id_shop;primaryKey" json:"shop_id" form:"shop_id"`
LangID uint `gorm:"column:id_lang;primaryKey" json:"lang_id"` LangID uint `gorm:"column:id_lang;primaryKey" json:"lang_id" form:"lang_id"`
Description string `gorm:"column:description;type:text" json:"description"` Description string `gorm:"column:description;type:text" json:"description" form:"description"`
DescriptionShort string `gorm:"column:description_short;type:text" json:"description_short"` DescriptionShort string `gorm:"column:description_short;type:text" json:"description_short" form:"description_short"`
LinkRewrite string `gorm:"column:link_rewrite;type:varchar(128)" json:"link_rewrite"` LinkRewrite string `gorm:"column:link_rewrite;type:varchar(128)" json:"link_rewrite" form:"link_rewrite"`
MetaDescription string `gorm:"column:meta_description;type:varchar(512)" json:"meta_description"` MetaDescription string `gorm:"column:meta_description;type:varchar(512)" json:"meta_description" form:"meta_description"`
MetaKeywords string `gorm:"column:meta_keywords;type:varchar(255)" json:"meta_keywords"` MetaKeywords string `gorm:"column:meta_keywords;type:varchar(255)" json:"meta_keywords" form:"meta_keywords"`
MetaTitle string `gorm:"column:meta_title;type:varchar(128)" json:"meta_title"` MetaTitle string `gorm:"column:meta_title;type:varchar(128)" json:"meta_title" form:"meta_title"`
Name string `gorm:"column:name;type:varchar(128)" json:"name"` Name string `gorm:"column:name;type:varchar(128)" json:"name" form:"name"`
AvailableNow string `gorm:"column:available_now;type:varchar(255)" json:"available_now"` AvailableNow string `gorm:"column:available_now;type:varchar(255)" json:"available_now" form:"available_now"`
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later"` AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"`
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock"` DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"`
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock"` DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
Usage string `gorm:"column:usage;type:text" json:"usage"` Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
} }

View File

@@ -1,10 +1,15 @@
package productDescriptionService package productDescriptionService
import ( import (
"encoding/xml"
"fmt" "fmt"
"io"
"slices"
"strings"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -18,12 +23,29 @@ func New() *ProductDescriptionService {
} }
} }
func isValidXHTML(s string) bool {
decoder := xml.NewDecoder(strings.NewReader(s))
hasStartTag := false
for {
tok, err := decoder.Token()
if err != nil {
if err == io.EOF {
return hasStartTag
}
return false
}
if _, ok := tok.(xml.StartElement); ok {
hasStartTag = true
}
}
}
// We assume that any user has access to all product descriptions // We assume that any user has access to all product descriptions
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
var ProductDescription model.ProductDescription var ProductDescription model.ProductDescription
fmt.Println(userID, productID, productShopID, productLangID)
err := s.db. err := s.db.
Table("ps_product_lang"). Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
@@ -35,6 +57,59 @@ func (s *ProductDescriptionService) GetProductDescription(userID uint, productID
return &ProductDescription, nil return &ProductDescription, nil
} }
// Updates relevant fields with the "updates" map
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
// only some fields can be affected
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
for key := range updates {
if !slices.Contains(allowedFields, key) {
return responseErrors.ErrBadField
}
}
// check that fields description, description_short and usage, if they exist, have a valid html format
mustBeHTML := []string{"description", "description_short", "usage"}
for i := 0; i < len(mustBeHTML); i++ {
if text, exists := updates[mustBeHTML[i]]; exists {
if !isValidXHTML(text) {
return responseErrors.ErrInvalidHTML
}
}
}
record := model.ProductDescription{
ProductID: productID,
ShopID: productShopID,
LangID: productLangID,
}
err := s.db.
Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
FirstOrCreate(&record).Error
if err != nil {
return fmt.Errorf("database error: %w", err)
}
if len(updates) == 0 {
return nil
}
updatesIface := make(map[string]interface{}, len(updates))
for k, v := range updates {
updatesIface[k] = v
}
err = s.db.
Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
Updates(updatesIface).Error
if err != nil {
return fmt.Errorf("database error: %w", err)
}
return nil
}
// func (s *ProductDescriptionService) GetRepositoriesForUser(userID uint) ([]uint, error) { // func (s *ProductDescriptionService) GetRepositoriesForUser(userID uint) ([]uint, error) {
// var repoIDs []uint // var repoIDs []uint

View File

@@ -39,6 +39,8 @@ var (
// Typed errors for product description handler // Typed errors for product description handler
ErrBadAttribute = errors.New("bad attribute") ErrBadAttribute = errors.New("bad attribute")
ErrBadField = errors.New("this field can not be updated")
ErrInvalidHTML = errors.New("text is not in html format")
) )
// Error represents an error with HTTP status code // Error represents an error with HTTP status code
@@ -110,6 +112,10 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadAttribute): case errors.Is(err, ErrBadAttribute):
return i18n.T_(c, "error.err_bad_attribute") return i18n.T_(c, "error.err_bad_attribute")
case errors.Is(err, ErrBadField):
return i18n.T_(c, "error.err_bad_field")
case errors.Is(err, ErrInvalidHTML):
return i18n.T_(c, "error.err_invalid_html")
default: default:
return i18n.T_(c, "error.err_internal_server_error") return i18n.T_(c, "error.err_internal_server_error")
@@ -140,7 +146,9 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidVerificationToken), errors.Is(err, ErrInvalidVerificationToken),
errors.Is(err, ErrVerificationTokenExpired), errors.Is(err, ErrVerificationTokenExpired),
errors.Is(err, ErrInvalidPassword), errors.Is(err, ErrInvalidPassword),
errors.Is(err, ErrBadAttribute): errors.Is(err, ErrBadAttribute),
errors.Is(err, ErrBadField),
errors.Is(err, ErrInvalidHTML):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict

View File

@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
tmp/main

Binary file not shown.