diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 30651f8..554a549 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "strconv" "strings" "git.ma-al.com/goc_daniel/b2b/app/config" @@ -60,10 +61,53 @@ func AuthMiddleware() fiber.Handler { }) } - // Set user in context - c.Locals(constdata.USER_LOCALES_NAME, user.ToSession()) - c.Locals(constdata.USER_LOCALES_ID, user.ID) - c.Locals(constdata.LANG_LOCALES_ID, user.LangID) + // Create locale. LangID is overwritten by auth Token + var userLocale model.UserLocale + userLocale.OriginalUser = user + + // Check if target user is present + targetUserIDAttribute := c.Query("target_user_id") + + if targetUserIDAttribute == "" { + userLocale.User = user + c.Locals(constdata.USER_LOCALE, &userLocale) + + return c.Next() + } + + // We now populate the target user + if user.Role != model.RoleAdmin { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "admin access required", + }) + } + + targetUserID, err := strconv.Atoi(targetUserIDAttribute) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "invalid target user id attribute", + }) + } + + // to verify target user, we use the same functionality as for verifying original user + // Get target user from database + user, err = authService.GetUserByID(uint(targetUserID)) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "target user not found", + }) + } + + // Check if target user is active + if !user.IsActive { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "target user account is inactive", + }) + } + + userLocale.User = user + c.Locals(constdata.USER_LOCALE, &userLocale) + return c.Next() } } @@ -95,24 +139,6 @@ func RequireAdmin() fiber.Handler { } } -// GetUserID extracts user ID from context -func GetUserID(c fiber.Ctx) uint { - userID, ok := c.Locals("userID").(uint) - if !ok { - return 0 - } - return userID -} - -// GetUser extracts user from context -func GetUser(c fiber.Ctx) *model.UserSession { - user, ok := c.Locals("user").(*model.UserSession) - if !ok { - return nil - } - return user -} - // GetConfig returns the app config func GetConfig() *config.Config { return config.Get() diff --git a/app/delivery/middleware/language.go b/app/delivery/middleware/language.go index e3fd5ca..c23a8e7 100644 --- a/app/delivery/middleware/language.go +++ b/app/delivery/middleware/language.go @@ -4,7 +4,9 @@ import ( "strconv" "strings" + "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/langsService" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "github.com/gofiber/fiber/v3" ) @@ -22,12 +24,8 @@ func LanguageMiddleware() fiber.Handler { if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil { langID = uint(id) if langID > 0 { - lang, err := langService.GetLanguageById(langID) - if err == nil { - c.Locals("langID", langID) - c.Locals("lang", lang) - return c.Next() - } + c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) + return c.Next() } } } @@ -38,12 +36,8 @@ func LanguageMiddleware() fiber.Handler { if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil { langID = uint(id) if langID > 0 { - lang, err := langService.GetLanguageById(langID) - if err == nil { - c.Locals("langID", langID) - c.Locals("lang", lang) - return c.Next() - } + c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) + return c.Next() } } } @@ -57,8 +51,7 @@ func LanguageMiddleware() fiber.Handler { lang, err := langService.GetLanguageByISOCode(isoCode) if err == nil && lang != nil { langID = uint(lang.ID) - c.Locals("langID", langID) - c.Locals("lang", lang) + c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) return c.Next() } } @@ -68,8 +61,7 @@ func LanguageMiddleware() fiber.Handler { defaultLang, err := langService.GetDefaultLanguage() if err == nil && defaultLang != nil { langID = uint(defaultLang.ID) - c.Locals("langID", langID) - c.Locals("lang", defaultLang) + c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) } return c.Next() @@ -104,11 +96,9 @@ func parseAcceptLanguage(header string) string { return strings.ToLower(first) } -// GetLanguageID extracts language ID from context -func GetLanguageID(c fiber.Ctx) uint { - langID, ok := c.Locals("langID").(uint) - if !ok { - return 0 - } - return langID +func returnNewLocale(lang_id uint) *model.UserLocale { + newLocale := model.UserLocale{} + newLocale.OriginalUser = &model.Customer{} + newLocale.OriginalUser.LangID = lang_id + return &newLocale } diff --git a/app/delivery/web/api/public/auth.go b/app/delivery/web/api/public/auth.go index edf67f3..5cb4e52 100644 --- a/app/delivery/web/api/public/auth.go +++ b/app/delivery/web/api/public/auth.go @@ -268,15 +268,15 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error { // Me returns the current user info func (h *AuthHandler) Me(c fiber.Ctx) error { - user := c.Locals("user") - if user == nil { + userLocale := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) + if userLocale.OriginalUser == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated), }) } return c.JSON(fiber.Map{ - "user": user, + "user": *userLocale.OriginalUser, }) } @@ -351,21 +351,12 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error { // Updates JWT Tokens. Requires authentication and updates access token only func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error { - userLocals, ok := c.Locals(constdata.USER_LOCALES_NAME).(*model.UserSession) + userLocale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) if !ok { return c.Status(fiber.StatusUnauthorized). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated))) } - user := model.Customer{ - ID: userLocals.UserID, - Email: userLocals.Email, - Role: model.Role{ID: userLocals.RoleID, Name: userLocals.RoleName}, - LangID: userLocals.LangID, - CountryID: userLocals.CountryID, - IsActive: userLocals.IsActive, - } - // Parse language and country_id from query params langIDStr := c.Query("lang_id") @@ -375,7 +366,7 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error { return c.Status(fiber.StatusBadRequest). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID))) } - user.LangID = uint(parsedID) + userLocale.OriginalUser.LangID = uint(parsedID) } countryIDStr := c.Query("country_id") @@ -386,10 +377,10 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error { return c.Status(fiber.StatusBadRequest). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID))) } - user.CountryID = uint(parsedID) + userLocale.OriginalUser.CountryID = uint(parsedID) } - newAccessToken, err := h.authService.UpdateJWTToken(&user) + newAccessToken, err := h.authService.UpdateJWTToken(userLocale.OriginalUser) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ diff --git a/app/delivery/web/api/public/routing.go b/app/delivery/web/api/public/routing.go index 9e36274..bad8746 100644 --- a/app/delivery/web/api/public/routing.go +++ b/app/delivery/web/api/public/routing.go @@ -3,6 +3,7 @@ package public import ( "git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -30,7 +31,7 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router { } func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { - lang_id, ok := c.Locals("langID").(uint) + lang_id, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) diff --git a/app/delivery/web/api/restricted/carts.go b/app/delivery/web/api/restricted/carts.go index aeed1ee..a787620 100644 --- a/app/delivery/web/api/restricted/carts.go +++ b/app/delivery/web/api/restricted/carts.go @@ -5,6 +5,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/service/cartsService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -37,7 +38,7 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router { } func (h *CartsHandler) AddNewCart(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -53,7 +54,7 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error { } func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -78,7 +79,7 @@ func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { } func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -94,7 +95,7 @@ func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error { } func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -117,7 +118,7 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error { } func (h *CartsHandler) AddProduct(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) diff --git a/app/delivery/web/api/restricted/list.go b/app/delivery/web/api/restricted/list.go index 7965424..c6b3116 100644 --- a/app/delivery/web/api/restricted/list.go +++ b/app/delivery/web/api/restricted/list.go @@ -5,6 +5,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/listService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/response" @@ -43,19 +44,19 @@ func (h *ListHandler) ListProducts(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - id_lang, ok := c.Locals("langID").(uint) + id_lang, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - listing, err := h.listService.ListProducts(id_lang, paging, filters) + list, err := h.listService.ListProducts(id_lang, paging, filters) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) } var columnMappingListProducts map[string]string = map[string]string{ @@ -74,19 +75,19 @@ func (h *ListHandler) ListUsers(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - id_lang, ok := c.Locals("langID").(uint) + id_lang, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - listing, err := h.listService.ListUsers(id_lang, paging, filters) + list, err := h.listService.ListUsers(id_lang, paging, filters) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) } var columnMappingListUsers map[string]string = map[string]string{ diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index 5173d3f..f06daf6 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -7,6 +7,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -35,7 +36,7 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { } func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error { - lang_id, ok := c.Locals("langID").(uint) + lang_id, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) @@ -58,7 +59,7 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error { } func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error { - lang_id, ok := c.Locals("langID").(uint) + lang_id, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) @@ -88,7 +89,7 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error { } func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { - session, ok := c.Locals("user").(*model.UserSession) + lang_id, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go index 760ddb3..ea6f906 100644 --- a/app/delivery/web/api/restricted/productTranslation.go +++ b/app/delivery/web/api/restricted/productTranslation.go @@ -6,6 +6,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -41,7 +42,7 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router { // GetProductDescription returns the product description for a given product ID func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -72,7 +73,7 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error { // SaveProductDescription saves the description for a given product ID, in given language func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) @@ -109,7 +110,7 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error { // TranslateProductDescription returns translated product description func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) + userID, ok := localeExtractor.GetUserID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) diff --git a/app/delivery/web/api/restricted/search.go b/app/delivery/web/api/restricted/search.go index ac6abb1..8881853 100644 --- a/app/delivery/web/api/restricted/search.go +++ b/app/delivery/web/api/restricted/search.go @@ -7,6 +7,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/service/meiliService" searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -36,7 +37,7 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { } func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { - id_lang, ok := c.Locals("langID").(uint) + id_lang, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) @@ -49,12 +50,11 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - nothing := "" - return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) } func (h *MeiliSearchHandler) Search(c fiber.Ctx) error { - id_lang, ok := c.Locals("langID").(uint) + id_lang, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) @@ -88,7 +88,7 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error { } func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error { - id_lang, ok := c.Locals("langID").(uint) + id_lang, ok := localeExtractor.GetLangID(c) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) diff --git a/app/model/customer.go b/app/model/customer.go index f7db443..d036e5b 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -77,6 +77,15 @@ func (us *UserSession) HasPermission(permission perms.Permission) bool { return false } +type UserLocale struct { + // User is the Target user if present, otherwise same as Original. + // User ought to be used in applications + User *Customer + // Original user is the one associated with auth token + OriginalUser *Customer + // Importantly, lang_id used in application is stored as OriginalUser.LangID +} + // ToSession converts User to UserSession func (u *Customer) ToSession() *UserSession { @@ -106,6 +115,7 @@ func BuildPermissionSlice(user *Customer) []perms.Permission { type LoginRequest struct { Email string `json:"email" form:"email"` Password string `json:"password" form:"password"` + LangID *uint `json:"lang_id" form:"lang_id"` } // RegisterRequest represents the initial registration form data diff --git a/app/model/productDescription.go b/app/model/productDescription.go index 985b819..2080b0b 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -18,7 +18,7 @@ type ProductDescription struct { 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"` + Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"` ImageLink string `gorm:"column:image_link" json:"image_link"` ExistsInDatabase bool `gorm:"-" json:"exists_in_database"` diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index 391ecb0..5083a42 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -52,7 +52,7 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid `+dbmodel.PsProductLangCols.AvailableLater.TabCol()+` AS available_later, `+dbmodel.PsProductLangCols.DeliveryInStock.TabCol()+` AS delivery_in_stock, `+dbmodel.PsProductLangCols.DeliveryOutStock.TabCol()+` AS delivery_out_stock, - `+dbmodel.PsProductLangCols.Usage.TabCol()+` AS _usage_, + `+dbmodel.PsProductLangCols.Usage.TabCol()+` AS `+"`usage`"+`, CONCAT(?, '/', `+dbmodel.PsImageShopCols.IDImage.TabCol()+`, '-large_default/', `+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+`, '.webp') AS image_link `, config.Get().Image.ImagePrefix). Joins("JOIN " + dbmodel.TableNamePsImageShop + diff --git a/app/service/authService/auth.go b/app/service/authService/auth.go index 6effc43..dc4a35b 100644 --- a/app/service/authService/auth.go +++ b/app/service/authService/auth.go @@ -83,6 +83,15 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin // Update last login time now := time.Now() user.LastLoginAt = &now + + if req.LangID != nil { + _, err := s.GetLangISOCode(*req.LangID) + if err != nil { + return nil, "", responseErrors.ErrBadLangID + } + user.LangID = *req.LangID + } + s.db.Save(&user) // Generate access token (JWT) diff --git a/app/service/productTranslationService/productTranslationService.go b/app/service/productTranslationService/productTranslationService.go index 55f4f66..6c4a559 100644 --- a/app/service/productTranslationService/productTranslationService.go +++ b/app/service/productTranslationService/productTranslationService.go @@ -90,13 +90,24 @@ func (s *ProductTranslationService) GetProductDescription(userID uint, productID // Updates relevant fields with the "updates" map func (s *ProductTranslationService) SaveProductDescription(userID uint, productID 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"} + allowedFields := []string{"description", "description_short", "link_rewrite", "meta_description", "meta_keywords", "meta_title", "name", + "available_now", "available_later", "delivery_in_stock", "delivery_out_stock", "usage"} for key := range updates { if !slices.Contains(allowedFields, key) { return responseErrors.ErrBadField } } + if text, exists := updates["link_rewrite"]; exists { + // sanitize and check that link_rewrite is a valid url slug + sanitized := SanitizeSlug(text) + if !IsValidSlug(sanitized) { + return responseErrors.ErrInvalidURLSlug + } + + updates["link_rewrite"] = sanitized + } + // 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++ { @@ -138,20 +149,28 @@ func (s *ProductTranslationService) TranslateProductDescription(userID uint, pro fields := []*string{&productDescription.Description, &productDescription.DescriptionShort, + &productDescription.LinkRewrite, &productDescription.MetaDescription, + &productDescription.MetaKeywords, &productDescription.MetaTitle, &productDescription.Name, &productDescription.AvailableNow, &productDescription.AvailableLater, + &productDescription.DeliveryInStock, + &productDescription.DeliveryOutStock, &productDescription.Usage, } keys := []string{"translation_of_product_description", "translation_of_product_short_description", + "translation_of_product_url_link", "translation_of_product_meta_description", + "translation_of_product_meta_keywords", "translation_of_product_meta_title", "translation_of_product_name", - "translation_of_product_available_now", - "translation_of_product_available_later", + "translation_of_product_available_now_message", + "translation_of_product_available_later_message", + "translation_of_product_delivery_in_stock_message", + "translation_of_product_delivery_out_stock_message", "translation_of_product_usage", } diff --git a/app/service/productTranslationService/sanitizeURLSlug.go b/app/service/productTranslationService/sanitizeURLSlug.go new file mode 100644 index 0000000..ea69d7c --- /dev/null +++ b/app/service/productTranslationService/sanitizeURLSlug.go @@ -0,0 +1,69 @@ +package productTranslationService + +import ( + "strings" + "unicode" + + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "github.com/dlclark/regexp2" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +func IsValidSlug(s string) bool { + var slug_regex2 = regexp2.MustCompile(constdata.SLUG_REGEX, regexp2.None) + + ok, _ := slug_regex2.MatchString(s) + return ok +} + +func SanitizeSlug(s string) string { + s = strings.TrimSpace(strings.ToLower(s)) + + // First apply explicit transliteration for language-specific letters. + s = transliterateWithTable(s) + + // Then normalize and strip any remaining combining marks. + s = removeDiacritics(s) + + // Replace all non-alphanumeric runs with "-" + var non_alphanum_regex2 = regexp2.MustCompile(constdata.NON_ALNUM_REGEX, regexp2.None) + s, _ = non_alphanum_regex2.Replace(s, "-", -1, -1) + + // Collapse repeated "-" and trim edges + var multi_dash_regex2 = regexp2.MustCompile(constdata.MULTI_DASH_REGEX, regexp2.None) + s, _ = multi_dash_regex2.Replace(s, "-", -1, -1) + + s = strings.Trim(s, "-") + + return s +} + +func transliterateWithTable(s string) string { + var b strings.Builder + b.Grow(len(s)) + + for _, r := range s { + if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok { + b.WriteString(repl) + } else { + b.WriteRune(r) + } + } + + return b.String() +} + +func removeDiacritics(s string) string { + t := transform.Chain( + norm.NFD, + runes.Remove(runes.In(unicode.Mn)), + norm.NFC, + ) + out, _, err := transform.String(t, s) + if err != nil { + return s + } + return out +} diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 9fb53b5..cbd5657 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -11,6 +11,29 @@ const CATEGORY_TREE_ROOT_ID = 2 const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" -const USER_LOCALES_NAME = "user" -const USER_LOCALES_ID = "userID" -const LANG_LOCALES_ID = "langID" +const USER_LOCALE = "user" + +// Slug sanitization +const NON_ALNUM_REGEX = `[^a-z0-9]+` +const MULTI_DASH_REGEX = `-+` +const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` + +// Currently supports only German+Polish specific cases +var TRANSLITERATION_TABLE = map[rune]string{ + // German + 'ä': "ae", + 'ö': "oe", + 'ü': "ue", + 'ß': "ss", + + // Polish + 'ą': "a", + 'ć': "c", + 'ę': "e", + 'ł': "l", + 'ń': "n", + 'ó': "o", + 'ś': "s", + 'ż': "z", + 'ź': "z", +} diff --git a/app/utils/localeExtractor/localeExtractor.go b/app/utils/localeExtractor/localeExtractor.go new file mode 100644 index 0000000..735397c --- /dev/null +++ b/app/utils/localeExtractor/localeExtractor.go @@ -0,0 +1,23 @@ +package localeExtractor + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "github.com/gofiber/fiber/v3" +) + +func GetLangID(c fiber.Ctx) (uint, bool) { + user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) + if !ok || user_locale.OriginalUser == nil { + return 0, false + } + return user_locale.OriginalUser.LangID, true +} + +func GetUserID(c fiber.Ctx) (uint, bool) { + user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) + if !ok || user_locale.User == nil { + return 0, false + } + return user_locale.User.ID, true +} diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index d2fce1a..dc54a4f 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -43,6 +43,7 @@ var ( // Typed errors for product description handler ErrBadAttribute = errors.New("bad or missing attribute value in header") ErrBadField = errors.New("this field can not be updated") + ErrInvalidURLSlug = errors.New("URL slug does not obey the industry standard") ErrInvalidXHTML = errors.New("text is not in xhtml format") ErrAIResponseFail = errors.New("AI responded with failure") ErrAIBadOutput = errors.New("AI response does not obey the format") @@ -142,6 +143,8 @@ func GetErrorCode(c fiber.Ctx, err error) string { 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, ErrInvalidURLSlug): + return i18n.T_(c, "error.invalid_url_slug") case errors.Is(err, ErrInvalidXHTML): return i18n.T_(c, "error.err_invalid_html") case errors.Is(err, ErrAIResponseFail): @@ -206,6 +209,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrInvalidPassword), errors.Is(err, ErrBadAttribute), errors.Is(err, ErrBadField), + errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrBadPaging), errors.Is(err, ErrNoRootFound), diff --git a/bruno/b2b-daniel/list-products.yml b/bruno/b2b-daniel/list-products.yml index adc88a7..20e6cac 100644 --- a/bruno/b2b-daniel/list-products.yml +++ b/bruno/b2b-daniel/list-products.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10 + url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10&target_user_id=2 params: - name: p value: "1" @@ -13,6 +13,9 @@ http: - name: elems value: "10" type: query + - name: target_user_id + value: "2" + type: query settings: encodeUrl: true diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml new file mode 100644 index 0000000..e843995 --- /dev/null +++ b/bruno/b2b-daniel/save-product-description.yml @@ -0,0 +1,39 @@ +info: + name: save-product-description + type: http + seq: 19 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=1 + params: + - name: productID + value: "1" + type: query + - name: productLangID + value: "1" + type: query + body: + type: json + data: |- + { + "description": "

Zastosowanie wałków rehabilitacyjnych w różnego rodzaju ćwiczeniach oraz zabiegach wpływa pozytywnie na łagodzenie urazów oraz zwiększa szanse na powrót pacjenta do pełnej sprawności fizycznej. Stosowane są w rehabilitacji ruchowej, podczas gimnastyki korekcyjnej, masaży tradycyjnych i sportowych, gdyż idealnie nadają się do unoszenia i separacji kończyn. Można je wykorzystać także do podpierania kolan, stóp, ramion, a także barków pacjenta. Wałki rehabilitacyjne polecane są także dla dzieci, wykorzystanie ich podczas zabawy znacznie wspiera rozwój dużej motoryki.

\n

Dzięki szerokiej ofercie kolorystycznej oraz zróżnicowanym rozmiarom, możliwe jest skomponowanie zestawu do ćwiczeń niezbędnego w każdym gabinecie fizjoterapeutycznym, gabinecie masażu czy też szkole i przedszkolu. 

\n

Wałek rehabilitacyjny  jest wyrobem medycznym zgodnie z wymaganiami zasadniczymi dla wyrobów medycznych i w rozumieniu ustawy o wyrobach medycznych, zgłoszonym do Rejestru Wyrobów Medycznych prowadzonego przez Urząd Rejestracji Produktów Leczniczych, Wyrobów Medycznych i Produktów Biobójczych, wyposażonym w deklarację zgodności producenta i opatrzonym znakiem CE.

\n

\n

\"Wyrób

\n

Polecane zastosowanie:

\n\n

\n

Specyfikacja materiału:

\n

Pokrowiec: materiał z powłoką PCV przeznaczony dla wyrobów medycznych, dzięki czemu jest bardzo łatwy w czyszczeniu oraz dezynfekcji:

\n\n

\"REACH\"\"Certyfikat\"Nie\"Ognioodporny\"\"Odporny\"Odporny\"Przeznaczony\"Odporny\"Olejoodporny\"

\n

Wypełnienie: średnio twarda pianka poliuretanowa o podwyższonej odporności na odkształcenia:

\n\n

\"Certyfikat\"Atest\"Atest

\n

\n

", + "description_short": "

Wałki rehabilitacyjne znajdują swoje zastosowanie w różnego rodzaju ćwiczeniach. Stosowane są w rehabilitacji ruchowej, podczas gimnastyki korekcyjnej, masaży tradycyjnych i sportowych, gdyż idealnie nadają się do unoszenia i separacji kończyn. Można je wykorzystać także do podpierania kolan, stóp, ramion, a także barków pacjenta. Wałki rehabilitacyjne polecane są także dla dzieci, wykorzystanie ich podczas zabawy, znacznie wspiera rozwój dużej motoryki. Produkt posiada certyfikację jako wyrób medyczny. 

", + "link_rewrite": " Wałek-Rehabilitacyjny-10x30-cm ", + "meta_description": "", + "meta_keywords": "", + "meta_title": "", + "name": "Wałek rehabilitacyjny 10 x 30 cm", + "available_now": "dostępny", + "available_later": "na zamówienie", + "delivery_in_stock": "Czas realizacji 3-7 dni roboczych", + "delivery_out_stock": "Czas realizacji 3-7 dni roboczych", + "usage": "

I. Czyszczenie i konserwacja

\r\n

Tapicerkę należy czyścić powierzchniowo stosując dozwolone środki:

\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
\r\n

Rodzaj zabrudzenia

\r\n
\r\n

Dozwolone środki

\r\n
\r\n

Postępowanie

\r\n
\r\n

Codzienne zabrudzenia

\r\n

 

\r\n
\r\n

Łagodny detergent najlepiej roztwór szarego mydła

\r\n
\r\n

Czyścić regularnie z użyciem gąbki lub miękkiej szczotki. Na koniec przetrzeć czyszczone miejsce wilgotną szmatką po czym wytrzeć do sucha (w celu usunięcia pozostałości detergentu).

\r\n
\r\n

Miejscowe, silniejsze zabrudzenia

\r\n
\r\n

25% roztwór alkoholu etylowego

\r\n
\r\n

Delikatnie przecierać nasączonym tamponem z gazy. Na koniec przetrzeć czyszczone miejsce wilgotną szmatką po czym wytrzeć do sucha (w celu usunięcia pozostałości detergentu).

\r\n
\r\n

Dezynfekcja

\r\n
\r\n

Ogólnodostępne środki do dezynfekcji zawierające:

\r\n

- aktywny chlor – dichloroizocyjanuran sodu, max stężenie 10000 ppm 

\r\n

- aktywny chlor - dwutlenek chloru w roztworze do 20 000 ppm 

\r\n

- alkohol izopropylowy max stężenie 70 % 

\r\n

\r\n
\r\n

Dezynfekować zgodnie z zaleceniami producenta używanego środka.

\r\n
\r\n

Przed użyciem środka innego niż łagodny detergent trzeba sprawdzić efekt w niewidocznym miejscu, a samo czyszczenie wykonać bardzo ostrożnie.

\r\n
\r\n


II. Informacje

\r\n

\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
\"\"\r\n

Szamponować przy użyciu gąbki

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie prać!!! (delikatne wyroby)   

\r\n
\r\n

\r\n
\r\n

Nie chlorować!!! (nie stosować do bielenia związków wydzielających wolny chlor)

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie prasować!!! (nie dopuszczać do kontaktu z nagrzanymi powierzchniami np. kaloryfer)

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie czyścić chemicznie!!!

\r\n
\r\n

\r\n

III. Warunki gwarancji

\r\n

Gwarancji nie podlegają:

\r\n" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/translate-product-description.yml b/bruno/b2b-daniel/translate-product-description.yml new file mode 100644 index 0000000..c914958 --- /dev/null +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -0,0 +1,28 @@ +info: + name: translate-product-description + type: http + seq: 20 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=1&productToLangID=3&model=Google + params: + - name: productID + value: "51" + type: query + - name: productFromLangID + value: "1" + type: query + - name: productToLangID + value: "3" + type: query + - name: model + value: Google + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/storage/folder/a.txt b/storage/folder/a.txt new file mode 100644 index 0000000..273c1a9 --- /dev/null +++ b/storage/folder/a.txt @@ -0,0 +1 @@ +This is a test. \ No newline at end of file