Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into test

This commit is contained in:
2026-03-12 13:56:33 +01:00
7 changed files with 159 additions and 23 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
***/node_modules/***
tmp
tmp/
assets/public/dist
bin/
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/service/productDescriptionService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
@@ -30,7 +31,7 @@ func ProductDescriptionHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductDescriptionHandler()
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-issues", handler.GetIssues)
@@ -42,7 +43,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(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 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)
}
// 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
// User represents a user in the system
// ProductDescription contains all the information visible on webpage, in given language.
type ProductDescription struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id"`
ShopID uint `gorm:"column:id_shop;primaryKey" json:"shop_id"`
LangID uint `gorm:"column:id_lang;primaryKey" json:"lang_id"`
Description string `gorm:"column:description;type:text" json:"description"`
DescriptionShort string `gorm:"column:description_short;type:text" json:"description_short"`
LinkRewrite string `gorm:"column:link_rewrite;type:varchar(128)" json:"link_rewrite"`
MetaDescription string `gorm:"column:meta_description;type:varchar(512)" json:"meta_description"`
MetaKeywords string `gorm:"column:meta_keywords;type:varchar(255)" json:"meta_keywords"`
MetaTitle string `gorm:"column:meta_title;type:varchar(128)" json:"meta_title"`
Name string `gorm:"column:name;type:varchar(128)" json:"name"`
AvailableNow string `gorm:"column:available_now;type:varchar(255)" json:"available_now"`
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later"`
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock"`
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock"`
Usage string `gorm:"column:usage;type:text" json:"usage"`
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
ShopID uint `gorm:"column:id_shop;primaryKey" json:"shop_id" form:"shop_id"`
LangID uint `gorm:"column:id_lang;primaryKey" json:"lang_id" form:"lang_id"`
Description string `gorm:"column:description;type:text" json:"description" form:"description"`
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" form:"link_rewrite"`
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" form:"meta_keywords"`
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" form:"name"`
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" form:"available_later"`
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" form:"delivery_out_stock"`
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
}

View File

@@ -1,10 +1,15 @@
package productDescriptionService
import (
"encoding/xml"
"fmt"
"io"
"slices"
"strings"
"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/utils/responseErrors"
"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
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
var ProductDescription model.ProductDescription
fmt.Println(userID, productID, productShopID, productLangID)
err := s.db.
Table("ps_product_lang").
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
}
// 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) {
// var repoIDs []uint

View File

@@ -39,6 +39,8 @@ var (
// Typed errors for product description handler
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
@@ -110,6 +112,10 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadAttribute):
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:
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, ErrVerificationTokenExpired),
errors.Is(err, ErrInvalidPassword),
errors.Is(err, ErrBadAttribute):
errors.Is(err, ErrBadAttribute),
errors.Is(err, ErrBadField),
errors.Is(err, ErrInvalidHTML):
return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists):
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.