diff --git a/.gitignore b/.gitignore index 027e001..b5e0d12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ ***/node_modules/*** -tmp +tmp/ assets/public/dist bin/ i18n/*.json diff --git a/app/delivery/web/api/restricted/productDescription.go b/app/delivery/web/api/restricted/productDescription.go index ddafe32..857ae5e 100644 --- a/app/delivery/web/api/restricted/productDescription.go +++ b/app/delivery/web/api/restricted/productDescription.go @@ -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"), + }) +} diff --git a/app/model/productDescription.go b/app/model/productDescription.go index 9a6287e..2411f59 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -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"` } diff --git a/app/service/productDescriptionService/productDescriptionService.go b/app/service/productDescriptionService/productDescriptionService.go index 698d6b0..3c24c03 100644 --- a/app/service/productDescriptionService/productDescriptionService.go +++ b/app/service/productDescriptionService/productDescriptionService.go @@ -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 diff --git a/app/utils/responseErrors/response_errors.go b/app/utils/responseErrors/response_errors.go index 85e6091..a9300ed 100644 --- a/app/utils/responseErrors/response_errors.go +++ b/app/utils/responseErrors/response_errors.go @@ -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 diff --git a/tmp/build-errors.log b/tmp/build-errors.log index cf4bfce..bad11c8 100644 --- a/tmp/build-errors.log +++ b/tmp/build-errors.log @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/tmp/main b/tmp/main index 5545b4c..8c9caa1 100755 Binary files a/tmp/main and b/tmp/main differ