From 55da953f32abdbd0d89822308f4987335fba7e4d Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 16:56:05 +0200 Subject: [PATCH 01/25] add teleporting --- app/delivery/middleware/auth.go | 68 +++++++++++++------ app/delivery/middleware/language.go | 36 ++++------ app/delivery/web/api/public/auth.go | 23 ++----- app/delivery/web/api/public/routing.go | 3 +- app/delivery/web/api/restricted/carts.go | 11 +-- app/delivery/web/api/restricted/list.go | 13 ++-- app/delivery/web/api/restricted/menu.go | 7 +- .../web/api/restricted/productTranslation.go | 7 +- app/delivery/web/api/restricted/search.go | 10 +-- app/model/customer.go | 10 +++ app/service/authService/auth.go | 9 +++ app/utils/const_data/consts.go | 3 +- app/utils/localeExtractor/localeExtractor.go | 23 +++++++ bruno/b2b-daniel/list-products.yml | 5 +- 14 files changed, 142 insertions(+), 86 deletions(-) create mode 100644 app/utils/localeExtractor/localeExtractor.go diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 2aefce0..c5a87cc 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,9 +61,52 @@ func AuthMiddleware() fiber.Handler { }) } - // Set user in context - c.Locals(constdata.USER_LOCALES_NAME, user.ToSession()) - c.Locals(constdata.USER_LOCALES_ID, user.ID) + // 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 852a4ac..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: userLocals.Role, - 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 b269fb7..4ed8300 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -5,6 +5,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" @@ -33,7 +34,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))) @@ -56,7 +57,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))) @@ -86,7 +87,7 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error { } func (h *MenuHandler) GetTopMenu(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/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 ec7b63d..3934dcd 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -82,6 +82,15 @@ type UserSession struct { IsActive bool `json:"is_active"` } +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 { return &UserSession{ @@ -98,6 +107,7 @@ func (u *Customer) ToSession() *UserSession { 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/service/authService/auth.go b/app/service/authService/auth.go index 2fc4a7d..c873ce0 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/utils/const_data/consts.go b/app/utils/const_data/consts.go index 9c64ee5..b3790c8 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -11,5 +11,4 @@ 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 USER_LOCALE = "user" 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/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 From b2acb8c922c53ce69f8dd02c00ceef65df536b8c Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 1 Apr 2026 13:30:54 +0200 Subject: [PATCH 02/25] storage --- .env | 4 + app/config/config.go | 45 ++++++++-- app/delivery/web/api/restricted/storage.go | 88 ++++++++++++++++++++ app/delivery/web/init.go | 4 + app/model/entry.go | 6 ++ app/repos/searchRepo/searchRepo.go | 8 +- app/repos/storageRepo/storageRepo.go | 51 ++++++++++++ app/service/meiliService/meiliService.go | 4 +- app/service/storageService/storageService.go | 75 +++++++++++++++++ app/utils/responseErrors/responseErrors.go | 17 +++- bruno/b2b-daniel/download-file.yml | 19 +++++ bruno/b2b-daniel/list-content.yml | 19 +++++ storage/folder1/test | 0 storage/folder1/test.txt | 1 + 14 files changed, 326 insertions(+), 15 deletions(-) create mode 100644 app/delivery/web/api/restricted/storage.go create mode 100644 app/model/entry.go create mode 100644 app/repos/storageRepo/storageRepo.go create mode 100644 app/service/storageService/storageService.go create mode 100644 bruno/b2b-daniel/download-file.yml create mode 100644 bruno/b2b-daniel/list-content.yml create mode 100644 storage/folder1/test create mode 100644 storage/folder1/test.txt diff --git a/.env b/.env index 88771c9..d45c67a 100644 --- a/.env +++ b/.env @@ -48,6 +48,10 @@ EMAIL_FROM=test@ma-al.com EMAIL_FROM_NAME=Gitea Manager EMAIL_ADMIN=goc_marek@ma-al.pl +# STORAGE +STORAGE_ROOT=./storage + + I18N_LANGS=en,pl,cs PDF_SERVER_URL=http://localhost:8000 diff --git a/app/config/config.go b/app/config/config.go index 586a182..1963d38 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -2,8 +2,10 @@ package config import ( "fmt" + "log" "log/slog" "os" + "path/filepath" "reflect" "strconv" "strings" @@ -24,7 +26,8 @@ type Config struct { GoogleTranslate GoogleTranslateConfig Image ImageConfig Cors CorsConfig - MailiSearch MeiliSearchConfig + MeiliSearch MeiliSearchConfig + Storage StorageConfig } type I18n struct { @@ -95,6 +98,10 @@ type EmailConfig struct { Enabled bool `env:"EMAIL_ENABLED,false"` } +type StorageConfig struct { + RootFolder string `env:"STORAGE_ROOT"` +} + type PdfPrinter struct { ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"` } @@ -155,7 +162,7 @@ func load() *Config { err = loadEnv(&cfg.OAuth.Google) if err != nil { - slog.Error("not possible to load env variables for outh google : ", err.Error(), "") + slog.Error("not possible to load env variables for oauth google : ", err.Error(), "") } err = loadEnv(&cfg.App) @@ -170,12 +177,12 @@ func load() *Config { err = loadEnv(&cfg.I18n) if err != nil { - slog.Error("not possible to load env variables for email : ", err.Error(), "") + slog.Error("not possible to load env variables for i18n : ", err.Error(), "") } err = loadEnv(&cfg.Pdf) if err != nil { - slog.Error("not possible to load env variables for email : ", err.Error(), "") + slog.Error("not possible to load env variables for pdf : ", err.Error(), "") } err = loadEnv(&cfg.GoogleTranslate) @@ -185,19 +192,25 @@ func load() *Config { err = loadEnv(&cfg.Image) if err != nil { - slog.Error("not possible to load env variables for google translate : ", err.Error(), "") + slog.Error("not possible to load env variables for image : ", err.Error(), "") } err = loadEnv(&cfg.Cors) if err != nil { - slog.Error("not possible to load env variables for google translate : ", err.Error(), "") + slog.Error("not possible to load env variables for cors : ", err.Error(), "") } - err = loadEnv(&cfg.MailiSearch) + err = loadEnv(&cfg.MeiliSearch) if err != nil { - slog.Error("not possible to load env variables for google translate : ", err.Error(), "") + slog.Error("not possible to load env variables for meili search : ", err.Error(), "") } + err = loadEnv(&cfg.Storage) + if err != nil { + slog.Error("not possible to load env variables for storage : ", err.Error(), "") + } + cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder) + return cfg } @@ -308,6 +321,22 @@ func setValue(field reflect.Value, val string, key string) error { return nil } +func ResolveRelativePath(relativePath string) string { + // get working directory (where program was started) + wd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + // convert to absolute path + absPath := relativePath + if !filepath.IsAbs(absPath) { + absPath = filepath.Join(wd, absPath) + } + + return filepath.Clean(absPath) +} + func parseEnvTag(tag string) (key string, def *string) { if tag == "" { return "", nil diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go new file mode 100644 index 0000000..bca3da6 --- /dev/null +++ b/app/delivery/web/api/restricted/storage.go @@ -0,0 +1,88 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/service/storageService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "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" + "github.com/gofiber/fiber/v3" +) + +type StorageHandler struct { + storageService *storageService.StorageService + config *config.Config +} + +func NewStorageHandler() *StorageHandler { + return &StorageHandler{ + storageService: storageService.New(), + config: config.Get(), + } +} + +func StorageHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewStorageHandler() + + r.Get("/list-content", handler.ListContent) + r.Get("/download-file", handler.DownloadFile) + r.Get("/create-folder", handler.CreateFolder) + + return r +} + +// accepted path looks like e.g. "/folder1/" or "folder1" +func (h *StorageHandler) ListContent(c fiber.Ctx) error { + // relative path defaults to root directory + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + entries_in_list, err := h.storageService.ListContent(absPath) + + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(entries_in_list, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + f, filename, filesize, err := h.storageService.DownloadFilePrep(absPath) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + c.Attachment(filename) + c.Set("Content-Length", strconv.FormatInt(filesize, 10)) + return c.SendStream(f, int(filesize)) +} + +func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.CreateFolder(absPath, c.Query("name")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index eaf41d9..c48a778 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -115,6 +115,10 @@ func (s *Server) Setup() error { carts := s.restricted.Group("/carts") restricted.CartsHandlerRoutes(carts) + // storage (restricted) + storage := s.restricted.Group("/storage") + restricted.StorageHandlerRoutes(storage) + s.api.All("*", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) }) diff --git a/app/model/entry.go b/app/model/entry.go new file mode 100644 index 0000000..ae63646 --- /dev/null +++ b/app/model/entry.go @@ -0,0 +1,6 @@ +package model + +type EntryInList struct { + Name string + IsFolder bool +} diff --git a/app/repos/searchRepo/searchRepo.go b/app/repos/searchRepo/searchRepo.go index 05afd3a..de4d5ea 100644 --- a/app/repos/searchRepo/searchRepo.go +++ b/app/repos/searchRepo/searchRepo.go @@ -32,12 +32,12 @@ func New() UISearchRepo { } func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) { - url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MailiSearch.ServerURL, index) + url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MeiliSearch.ServerURL, index) return r.doRequest(http.MethodPost, url, body) } func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) { - url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MailiSearch.ServerURL, index) + url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MeiliSearch.ServerURL, index) return r.doRequest(http.MethodGet, url, nil) } @@ -55,8 +55,8 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes } req.Header.Set("Content-Type", "application/json") - if r.cfg.MailiSearch.ApiKey != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey)) + if r.cfg.MeiliSearch.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey)) } client := &http.Client{} diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go new file mode 100644 index 0000000..aa60c09 --- /dev/null +++ b/app/repos/storageRepo/storageRepo.go @@ -0,0 +1,51 @@ +package storageRepo + +import ( + "os" + + "git.ma-al.com/goc_daniel/b2b/app/model" +) + +type UIStorageRepo interface { + EntryInfo(absPath string) (os.FileInfo, error) + ListContent(absPath string) (*[]model.EntryInList, error) + OpenFile(absPath string) (*os.File, error) + CreateFolder(absPath string, name string) error +} + +type StorageRepo struct{} + +func New() UIStorageRepo { + return &StorageRepo{} +} + +func (r *StorageRepo) EntryInfo(absPath string) (os.FileInfo, error) { + return os.Stat(absPath) +} + +func (r *StorageRepo) ListContent(absPath string) (*[]model.EntryInList, error) { + entries, err := os.ReadDir(absPath) + if err != nil { + return nil, err + } + + var entries_in_list []model.EntryInList + + for _, entry := range entries { + var next_entry_in_list model.EntryInList + next_entry_in_list.Name = entry.Name() + next_entry_in_list.IsFolder = entry.IsDir() + + entries_in_list = append(entries_in_list, next_entry_in_list) + } + + return &entries_in_list, nil +} + +func (r *StorageRepo) OpenFile(absPath string) (*os.File, error) { + return os.Open(absPath) +} + +func (r *StorageRepo) CreateFolder(absPath string, name string) error { + os.(absPath) +} diff --git a/app/service/meiliService/meiliService.go b/app/service/meiliService/meiliService.go index 87b196b..6d9120a 100644 --- a/app/service/meiliService/meiliService.go +++ b/app/service/meiliService/meiliService.go @@ -27,8 +27,8 @@ type MeiliService struct { func New() *MeiliService { client := meilisearch.New( - config.Get().MailiSearch.ServerURL, - meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey), + config.Get().MeiliSearch.ServerURL, + meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey), ) return &MeiliService{ diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go new file mode 100644 index 0000000..963f934 --- /dev/null +++ b/app/service/storageService/storageService.go @@ -0,0 +1,75 @@ +package storageService + +import ( + "os" + "path/filepath" + "strings" + + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" +) + +type StorageService struct { + storageRepo storageRepo.UIStorageRepo +} + +func New() *StorageService { + return &StorageService{ + storageRepo: storageRepo.New(), + } +} + +func (s *StorageService) ListContent(absPath string) (*[]model.EntryInList, error) { + info, err := s.storageRepo.EntryInfo(absPath) + if err != nil || !info.IsDir() { + return nil, responseErrors.ErrFolderDoesNotExist + } + + entries_in_list, err := s.storageRepo.ListContent(absPath) + return entries_in_list, err +} + +func (s *StorageService) DownloadFilePrep(absPath string) (*os.File, string, int64, error) { + info, err := s.storageRepo.EntryInfo(absPath) + if err != nil || info.IsDir() { + return nil, "", 0, responseErrors.ErrFileDoesNotExist + } + + f, err := s.storageRepo.OpenFile(absPath) + if err != nil { + return nil, "", 0, err + } + + return f, filepath.Base(absPath), info.Size(), nil +} + +func (s *StorageService) CreateFolder(absPath string, name string) error { + info, err := s.storageRepo.EntryInfo(absPath) + if err != nil || !info.IsDir() { + return responseErrors.ErrFolderDoesNotExist + } + + if name == "" || name == "." filepath.Base(name) != name { + return responseErrors.ErrBadAttribute + } + + absPath2, err := s.AbsPath(absPath, name) + if err != nil { + return err + } + + return s.storageRepo.CreateFolder(absPath, name) +} + +// AbsPath extracts an absolute path and validates it +func (s *StorageService) AbsPath(root string, relativePath string) (string, error) { + cleanName := filepath.Clean(relativePath) + fullPath := filepath.Join(root, cleanName) + + if fullPath != root && !strings.HasPrefix(fullPath, root+string(os.PathSeparator)) { + return "", responseErrors.ErrAccessDenied + } + + return fullPath, nil +} diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index c4247ea..65db40a 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -59,6 +59,11 @@ var ( ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") + + // Typed errors for storage + ErrAccessDenied = errors.New("access denied!") + ErrFolderDoesNotExist = errors.New("folder does not exist") + ErrFileDoesNotExist = errors.New("file does not exist") ) // Error represents an error with HTTP status code @@ -162,6 +167,13 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrProductOrItsVariationDoesNotExist): return i18n.T_(c, "error.product_or_its_variation_does_not_exist") + case errors.Is(err, ErrAccessDenied): + return i18n.T_(c, "error.access_denied") + case errors.Is(err, ErrFolderDoesNotExist): + return i18n.T_(c, "error.folder_does_not_exist") + case errors.Is(err, ErrFileDoesNotExist): + return i18n.T_(c, "error.file_does_not_exist") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -203,7 +215,10 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrRootNeverReached), errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), - errors.Is(err, ErrProductOrItsVariationDoesNotExist): + errors.Is(err, ErrProductOrItsVariationDoesNotExist), + errors.Is(err, ErrAccessDenied), + errors.Is(err, ErrFolderDoesNotExist), + errors.Is(err, ErrFileDoesNotExist): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/bruno/b2b-daniel/download-file.yml b/bruno/b2b-daniel/download-file.yml new file mode 100644 index 0000000..468be8d --- /dev/null +++ b/bruno/b2b-daniel/download-file.yml @@ -0,0 +1,19 @@ +info: + name: download-file + type: http + seq: 20 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/download-file?path=/folder1/test.txt + params: + - name: path + value: /folder1/test.txt + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/list-content.yml b/bruno/b2b-daniel/list-content.yml new file mode 100644 index 0000000..8a9d600 --- /dev/null +++ b/bruno/b2b-daniel/list-content.yml @@ -0,0 +1,19 @@ +info: + name: list-content + type: http + seq: 19 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/list-content?path=/folder1 + params: + - name: path + value: /folder1 + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/storage/folder1/test b/storage/folder1/test new file mode 100644 index 0000000..e69de29 diff --git a/storage/folder1/test.txt b/storage/folder1/test.txt new file mode 100644 index 0000000..273c1a9 --- /dev/null +++ b/storage/folder1/test.txt @@ -0,0 +1 @@ +This is a test. \ No newline at end of file From b9bc121d43e7d5205d42da501dfa4a08a3a3d988 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 2 Apr 2026 10:27:14 +0200 Subject: [PATCH 03/25] getting to upload --- app/delivery/web/api/restricted/storage.go | 73 +++++++++++++-- app/repos/storageRepo/storageRepo.go | 40 ++++++--- app/service/storageService/storageService.go | 89 +++++++++++++++---- app/utils/responseErrors/responseErrors.go | 16 +++- bruno/b2b-daniel/create-folder.yml | 22 +++++ bruno/b2b-daniel/delete-entry.yml | 19 ++++ bruno/b2b-daniel/get-description.yml | 22 +++++ bruno/b2b-daniel/save-product-description.yml | 28 ++++++ .../translate-product-description.yml | 28 ++++++ 9 files changed, 297 insertions(+), 40 deletions(-) create mode 100644 bruno/b2b-daniel/create-folder.yml create mode 100644 bruno/b2b-daniel/delete-entry.yml create mode 100644 bruno/b2b-daniel/get-description.yml create mode 100644 bruno/b2b-daniel/save-product-description.yml create mode 100644 bruno/b2b-daniel/translate-product-description.yml diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index bca3da6..6722a9b 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -29,7 +29,11 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/list-content", handler.ListContent) r.Get("/download-file", handler.DownloadFile) + + r.Post("/upload-file", handler.CreateFolder) r.Get("/create-folder", handler.CreateFolder) + r.Get("/delete-file", handler.DeleteFile) + r.Get("/delete-folder", handler.DeleteFolder) return r } @@ -37,13 +41,13 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { // accepted path looks like e.g. "/folder1/" or "folder1" func (h *StorageHandler) ListContent(c fiber.Ctx) error { // relative path defaults to root directory - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - entries_in_list, err := h.storageService.ListContent(absPath) + entries_in_list, err := h.storageService.ListContent(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). @@ -54,13 +58,13 @@ func (h *StorageHandler) ListContent(c fiber.Ctx) error { } func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - f, filename, filesize, err := h.storageService.DownloadFilePrep(absPath) + f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -71,14 +75,69 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { return c.SendStream(f, int(filesize)) } -func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) +func (h *StorageHandler) UploadFile(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - err = h.storageService.CreateFolder(absPath, c.Query("name")) + f, err := c.FormFile("document") + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrMissingFileFieldDocument)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument))) + } + + err = h.storageService.UploadFile(abs_path, c.Query("name"), f) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + err = c.SaveFile(f, abs_path) + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.CreateFolder(abs_path, c.Query("name")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) DeleteFile(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.DeleteFile(abs_path) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.DeleteFolder(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go index aa60c09..ac838ff 100644 --- a/app/repos/storageRepo/storageRepo.go +++ b/app/repos/storageRepo/storageRepo.go @@ -1,16 +1,20 @@ package storageRepo import ( + "mime/multipart" "os" "git.ma-al.com/goc_daniel/b2b/app/model" ) type UIStorageRepo interface { - EntryInfo(absPath string) (os.FileInfo, error) - ListContent(absPath string) (*[]model.EntryInList, error) - OpenFile(absPath string) (*os.File, error) - CreateFolder(absPath string, name string) error + EntryInfo(abs_path string) (os.FileInfo, error) + ListContent(abs_path string) (*[]model.EntryInList, error) + OpenFile(abs_path string) (*os.File, error) + UploadFile(abs_path string, f *multipart.FileHeader) error + CreateFolder(abs_path string) error + DeleteFile(abs_path string) error + DeleteFolder(abs_path string) error } type StorageRepo struct{} @@ -19,12 +23,12 @@ func New() UIStorageRepo { return &StorageRepo{} } -func (r *StorageRepo) EntryInfo(absPath string) (os.FileInfo, error) { - return os.Stat(absPath) +func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) { + return os.Stat(abs_path) } -func (r *StorageRepo) ListContent(absPath string) (*[]model.EntryInList, error) { - entries, err := os.ReadDir(absPath) +func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) { + entries, err := os.ReadDir(abs_path) if err != nil { return nil, err } @@ -42,10 +46,22 @@ func (r *StorageRepo) ListContent(absPath string) (*[]model.EntryInList, error) return &entries_in_list, nil } -func (r *StorageRepo) OpenFile(absPath string) (*os.File, error) { - return os.Open(absPath) +func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { + return os.Open(abs_path) } -func (r *StorageRepo) CreateFolder(absPath string, name string) error { - os.(absPath) +func (r *StorageRepo) UploadFile(abs_path string, f *multipart.FileHeader) error { + return nil +} + +func (r *StorageRepo) CreateFolder(abs_path string) error { + return os.Mkdir(abs_path, 0755) +} + +func (r *StorageRepo) DeleteFile(abs_path string) error { + return os.Remove(abs_path) +} + +func (r *StorageRepo) DeleteFolder(abs_path string) error { + return os.RemoveAll(abs_path) } diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index 963f934..f60f14b 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -1,6 +1,7 @@ package storageService import ( + "mime/multipart" "os" "path/filepath" "strings" @@ -20,56 +21,110 @@ func New() *StorageService { } } -func (s *StorageService) ListContent(absPath string) (*[]model.EntryInList, error) { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { return nil, responseErrors.ErrFolderDoesNotExist } - entries_in_list, err := s.storageRepo.ListContent(absPath) + entries_in_list, err := s.storageRepo.ListContent(abs_path) return entries_in_list, err } -func (s *StorageService) DownloadFilePrep(absPath string) (*os.File, string, int64, error) { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || info.IsDir() { return nil, "", 0, responseErrors.ErrFileDoesNotExist } - f, err := s.storageRepo.OpenFile(absPath) + f, err := s.storageRepo.OpenFile(abs_path) if err != nil { return nil, "", 0, err } - return f, filepath.Base(absPath), info.Size(), nil + return f, filepath.Base(abs_path), info.Size(), nil } -func (s *StorageService) CreateFolder(absPath string, name string) error { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) UploadFile(abs_path string, name string, f *multipart.FileHeader) error { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { return responseErrors.ErrFolderDoesNotExist } - if name == "" || name == "." filepath.Base(name) != name { + if name == "" || name == "." || name == ".." || filepath.Base(name) != name { return responseErrors.ErrBadAttribute } - - absPath2, err := s.AbsPath(absPath, name) + abs_file_path, err := s.AbsPath(abs_path, name) if err != nil { return err } + if abs_file_path == abs_path { + return responseErrors.ErrBadAttribute + } - return s.storageRepo.CreateFolder(absPath, name) + info, err = s.storageRepo.EntryInfo(abs_file_path) + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.UploadFile(abs_file_path, f) + } else { + return err + } +} + +func (s *StorageService) CreateFolder(abs_path string, name string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || !info.IsDir() { + return responseErrors.ErrFolderDoesNotExist + } + + if name == "" || name == "." || name == ".." || filepath.Base(name) != name { + return responseErrors.ErrBadAttribute + } + abs_folder_path, err := s.AbsPath(abs_path, name) + if err != nil { + return err + } + if abs_folder_path == abs_path { + return responseErrors.ErrBadAttribute + } + + info, err = s.storageRepo.EntryInfo(abs_folder_path) + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.CreateFolder(abs_folder_path) + } else { + return err + } +} + +func (s *StorageService) DeleteFile(abs_path string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || info.IsDir() { + return responseErrors.ErrFileDoesNotExist + } + + return s.storageRepo.DeleteFile(abs_path) +} + +func (s *StorageService) DeleteFolder(abs_path string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || !info.IsDir() { + return responseErrors.ErrFolderDoesNotExist + } + + return s.storageRepo.DeleteFolder(abs_path) } // AbsPath extracts an absolute path and validates it func (s *StorageService) AbsPath(root string, relativePath string) (string, error) { - cleanName := filepath.Clean(relativePath) - fullPath := filepath.Join(root, cleanName) + clean_name := filepath.Clean(relativePath) + full_path := filepath.Join(root, clean_name) - if fullPath != root && !strings.HasPrefix(fullPath, root+string(os.PathSeparator)) { + if full_path != root && !strings.HasPrefix(full_path, root+string(os.PathSeparator)) { return "", responseErrors.ErrAccessDenied } - return fullPath, nil + return full_path, nil } diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 65db40a..2ea09e5 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -61,9 +61,11 @@ var ( ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") // Typed errors for storage - ErrAccessDenied = errors.New("access denied!") - ErrFolderDoesNotExist = errors.New("folder does not exist") - ErrFileDoesNotExist = errors.New("file does not exist") + ErrAccessDenied = errors.New("access denied!") + ErrFolderDoesNotExist = errors.New("folder does not exist") + ErrFileDoesNotExist = errors.New("file does not exist") + ErrNameTaken = errors.New("name taken") + ErrMissingFileFieldDocument = errors.New("missing file field 'document'") ) // Error represents an error with HTTP status code @@ -173,6 +175,10 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.folder_does_not_exist") case errors.Is(err, ErrFileDoesNotExist): return i18n.T_(c, "error.file_does_not_exist") + case errors.Is(err, ErrNameTaken): + return i18n.T_(c, "error.name_taken") + case errors.Is(err, ErrMissingFileFieldDocument): + return i18n.T_(c, "error.missing_file_field_document") default: return i18n.T_(c, "error.err_internal_server_error") @@ -218,7 +224,9 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrAccessDenied), errors.Is(err, ErrFolderDoesNotExist), - errors.Is(err, ErrFileDoesNotExist): + errors.Is(err, ErrFileDoesNotExist), + errors.Is(err, ErrNameTaken), + errors.Is(err, ErrMissingFileFieldDocument): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/bruno/b2b-daniel/create-folder.yml b/bruno/b2b-daniel/create-folder.yml new file mode 100644 index 0000000..fb04ec1 --- /dev/null +++ b/bruno/b2b-daniel/create-folder.yml @@ -0,0 +1,22 @@ +info: + name: create-folder + type: http + seq: 22 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/create-folder?path&name=../k + params: + - name: path + value: "" + type: query + - name: name + value: ../k + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/delete-entry.yml b/bruno/b2b-daniel/delete-entry.yml new file mode 100644 index 0000000..baf63f2 --- /dev/null +++ b/bruno/b2b-daniel/delete-entry.yml @@ -0,0 +1,19 @@ +info: + name: delete-entry + type: http + seq: 23 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/delete-entry?path=folder2 + params: + - name: path + value: folder2 + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/get-description.yml b/bruno/b2b-daniel/get-description.yml new file mode 100644 index 0000000..bd2137d --- /dev/null +++ b/bruno/b2b-daniel/get-description.yml @@ -0,0 +1,22 @@ +info: + name: get-description + type: http + seq: 24 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=2 + params: + - name: productID + value: "51" + type: query + - name: productLangID + value: "2" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml new file mode 100644 index 0000000..201f4f8 --- /dev/null +++ b/bruno/b2b-daniel/save-product-description.yml @@ -0,0 +1,28 @@ +info: + name: save-product-description + type: http + seq: 25 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=2 + params: + - name: productID + value: "1" + type: query + - name: productLangID + value: "2" + type: query + body: + type: json + data: |- + { + "description": "

Rehabilitation rollers are essential, multi-functional tools used across various exercises and treatments. Their application positively influences the alleviation of injuries and significantly enhances a patient's chances of returning to full physical fitness.

\n

These versatile medical rollers are a staple in movement rehabilitation, corrective gymnastics, and both traditional and sports massages, as their design is perfect for precise limb elevation and separation. They provide critical support, making them ideal for comfortably cushioning the patient's knees, feet, arms, and shoulders.

\n

Support for Children's Development: Rehabilitation bolsters are also highly recommended for children. Incorporating them into play activities offers an engaging way to substantially support the development of gross motor skills.

\n

Customize Your Therapy Space: Thanks to our wide range of colours and various sizes, you can easily assemble a comprehensive exercise kit necessary for every physiotherapy clinic, massage parlour, school, or kindergarten.

\n

\n

Certified Medical Device:

\n

\n

This Rehabilitation Roller is a certified medical device compliant with the essential requirements for medical products and the Medical Devices Act. It has been officially registered with the Office for Registration of Medicinal Products, Medical Devices and Biocidal Products (URPL), is equipped with the manufacturer's Declaration of Conformity, and bears the CE mark.

\n

\"\"

\n
\n

\n

Recommended use:

\n
    \n
  • \n

    In rehabilitation and physical therapy

    \n
  • \n
  • \n

    During traditional and sports massages

    \n
  • \n
  • \n

    In corrective gymnastics (especially for children)

    \n
  • \n
  • \n

    For the alleviation of injuries to various body parts

    \n
  • \n
  • \n

    For comfortable support of the knees, ankles, and patient's head

    \n
  • \n
  • \n

    In exercises developing children's motor skills

    \n
  • \n
  • \n

    In beauty salons and SPA centers

    \n
  • \n
  • \n

    In children's playrooms

    \n
  • \n
\n
\n

\n

Material Specification & Certification

\n

\n

Cover Material: PVC-coated material specifically designated for medical devices, making it extremely easy to clean and disinfect:

\n
    \n
  • \n

    REACH Regulation Compliant

    \n
  • \n
  • \n

    STANDARD 100 by OEKO-TEX ® Certified

    \n
  • \n
  • \n

    Phthalate-Free (Non-Toxic PVC)

    \n
  • \n
  • \n

    Flame Retardant (Fire Resistant)

    \n
  • \n
  • \n

    Resistant to Physiological Fluids (blood, urine, sweat) and Alcohol (Ideal for Clinics)

    \n
  • \n
  • \n

    UV Resistant (Suitable for Outdoor Use)

    \n
  • \n
  • \n

    Scratch Resistant

    \n
  • \n
  • \n

    Oil Resistant

    \n
  • \n
\n

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

\n

Filling: Medium-firm polyurethane foam with enhanced resistance to deformation.

\n
    \n
  • \n

    Holds a HYGIENE CERTIFICATE

    \n
  • \n
  • \n

    Certified with STANDARD 100 by OEKO-TEX® – Product Class I.

    \n
  • \n
  • \n

    Manufactured using high-quality raw materials that do not contribute to ozone depletion.

    \n
  • \n
\n

\"Certyfikat\"Atest\"Atest

" + } + 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..f08dc01 --- /dev/null +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -0,0 +1,28 @@ +info: + name: translate-product-description + type: http + seq: 19 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=2&productToLangID=3&model=Google + params: + - name: productID + value: "51" + type: query + - name: productFromLangID + value: "2" + 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 From 833f4a5a0743436c92273014cdf496a3f1c455e6 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 2 Apr 2026 11:26:58 +0200 Subject: [PATCH 04/25] deleting and uploading files --- app/delivery/web/api/restricted/storage.go | 9 +++---- app/repos/storageRepo/storageRepo.go | 7 ++--- app/service/storageService/storageService.go | 6 +++-- bruno/b2b-daniel/add-new-cart.yml | 2 +- bruno/b2b-daniel/add-product-to-cart (1).yml | 2 +- bruno/b2b-daniel/add-product-to-cart.yml | 2 +- bruno/b2b-daniel/change-cart-name.yml | 2 +- bruno/b2b-daniel/create-folder.yml | 6 ++--- bruno/b2b-daniel/create-index.yml | 2 +- bruno/b2b-daniel/delete-file.yml | 19 ++++++++++++++ .../{delete-entry.yml => delete-folder.yml} | 10 +++---- bruno/b2b-daniel/download-file.yml | 2 +- bruno/b2b-daniel/get-breadcrumb.yml | 2 +- bruno/b2b-daniel/get-category-tree.yml | 2 +- bruno/b2b-daniel/get-description.yml | 22 ---------------- bruno/b2b-daniel/get-indexes.yml | 2 +- bruno/b2b-daniel/get-product-description.yml | 2 +- bruno/b2b-daniel/get_countries.yml | 2 +- bruno/b2b-daniel/list-content.yml | 2 +- bruno/b2b-daniel/list-products.yml | 2 +- bruno/b2b-daniel/list-users.yml | 2 +- bruno/b2b-daniel/remove-index.yml | 2 +- bruno/b2b-daniel/retrieve-cart.yml | 2 +- bruno/b2b-daniel/retrieve-carts-info.yml | 2 +- bruno/b2b-daniel/save-product-description.yml | 2 +- bruno/b2b-daniel/search.yml | 2 +- bruno/b2b-daniel/test.yml | 2 +- .../translate-product-description.yml | 2 +- bruno/b2b-daniel/update-choice.yml | 2 +- bruno/b2b-daniel/upload-file.yml | 26 +++++++++++++++++++ storage/test.txt | 1 + 31 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 bruno/b2b-daniel/delete-file.yml rename bruno/b2b-daniel/{delete-entry.yml => delete-folder.yml} (52%) delete mode 100644 bruno/b2b-daniel/get-description.yml create mode 100644 bruno/b2b-daniel/upload-file.yml create mode 100644 storage/test.txt diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index 6722a9b..3760547 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -30,10 +30,10 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/list-content", handler.ListContent) r.Get("/download-file", handler.DownloadFile) - r.Post("/upload-file", handler.CreateFolder) + r.Post("/upload-file", handler.UploadFile) r.Get("/create-folder", handler.CreateFolder) - r.Get("/delete-file", handler.DeleteFile) - r.Get("/delete-folder", handler.DeleteFolder) + r.Delete("/delete-file", handler.DeleteFile) + r.Delete("/delete-folder", handler.DeleteFolder) return r } @@ -88,12 +88,11 @@ func (h *StorageHandler) UploadFile(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument))) } - err = h.storageService.UploadFile(abs_path, c.Query("name"), f) + err = h.storageService.UploadFile(c, abs_path, f) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - err = c.SaveFile(f, abs_path) return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) } diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go index ac838ff..e6c1461 100644 --- a/app/repos/storageRepo/storageRepo.go +++ b/app/repos/storageRepo/storageRepo.go @@ -5,13 +5,14 @@ import ( "os" "git.ma-al.com/goc_daniel/b2b/app/model" + "github.com/gofiber/fiber/v3" ) type UIStorageRepo interface { EntryInfo(abs_path string) (os.FileInfo, error) ListContent(abs_path string) (*[]model.EntryInList, error) OpenFile(abs_path string) (*os.File, error) - UploadFile(abs_path string, f *multipart.FileHeader) error + UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error CreateFolder(abs_path string) error DeleteFile(abs_path string) error DeleteFolder(abs_path string) error @@ -50,8 +51,8 @@ func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { return os.Open(abs_path) } -func (r *StorageRepo) UploadFile(abs_path string, f *multipart.FileHeader) error { - return nil +func (r *StorageRepo) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error { + return c.SaveFile(f, abs_path) } func (r *StorageRepo) CreateFolder(abs_path string) error { diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index f60f14b..3e01c61 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -9,6 +9,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" ) type StorageService struct { @@ -45,12 +46,13 @@ func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, in return f, filepath.Base(abs_path), info.Size(), nil } -func (s *StorageService) UploadFile(abs_path string, name string, f *multipart.FileHeader) error { +func (s *StorageService) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error { info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { return responseErrors.ErrFolderDoesNotExist } + name := f.Filename if name == "" || name == "." || name == ".." || filepath.Base(name) != name { return responseErrors.ErrBadAttribute } @@ -66,7 +68,7 @@ func (s *StorageService) UploadFile(abs_path string, name string, f *multipart.F if err == nil { return responseErrors.ErrNameTaken } else if os.IsNotExist(err) { - return s.storageRepo.UploadFile(abs_file_path, f) + return s.storageRepo.UploadFile(c, abs_file_path, f) } else { return err } diff --git a/bruno/b2b-daniel/add-new-cart.yml b/bruno/b2b-daniel/add-new-cart.yml index a6beb62..1b6cbde 100644 --- a/bruno/b2b-daniel/add-new-cart.yml +++ b/bruno/b2b-daniel/add-new-cart.yml @@ -1,7 +1,7 @@ info: name: add-new-cart type: http - seq: 11 + seq: 14 http: method: GET diff --git a/bruno/b2b-daniel/add-product-to-cart (1).yml b/bruno/b2b-daniel/add-product-to-cart (1).yml index 7441656..bf5252b 100644 --- a/bruno/b2b-daniel/add-product-to-cart (1).yml +++ b/bruno/b2b-daniel/add-product-to-cart (1).yml @@ -1,7 +1,7 @@ info: name: add-product-to-cart (1) type: http - seq: 16 + seq: 19 http: method: GET diff --git a/bruno/b2b-daniel/add-product-to-cart.yml b/bruno/b2b-daniel/add-product-to-cart.yml index 95e978b..045c7b0 100644 --- a/bruno/b2b-daniel/add-product-to-cart.yml +++ b/bruno/b2b-daniel/add-product-to-cart.yml @@ -1,7 +1,7 @@ info: name: add-product-to-cart type: http - seq: 15 + seq: 18 http: method: GET diff --git a/bruno/b2b-daniel/change-cart-name.yml b/bruno/b2b-daniel/change-cart-name.yml index 5dd32ee..ced76f1 100644 --- a/bruno/b2b-daniel/change-cart-name.yml +++ b/bruno/b2b-daniel/change-cart-name.yml @@ -1,7 +1,7 @@ info: name: change-cart-name type: http - seq: 12 + seq: 15 http: method: GET diff --git a/bruno/b2b-daniel/create-folder.yml b/bruno/b2b-daniel/create-folder.yml index fb04ec1..0f9fabb 100644 --- a/bruno/b2b-daniel/create-folder.yml +++ b/bruno/b2b-daniel/create-folder.yml @@ -1,17 +1,17 @@ info: name: create-folder type: http - seq: 22 + seq: 24 http: method: GET - url: http://localhost:3000/api/v1/restricted/storage/create-folder?path&name=../k + url: http://localhost:3000/api/v1/restricted/storage/create-folder?path=&name=folder params: - name: path value: "" type: query - name: name - value: ../k + value: folder type: query auth: inherit diff --git a/bruno/b2b-daniel/create-index.yml b/bruno/b2b-daniel/create-index.yml index 79eb62e..6f00a56 100644 --- a/bruno/b2b-daniel/create-index.yml +++ b/bruno/b2b-daniel/create-index.yml @@ -1,7 +1,7 @@ info: name: create-index type: http - seq: 7 + seq: 10 http: method: GET diff --git a/bruno/b2b-daniel/delete-file.yml b/bruno/b2b-daniel/delete-file.yml new file mode 100644 index 0000000..32f7104 --- /dev/null +++ b/bruno/b2b-daniel/delete-file.yml @@ -0,0 +1,19 @@ +info: + name: delete-file + type: http + seq: 25 + +http: + method: DELETE + url: http://localhost:3000/api/v1/restricted/storage/delete-file?path=/folder/test.txt + params: + - name: path + value: /folder/test.txt + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/delete-entry.yml b/bruno/b2b-daniel/delete-folder.yml similarity index 52% rename from bruno/b2b-daniel/delete-entry.yml rename to bruno/b2b-daniel/delete-folder.yml index baf63f2..49dacb7 100644 --- a/bruno/b2b-daniel/delete-entry.yml +++ b/bruno/b2b-daniel/delete-folder.yml @@ -1,14 +1,14 @@ info: - name: delete-entry + name: delete-folder type: http - seq: 23 + seq: 26 http: - method: GET - url: http://localhost:3000/api/v1/restricted/storage/delete-entry?path=folder2 + method: DELETE + url: http://localhost:3000/api/v1/restricted/storage/delete-folder?path=/folder/ params: - name: path - value: folder2 + value: /folder/ type: query auth: inherit diff --git a/bruno/b2b-daniel/download-file.yml b/bruno/b2b-daniel/download-file.yml index 468be8d..d400ef4 100644 --- a/bruno/b2b-daniel/download-file.yml +++ b/bruno/b2b-daniel/download-file.yml @@ -1,7 +1,7 @@ info: name: download-file type: http - seq: 20 + seq: 22 http: method: GET diff --git a/bruno/b2b-daniel/get-breadcrumb.yml b/bruno/b2b-daniel/get-breadcrumb.yml index 8b10c00..9a49428 100644 --- a/bruno/b2b-daniel/get-breadcrumb.yml +++ b/bruno/b2b-daniel/get-breadcrumb.yml @@ -1,7 +1,7 @@ info: name: get-breadcrumb type: http - seq: 18 + seq: 20 http: method: GET diff --git a/bruno/b2b-daniel/get-category-tree.yml b/bruno/b2b-daniel/get-category-tree.yml index c6b436e..b81d6d1 100644 --- a/bruno/b2b-daniel/get-category-tree.yml +++ b/bruno/b2b-daniel/get-category-tree.yml @@ -1,7 +1,7 @@ info: name: get-category-tree type: http - seq: 5 + seq: 8 http: method: GET diff --git a/bruno/b2b-daniel/get-description.yml b/bruno/b2b-daniel/get-description.yml deleted file mode 100644 index bd2137d..0000000 --- a/bruno/b2b-daniel/get-description.yml +++ /dev/null @@ -1,22 +0,0 @@ -info: - name: get-description - type: http - seq: 24 - -http: - method: GET - url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=2 - params: - - name: productID - value: "51" - type: query - - name: productLangID - value: "2" - type: query - auth: inherit - -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 diff --git a/bruno/b2b-daniel/get-indexes.yml b/bruno/b2b-daniel/get-indexes.yml index 850f7bc..ebca4aa 100644 --- a/bruno/b2b-daniel/get-indexes.yml +++ b/bruno/b2b-daniel/get-indexes.yml @@ -1,7 +1,7 @@ info: name: get-indexes type: http - seq: 9 + seq: 12 http: method: GET diff --git a/bruno/b2b-daniel/get-product-description.yml b/bruno/b2b-daniel/get-product-description.yml index 63a7447..4b6086d 100644 --- a/bruno/b2b-daniel/get-product-description.yml +++ b/bruno/b2b-daniel/get-product-description.yml @@ -1,7 +1,7 @@ info: name: get-product-description type: http - seq: 17 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/get_countries.yml b/bruno/b2b-daniel/get_countries.yml index e7077fd..07fed01 100644 --- a/bruno/b2b-daniel/get_countries.yml +++ b/bruno/b2b-daniel/get_countries.yml @@ -1,7 +1,7 @@ info: name: get_countries type: http - seq: 4 + seq: 7 http: method: GET diff --git a/bruno/b2b-daniel/list-content.yml b/bruno/b2b-daniel/list-content.yml index 8a9d600..972779f 100644 --- a/bruno/b2b-daniel/list-content.yml +++ b/bruno/b2b-daniel/list-content.yml @@ -1,7 +1,7 @@ info: name: list-content type: http - seq: 19 + seq: 21 http: method: GET diff --git a/bruno/b2b-daniel/list-products.yml b/bruno/b2b-daniel/list-products.yml index adc88a7..8fed3db 100644 --- a/bruno/b2b-daniel/list-products.yml +++ b/bruno/b2b-daniel/list-products.yml @@ -1,7 +1,7 @@ info: name: list-products type: http - seq: 1 + seq: 4 http: method: GET diff --git a/bruno/b2b-daniel/list-users.yml b/bruno/b2b-daniel/list-users.yml index 288afbc..4e435aa 100644 --- a/bruno/b2b-daniel/list-users.yml +++ b/bruno/b2b-daniel/list-users.yml @@ -1,7 +1,7 @@ info: name: list-users type: http - seq: 2 + seq: 5 http: method: GET diff --git a/bruno/b2b-daniel/remove-index.yml b/bruno/b2b-daniel/remove-index.yml index aecc977..6ee9ebb 100644 --- a/bruno/b2b-daniel/remove-index.yml +++ b/bruno/b2b-daniel/remove-index.yml @@ -1,7 +1,7 @@ info: name: remove-index type: http - seq: 8 + seq: 11 http: method: DELETE diff --git a/bruno/b2b-daniel/retrieve-cart.yml b/bruno/b2b-daniel/retrieve-cart.yml index 114116c..8316965 100644 --- a/bruno/b2b-daniel/retrieve-cart.yml +++ b/bruno/b2b-daniel/retrieve-cart.yml @@ -1,7 +1,7 @@ info: name: retrieve-cart type: http - seq: 14 + seq: 17 http: method: GET diff --git a/bruno/b2b-daniel/retrieve-carts-info.yml b/bruno/b2b-daniel/retrieve-carts-info.yml index f15ce51..8d76d52 100644 --- a/bruno/b2b-daniel/retrieve-carts-info.yml +++ b/bruno/b2b-daniel/retrieve-carts-info.yml @@ -1,7 +1,7 @@ info: name: retrieve-carts-info type: http - seq: 13 + seq: 16 http: method: GET diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml index 201f4f8..eb3fb26 100644 --- a/bruno/b2b-daniel/save-product-description.yml +++ b/bruno/b2b-daniel/save-product-description.yml @@ -1,7 +1,7 @@ info: name: save-product-description type: http - seq: 25 + seq: 3 http: method: POST diff --git a/bruno/b2b-daniel/search.yml b/bruno/b2b-daniel/search.yml index 39d3f04..16cb913 100644 --- a/bruno/b2b-daniel/search.yml +++ b/bruno/b2b-daniel/search.yml @@ -1,7 +1,7 @@ info: name: search type: http - seq: 10 + seq: 13 http: method: GET diff --git a/bruno/b2b-daniel/test.yml b/bruno/b2b-daniel/test.yml index e63fe60..0b73d2f 100644 --- a/bruno/b2b-daniel/test.yml +++ b/bruno/b2b-daniel/test.yml @@ -1,7 +1,7 @@ info: name: test type: http - seq: 6 + seq: 9 http: method: GET diff --git a/bruno/b2b-daniel/translate-product-description.yml b/bruno/b2b-daniel/translate-product-description.yml index f08dc01..2e8a7b4 100644 --- a/bruno/b2b-daniel/translate-product-description.yml +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -1,7 +1,7 @@ info: name: translate-product-description type: http - seq: 19 + seq: 2 http: method: GET diff --git a/bruno/b2b-daniel/update-choice.yml b/bruno/b2b-daniel/update-choice.yml index 53a469b..3cd6ece 100644 --- a/bruno/b2b-daniel/update-choice.yml +++ b/bruno/b2b-daniel/update-choice.yml @@ -1,7 +1,7 @@ info: name: update-choice type: http - seq: 3 + seq: 6 http: method: POST diff --git a/bruno/b2b-daniel/upload-file.yml b/bruno/b2b-daniel/upload-file.yml new file mode 100644 index 0000000..41ab47a --- /dev/null +++ b/bruno/b2b-daniel/upload-file.yml @@ -0,0 +1,26 @@ +info: + name: upload-file + type: http + seq: 23 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/storage/upload-file?path=folder/ + params: + - name: path + value: folder/ + type: query + body: + type: multipart-form + data: + - name: document + type: file + value: + - /home/daniel/coding/work/b2b/storage/folder1/test.txt + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/storage/test.txt b/storage/test.txt new file mode 100644 index 0000000..273c1a9 --- /dev/null +++ b/storage/test.txt @@ -0,0 +1 @@ +This is a test. \ No newline at end of file From 7d4242abb191a012edef10e7a3822084f69d0277 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 2 Apr 2026 13:52:50 +0200 Subject: [PATCH 05/25] move path to params --- app/delivery/web/api/restricted/storage.go | 25 ++++++++++--------- bruno/{b2b-daniel => b2b_daniel}/.gitignore | 0 bruno/b2b_daniel/auth/folder.yml | 7 ++++++ .../auth}/update-choice.yml | 2 +- .../carts}/add-new-cart.yml | 2 +- .../carts}/add-product-to-cart (1).yml | 2 +- .../carts}/add-product-to-cart.yml | 2 +- .../carts}/change-cart-name.yml | 2 +- bruno/b2b_daniel/carts/folder.yml | 7 ++++++ .../carts}/retrieve-cart.yml | 2 +- .../carts}/retrieve-carts-info.yml | 2 +- .../b2b_daniel/langs-and-countries/folder.yml | 7 ++++++ .../langs-and-countries}/get_countries.yml | 2 +- bruno/b2b_daniel/list/folder.yml | 7 ++++++ .../list}/list-products.yml | 2 +- .../list}/list-users.yml | 2 +- bruno/b2b_daniel/menu/folder.yml | 7 ++++++ .../menu}/get-breadcrumb.yml | 2 +- .../menu}/get-category-tree.yml | 2 +- .../opencollection.yml | 0 .../b2b_daniel/product-translation/folder.yml | 7 ++++++ .../get-product-description.yml | 0 .../save-product-description.yml | 2 +- .../translate-product-description.yml | 2 +- .../search}/create-index.yml | 4 +-- bruno/b2b_daniel/search/folder.yml | 7 ++++++ .../search}/get-indexes.yml | 2 +- .../search}/remove-index.yml | 2 +- .../search}/search.yml | 4 +-- .../search}/test.yml | 4 +-- .../storage}/create-folder.yml | 7 ++---- .../storage}/delete-file.yml | 8 ++---- .../storage}/delete-folder.yml | 8 ++---- .../storage}/download-file.yml | 8 ++---- bruno/b2b_daniel/storage/folder.yml | 7 ++++++ .../storage}/list-content.yml | 8 ++---- .../storage}/upload-file.yml | 10 +++----- 37 files changed, 104 insertions(+), 70 deletions(-) rename bruno/{b2b-daniel => b2b_daniel}/.gitignore (100%) create mode 100644 bruno/b2b_daniel/auth/folder.yml rename bruno/{b2b-daniel => b2b_daniel/auth}/update-choice.yml (97%) rename bruno/{b2b-daniel => b2b_daniel/carts}/add-new-cart.yml (95%) rename bruno/{b2b-daniel => b2b_daniel/carts}/add-product-to-cart (1).yml (97%) rename bruno/{b2b-daniel => b2b_daniel/carts}/add-product-to-cart.yml (98%) rename bruno/{b2b-daniel => b2b_daniel/carts}/change-cart-name.yml (97%) create mode 100644 bruno/b2b_daniel/carts/folder.yml rename bruno/{b2b-daniel => b2b_daniel/carts}/retrieve-cart.yml (96%) rename bruno/{b2b-daniel => b2b_daniel/carts}/retrieve-carts-info.yml (96%) create mode 100644 bruno/b2b_daniel/langs-and-countries/folder.yml rename bruno/{b2b-daniel => b2b_daniel/langs-and-countries}/get_countries.yml (96%) create mode 100644 bruno/b2b_daniel/list/folder.yml rename bruno/{b2b-daniel => b2b_daniel/list}/list-products.yml (97%) rename bruno/{b2b-daniel => b2b_daniel/list}/list-users.yml (97%) create mode 100644 bruno/b2b_daniel/menu/folder.yml rename bruno/{b2b-daniel => b2b_daniel/menu}/get-breadcrumb.yml (97%) rename bruno/{b2b-daniel => b2b_daniel/menu}/get-category-tree.yml (97%) rename bruno/{b2b-daniel => b2b_daniel}/opencollection.yml (100%) create mode 100644 bruno/b2b_daniel/product-translation/folder.yml rename bruno/{b2b-daniel => b2b_daniel/product-translation}/get-product-description.yml (100%) rename bruno/{b2b-daniel => b2b_daniel/product-translation}/save-product-description.yml (99%) rename bruno/{b2b-daniel => b2b_daniel/product-translation}/translate-product-description.yml (98%) rename bruno/{b2b-daniel => b2b_daniel/search}/create-index.yml (65%) create mode 100644 bruno/b2b_daniel/search/folder.yml rename bruno/{b2b-daniel => b2b_daniel/search}/get-indexes.yml (96%) rename bruno/{b2b-daniel => b2b_daniel/search}/remove-index.yml (96%) rename bruno/{b2b-daniel => b2b_daniel/search}/search.yml (75%) rename bruno/{b2b-daniel => b2b_daniel/search}/test.yml (67%) rename bruno/{b2b-daniel => b2b_daniel/storage}/create-folder.yml (77%) rename bruno/{b2b-daniel => b2b_daniel/storage}/delete-file.yml (67%) rename bruno/{b2b-daniel => b2b_daniel/storage}/delete-folder.yml (70%) rename bruno/{b2b-daniel => b2b_daniel/storage}/download-file.yml (66%) create mode 100644 bruno/b2b_daniel/storage/folder.yml rename bruno/{b2b-daniel => b2b_daniel/storage}/list-content.yml (70%) rename bruno/{b2b-daniel => b2b_daniel/storage}/upload-file.yml (67%) diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index 3760547..b851f9a 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -27,13 +27,13 @@ func NewStorageHandler() *StorageHandler { func StorageHandlerRoutes(r fiber.Router) fiber.Router { handler := NewStorageHandler() - r.Get("/list-content", handler.ListContent) - r.Get("/download-file", handler.DownloadFile) + r.Get("/list-content/*", handler.ListContent) + r.Get("/download-file/*", handler.DownloadFile) - r.Post("/upload-file", handler.UploadFile) - r.Get("/create-folder", handler.CreateFolder) - r.Delete("/delete-file", handler.DeleteFile) - r.Delete("/delete-folder", handler.DeleteFolder) + r.Post("/upload-file/*", handler.UploadFile) + r.Get("/create-folder/*", handler.CreateFolder) + r.Delete("/delete-file/*", handler.DeleteFile) + r.Delete("/delete-folder/*", handler.DeleteFolder) return r } @@ -41,7 +41,7 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { // accepted path looks like e.g. "/folder1/" or "folder1" func (h *StorageHandler) ListContent(c fiber.Ctx) error { // relative path defaults to root directory - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -58,7 +58,7 @@ func (h *StorageHandler) ListContent(c fiber.Ctx) error { } func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -72,11 +72,12 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { c.Attachment(filename) c.Set("Content-Length", strconv.FormatInt(filesize, 10)) + c.Set("Content-Type", "application/octet-stream") return c.SendStream(f, int(filesize)) } func (h *StorageHandler) UploadFile(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -98,7 +99,7 @@ func (h *StorageHandler) UploadFile(c fiber.Ctx) error { } func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -114,7 +115,7 @@ func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { } func (h *StorageHandler) DeleteFile(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -130,7 +131,7 @@ func (h *StorageHandler) DeleteFile(c fiber.Ctx) error { } func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/bruno/b2b-daniel/.gitignore b/bruno/b2b_daniel/.gitignore similarity index 100% rename from bruno/b2b-daniel/.gitignore rename to bruno/b2b_daniel/.gitignore diff --git a/bruno/b2b_daniel/auth/folder.yml b/bruno/b2b_daniel/auth/folder.yml new file mode 100644 index 0000000..120ac3e --- /dev/null +++ b/bruno/b2b_daniel/auth/folder.yml @@ -0,0 +1,7 @@ +info: + name: auth + type: folder + seq: 1 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/update-choice.yml b/bruno/b2b_daniel/auth/update-choice.yml similarity index 97% rename from bruno/b2b-daniel/update-choice.yml rename to bruno/b2b_daniel/auth/update-choice.yml index 3cd6ece..0a511b0 100644 --- a/bruno/b2b-daniel/update-choice.yml +++ b/bruno/b2b_daniel/auth/update-choice.yml @@ -1,7 +1,7 @@ info: name: update-choice type: http - seq: 6 + seq: 1 http: method: POST diff --git a/bruno/b2b-daniel/add-new-cart.yml b/bruno/b2b_daniel/carts/add-new-cart.yml similarity index 95% rename from bruno/b2b-daniel/add-new-cart.yml rename to bruno/b2b_daniel/carts/add-new-cart.yml index 1b6cbde..20199cf 100644 --- a/bruno/b2b-daniel/add-new-cart.yml +++ b/bruno/b2b_daniel/carts/add-new-cart.yml @@ -1,7 +1,7 @@ info: name: add-new-cart type: http - seq: 14 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/add-product-to-cart (1).yml b/bruno/b2b_daniel/carts/add-product-to-cart (1).yml similarity index 97% rename from bruno/b2b-daniel/add-product-to-cart (1).yml rename to bruno/b2b_daniel/carts/add-product-to-cart (1).yml index bf5252b..eb7a5a1 100644 --- a/bruno/b2b-daniel/add-product-to-cart (1).yml +++ b/bruno/b2b_daniel/carts/add-product-to-cart (1).yml @@ -1,7 +1,7 @@ info: name: add-product-to-cart (1) type: http - seq: 19 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/add-product-to-cart.yml b/bruno/b2b_daniel/carts/add-product-to-cart.yml similarity index 98% rename from bruno/b2b-daniel/add-product-to-cart.yml rename to bruno/b2b_daniel/carts/add-product-to-cart.yml index 045c7b0..ff780c1 100644 --- a/bruno/b2b-daniel/add-product-to-cart.yml +++ b/bruno/b2b_daniel/carts/add-product-to-cart.yml @@ -1,7 +1,7 @@ info: name: add-product-to-cart type: http - seq: 18 + seq: 14 http: method: GET diff --git a/bruno/b2b-daniel/change-cart-name.yml b/bruno/b2b_daniel/carts/change-cart-name.yml similarity index 97% rename from bruno/b2b-daniel/change-cart-name.yml rename to bruno/b2b_daniel/carts/change-cart-name.yml index ced76f1..08838dc 100644 --- a/bruno/b2b-daniel/change-cart-name.yml +++ b/bruno/b2b_daniel/carts/change-cart-name.yml @@ -1,7 +1,7 @@ info: name: change-cart-name type: http - seq: 15 + seq: 1 http: method: GET diff --git a/bruno/b2b_daniel/carts/folder.yml b/bruno/b2b_daniel/carts/folder.yml new file mode 100644 index 0000000..4f51dfd --- /dev/null +++ b/bruno/b2b_daniel/carts/folder.yml @@ -0,0 +1,7 @@ +info: + name: carts + type: folder + seq: 7 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/retrieve-cart.yml b/bruno/b2b_daniel/carts/retrieve-cart.yml similarity index 96% rename from bruno/b2b-daniel/retrieve-cart.yml rename to bruno/b2b_daniel/carts/retrieve-cart.yml index 8316965..69c5e2e 100644 --- a/bruno/b2b-daniel/retrieve-cart.yml +++ b/bruno/b2b_daniel/carts/retrieve-cart.yml @@ -1,7 +1,7 @@ info: name: retrieve-cart type: http - seq: 17 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/retrieve-carts-info.yml b/bruno/b2b_daniel/carts/retrieve-carts-info.yml similarity index 96% rename from bruno/b2b-daniel/retrieve-carts-info.yml rename to bruno/b2b_daniel/carts/retrieve-carts-info.yml index 8d76d52..479be4e 100644 --- a/bruno/b2b-daniel/retrieve-carts-info.yml +++ b/bruno/b2b_daniel/carts/retrieve-carts-info.yml @@ -1,7 +1,7 @@ info: name: retrieve-carts-info type: http - seq: 16 + seq: 1 http: method: GET diff --git a/bruno/b2b_daniel/langs-and-countries/folder.yml b/bruno/b2b_daniel/langs-and-countries/folder.yml new file mode 100644 index 0000000..b895323 --- /dev/null +++ b/bruno/b2b_daniel/langs-and-countries/folder.yml @@ -0,0 +1,7 @@ +info: + name: langs-and-countries + type: folder + seq: 4 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/get_countries.yml b/bruno/b2b_daniel/langs-and-countries/get_countries.yml similarity index 96% rename from bruno/b2b-daniel/get_countries.yml rename to bruno/b2b_daniel/langs-and-countries/get_countries.yml index 07fed01..b7204b4 100644 --- a/bruno/b2b-daniel/get_countries.yml +++ b/bruno/b2b_daniel/langs-and-countries/get_countries.yml @@ -1,7 +1,7 @@ info: name: get_countries type: http - seq: 7 + seq: 1 http: method: GET diff --git a/bruno/b2b_daniel/list/folder.yml b/bruno/b2b_daniel/list/folder.yml new file mode 100644 index 0000000..52fa517 --- /dev/null +++ b/bruno/b2b_daniel/list/folder.yml @@ -0,0 +1,7 @@ +info: + name: list + type: folder + seq: 3 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/list-products.yml b/bruno/b2b_daniel/list/list-products.yml similarity index 97% rename from bruno/b2b-daniel/list-products.yml rename to bruno/b2b_daniel/list/list-products.yml index 8fed3db..adc88a7 100644 --- a/bruno/b2b-daniel/list-products.yml +++ b/bruno/b2b_daniel/list/list-products.yml @@ -1,7 +1,7 @@ info: name: list-products type: http - seq: 4 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/list-users.yml b/bruno/b2b_daniel/list/list-users.yml similarity index 97% rename from bruno/b2b-daniel/list-users.yml rename to bruno/b2b_daniel/list/list-users.yml index 4e435aa..85d70fa 100644 --- a/bruno/b2b-daniel/list-users.yml +++ b/bruno/b2b_daniel/list/list-users.yml @@ -1,7 +1,7 @@ info: name: list-users type: http - seq: 5 + seq: 1 http: method: GET diff --git a/bruno/b2b_daniel/menu/folder.yml b/bruno/b2b_daniel/menu/folder.yml new file mode 100644 index 0000000..32bc162 --- /dev/null +++ b/bruno/b2b_daniel/menu/folder.yml @@ -0,0 +1,7 @@ +info: + name: menu + type: folder + seq: 5 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/get-breadcrumb.yml b/bruno/b2b_daniel/menu/get-breadcrumb.yml similarity index 97% rename from bruno/b2b-daniel/get-breadcrumb.yml rename to bruno/b2b_daniel/menu/get-breadcrumb.yml index 9a49428..a805790 100644 --- a/bruno/b2b-daniel/get-breadcrumb.yml +++ b/bruno/b2b_daniel/menu/get-breadcrumb.yml @@ -1,7 +1,7 @@ info: name: get-breadcrumb type: http - seq: 20 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/get-category-tree.yml b/bruno/b2b_daniel/menu/get-category-tree.yml similarity index 97% rename from bruno/b2b-daniel/get-category-tree.yml rename to bruno/b2b_daniel/menu/get-category-tree.yml index b81d6d1..6e9d875 100644 --- a/bruno/b2b-daniel/get-category-tree.yml +++ b/bruno/b2b_daniel/menu/get-category-tree.yml @@ -1,7 +1,7 @@ info: name: get-category-tree type: http - seq: 8 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/opencollection.yml b/bruno/b2b_daniel/opencollection.yml similarity index 100% rename from bruno/b2b-daniel/opencollection.yml rename to bruno/b2b_daniel/opencollection.yml diff --git a/bruno/b2b_daniel/product-translation/folder.yml b/bruno/b2b_daniel/product-translation/folder.yml new file mode 100644 index 0000000..cda7116 --- /dev/null +++ b/bruno/b2b_daniel/product-translation/folder.yml @@ -0,0 +1,7 @@ +info: + name: product-translation + type: folder + seq: 2 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/get-product-description.yml b/bruno/b2b_daniel/product-translation/get-product-description.yml similarity index 100% rename from bruno/b2b-daniel/get-product-description.yml rename to bruno/b2b_daniel/product-translation/get-product-description.yml diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b_daniel/product-translation/save-product-description.yml similarity index 99% rename from bruno/b2b-daniel/save-product-description.yml rename to bruno/b2b_daniel/product-translation/save-product-description.yml index eb3fb26..201f4f8 100644 --- a/bruno/b2b-daniel/save-product-description.yml +++ b/bruno/b2b_daniel/product-translation/save-product-description.yml @@ -1,7 +1,7 @@ info: name: save-product-description type: http - seq: 3 + seq: 25 http: method: POST diff --git a/bruno/b2b-daniel/translate-product-description.yml b/bruno/b2b_daniel/product-translation/translate-product-description.yml similarity index 98% rename from bruno/b2b-daniel/translate-product-description.yml rename to bruno/b2b_daniel/product-translation/translate-product-description.yml index 2e8a7b4..12c65b4 100644 --- a/bruno/b2b-daniel/translate-product-description.yml +++ b/bruno/b2b_daniel/product-translation/translate-product-description.yml @@ -1,7 +1,7 @@ info: name: translate-product-description type: http - seq: 2 + seq: 24 http: method: GET diff --git a/bruno/b2b-daniel/create-index.yml b/bruno/b2b_daniel/search/create-index.yml similarity index 65% rename from bruno/b2b-daniel/create-index.yml rename to bruno/b2b_daniel/search/create-index.yml index 6f00a56..1469dc4 100644 --- a/bruno/b2b-daniel/create-index.yml +++ b/bruno/b2b_daniel/search/create-index.yml @@ -1,11 +1,11 @@ info: name: create-index type: http - seq: 10 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/meili-search/create-index + url: http://localhost:3000/api/v1/restricted/search/create-index auth: inherit settings: diff --git a/bruno/b2b_daniel/search/folder.yml b/bruno/b2b_daniel/search/folder.yml new file mode 100644 index 0000000..7b92aae --- /dev/null +++ b/bruno/b2b_daniel/search/folder.yml @@ -0,0 +1,7 @@ +info: + name: search + type: folder + seq: 6 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/get-indexes.yml b/bruno/b2b_daniel/search/get-indexes.yml similarity index 96% rename from bruno/b2b-daniel/get-indexes.yml rename to bruno/b2b_daniel/search/get-indexes.yml index ebca4aa..0b85acf 100644 --- a/bruno/b2b-daniel/get-indexes.yml +++ b/bruno/b2b_daniel/search/get-indexes.yml @@ -1,7 +1,7 @@ info: name: get-indexes type: http - seq: 12 + seq: 1 http: method: GET diff --git a/bruno/b2b-daniel/remove-index.yml b/bruno/b2b_daniel/search/remove-index.yml similarity index 96% rename from bruno/b2b-daniel/remove-index.yml rename to bruno/b2b_daniel/search/remove-index.yml index 6ee9ebb..c1c8856 100644 --- a/bruno/b2b-daniel/remove-index.yml +++ b/bruno/b2b_daniel/search/remove-index.yml @@ -1,7 +1,7 @@ info: name: remove-index type: http - seq: 11 + seq: 1 http: method: DELETE diff --git a/bruno/b2b-daniel/search.yml b/bruno/b2b_daniel/search/search.yml similarity index 75% rename from bruno/b2b-daniel/search.yml rename to bruno/b2b_daniel/search/search.yml index 16cb913..5200d85 100644 --- a/bruno/b2b-daniel/search.yml +++ b/bruno/b2b_daniel/search/search.yml @@ -1,11 +1,11 @@ info: name: search type: http - seq: 13 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/meili-search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0 + url: http://localhost:3000/api/v1/restricted/search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0 params: - name: query value: w diff --git a/bruno/b2b-daniel/test.yml b/bruno/b2b_daniel/search/test.yml similarity index 67% rename from bruno/b2b-daniel/test.yml rename to bruno/b2b_daniel/search/test.yml index 0b73d2f..60fe55a 100644 --- a/bruno/b2b-daniel/test.yml +++ b/bruno/b2b_daniel/search/test.yml @@ -1,11 +1,11 @@ info: name: test type: http - seq: 9 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/meili-search/test + url: http://localhost:3000/api/v1/restricted/search/test auth: inherit settings: diff --git a/bruno/b2b-daniel/create-folder.yml b/bruno/b2b_daniel/storage/create-folder.yml similarity index 77% rename from bruno/b2b-daniel/create-folder.yml rename to bruno/b2b_daniel/storage/create-folder.yml index 0f9fabb..1250965 100644 --- a/bruno/b2b-daniel/create-folder.yml +++ b/bruno/b2b_daniel/storage/create-folder.yml @@ -1,15 +1,12 @@ info: name: create-folder type: http - seq: 24 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/storage/create-folder?path=&name=folder + url: http://localhost:3000/api/v1/restricted/storage/create-folder?name=folder params: - - name: path - value: "" - type: query - name: name value: folder type: query diff --git a/bruno/b2b-daniel/delete-file.yml b/bruno/b2b_daniel/storage/delete-file.yml similarity index 67% rename from bruno/b2b-daniel/delete-file.yml rename to bruno/b2b_daniel/storage/delete-file.yml index 32f7104..01b1744 100644 --- a/bruno/b2b-daniel/delete-file.yml +++ b/bruno/b2b_daniel/storage/delete-file.yml @@ -1,15 +1,11 @@ info: name: delete-file type: http - seq: 25 + seq: 1 http: method: DELETE - url: http://localhost:3000/api/v1/restricted/storage/delete-file?path=/folder/test.txt - params: - - name: path - value: /folder/test.txt - type: query + url: http://localhost:3000/api/v1/restricted/storage/delete-file/folder1/TODO.txt auth: inherit settings: diff --git a/bruno/b2b-daniel/delete-folder.yml b/bruno/b2b_daniel/storage/delete-folder.yml similarity index 70% rename from bruno/b2b-daniel/delete-folder.yml rename to bruno/b2b_daniel/storage/delete-folder.yml index 49dacb7..3c578ce 100644 --- a/bruno/b2b-daniel/delete-folder.yml +++ b/bruno/b2b_daniel/storage/delete-folder.yml @@ -1,15 +1,11 @@ info: name: delete-folder type: http - seq: 26 + seq: 1 http: method: DELETE - url: http://localhost:3000/api/v1/restricted/storage/delete-folder?path=/folder/ - params: - - name: path - value: /folder/ - type: query + url: http://localhost:3000/api/v1/restricted/storage/delete-folder/folder/ auth: inherit settings: diff --git a/bruno/b2b-daniel/download-file.yml b/bruno/b2b_daniel/storage/download-file.yml similarity index 66% rename from bruno/b2b-daniel/download-file.yml rename to bruno/b2b_daniel/storage/download-file.yml index d400ef4..d6c65a1 100644 --- a/bruno/b2b-daniel/download-file.yml +++ b/bruno/b2b_daniel/storage/download-file.yml @@ -1,15 +1,11 @@ info: name: download-file type: http - seq: 22 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/storage/download-file?path=/folder1/test.txt - params: - - name: path - value: /folder1/test.txt - type: query + url: http://localhost:3000/api/v1/restricted/storage/download-file/folder1/test.xlsx auth: inherit settings: diff --git a/bruno/b2b_daniel/storage/folder.yml b/bruno/b2b_daniel/storage/folder.yml new file mode 100644 index 0000000..70062a4 --- /dev/null +++ b/bruno/b2b_daniel/storage/folder.yml @@ -0,0 +1,7 @@ +info: + name: storage + type: folder + seq: 1 + +request: + auth: inherit diff --git a/bruno/b2b-daniel/list-content.yml b/bruno/b2b_daniel/storage/list-content.yml similarity index 70% rename from bruno/b2b-daniel/list-content.yml rename to bruno/b2b_daniel/storage/list-content.yml index 972779f..ed67b6d 100644 --- a/bruno/b2b-daniel/list-content.yml +++ b/bruno/b2b_daniel/storage/list-content.yml @@ -1,15 +1,11 @@ info: name: list-content type: http - seq: 21 + seq: 1 http: method: GET - url: http://localhost:3000/api/v1/restricted/storage/list-content?path=/folder1 - params: - - name: path - value: /folder1 - type: query + url: http://localhost:3000/api/v1/restricted/storage/list-content/folder1 auth: inherit settings: diff --git a/bruno/b2b-daniel/upload-file.yml b/bruno/b2b_daniel/storage/upload-file.yml similarity index 67% rename from bruno/b2b-daniel/upload-file.yml rename to bruno/b2b_daniel/storage/upload-file.yml index 41ab47a..aa8d740 100644 --- a/bruno/b2b-daniel/upload-file.yml +++ b/bruno/b2b_daniel/storage/upload-file.yml @@ -1,22 +1,18 @@ info: name: upload-file type: http - seq: 23 + seq: 1 http: method: POST - url: http://localhost:3000/api/v1/restricted/storage/upload-file?path=folder/ - params: - - name: path - value: folder/ - type: query + url: http://localhost:3000/api/v1/restricted/storage/upload-file/folder1/ body: type: multipart-form data: - name: document type: file value: - - /home/daniel/coding/work/b2b/storage/folder1/test.txt + - /home/daniel/TODO.txt auth: inherit settings: From 395d67029836a64fc9edd11ba84f2208ad607e86 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 2 Apr 2026 14:00:58 +0200 Subject: [PATCH 06/25] add storage to .gitignore --- .gitignore | 4 +++- storage/.gitkeep | 0 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 storage/.gitkeep diff --git a/.gitignore b/.gitignore index 0408331..d9058fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ bin/ i18n/*.json *_templ.go tmp/main -test.go \ No newline at end of file +test.go +storage/* +!storage/.gitkeep \ No newline at end of file diff --git a/storage/.gitkeep b/storage/.gitkeep new file mode 100644 index 0000000..e69de29 From a988bbbc33640417bda1a7652c7fa66e3f01c5d5 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 3 Apr 2026 11:25:16 +0200 Subject: [PATCH 07/25] added copying and moving --- app/delivery/web/api/restricted/storage.go | 46 +++++++++++++++++++ app/repos/storageRepo/storageRepo.go | 32 +++++++++++++ app/service/storageService/storageService.go | 29 ++++++++++++ bruno/b2b-daniel/save-product-description.yml | 28 +++++++++++ .../translate-product-description.yml | 28 +++++++++++ bruno/b2b_daniel/storage/copy.yml | 19 ++++++++ bruno/b2b_daniel/storage/move.yml | 19 ++++++++ 7 files changed, 201 insertions(+) create mode 100644 bruno/b2b-daniel/save-product-description.yml create mode 100644 bruno/b2b-daniel/translate-product-description.yml create mode 100644 bruno/b2b_daniel/storage/copy.yml create mode 100644 bruno/b2b_daniel/storage/move.yml diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index b851f9a..f337547 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -30,6 +30,8 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/list-content/*", handler.ListContent) r.Get("/download-file/*", handler.DownloadFile) + r.Get("/move/*", handler.Move) + r.Get("/copy/*", handler.Copy) r.Post("/upload-file/*", handler.UploadFile) r.Get("/create-folder/*", handler.CreateFolder) r.Delete("/delete-file/*", handler.DeleteFile) @@ -38,6 +40,50 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { return r } +func (h *StorageHandler) Move(c fiber.Ctx) error { + src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.Move(src_abs_path, dest_abs_path) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) Copy(c fiber.Ctx) error { + src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.Copy(src_abs_path, dest_abs_path) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + // accepted path looks like e.g. "/folder1/" or "folder1" func (h *StorageHandler) ListContent(c fiber.Ctx) error { // relative path defaults to root directory diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go index e6c1461..08441a6 100644 --- a/app/repos/storageRepo/storageRepo.go +++ b/app/repos/storageRepo/storageRepo.go @@ -1,6 +1,7 @@ package storageRepo import ( + "io" "mime/multipart" "os" @@ -11,6 +12,8 @@ import ( type UIStorageRepo interface { EntryInfo(abs_path string) (os.FileInfo, error) ListContent(abs_path string) (*[]model.EntryInList, error) + Move(src_abs_path string, dest_abs_path string) error + Copy(src_abs_path string, dest_abs_path string) error OpenFile(abs_path string) (*os.File, error) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error CreateFolder(abs_path string) error @@ -47,6 +50,35 @@ func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) return &entries_in_list, nil } +func (r *StorageRepo) Move(src_abs_path string, dest_abs_path string) error { + return os.Rename(src_abs_path, dest_abs_path) +} + +func (r *StorageRepo) Copy(src_abs_path string, dest_abs_path string) error { + in, err := os.Open(src_abs_path) + if err != nil { + return err + } + defer in.Close() + + info, err := in.Stat() + if err != nil { + return err + } + + out, err := os.OpenFile(dest_abs_path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Sync() +} + func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { return os.Open(abs_path) } diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index 3e01c61..28b78ed 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -46,6 +46,35 @@ func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, in return f, filepath.Base(abs_path), info.Size(), nil } +func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error { + _, err := s.storageRepo.EntryInfo(src_abs_path) + if err != nil { + return responseErrors.ErrFileDoesNotExist + } + + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.Move(src_abs_path, dest_abs_path) + } else { + return err + } +} +func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error { + _, err := s.storageRepo.EntryInfo(src_abs_path) + if err != nil { + return responseErrors.ErrFileDoesNotExist + } + + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.Copy(src_abs_path, dest_abs_path) + } else { + return err + } +} + func (s *StorageService) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error { info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml new file mode 100644 index 0000000..3ea103c --- /dev/null +++ b/bruno/b2b-daniel/save-product-description.yml @@ -0,0 +1,28 @@ +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=3 + params: + - name: productID + value: "1" + type: query + - name: productLangID + value: "3" + type: query + body: + type: json + data: |- + { + "description": "

Der Einsatz von Rehabilitationsrollen in verschiedenen Übungen und Behandlungen wirkt sich positiv auf die Reduzierung von Verletzungen und die Genesungschancen aus. Sie werden in der Rehabilitation, bei Korrekturgymnastik sowie in der traditionellen und Sportmassage eingesetzt, da sie ideal zum Anheben und Spreizen von Gliedmaßen geeignet sind. Zudem können sie zur Unterstützung von Knien, Füßen, Armen und Schultern verwendet werden. Auch für Kinder sind Rehabilitationsrollen empfehlenswert; ihre spielerische Anwendung fördert die Entwicklung der Grobmotorik.

Dank der großen Auswahl an Farben und Größen lässt sich ein Übungsset zusammenstellen, das in jeder Physiotherapiepraxis, jedem Massageraum, jeder Schule oder jedem Kindergarten benötigt wird.

Die Rehabilitationsrolle ist ein Medizinprodukt, das den grundlegenden Anforderungen an Medizinprodukte und den Bestimmungen des Medizinproduktegesetzes entspricht, im Register für Medizinprodukte des Amtes für die Registrierung von Arzneimitteln, Medizinprodukten und Biozidprodukten eingetragen ist, mit der Konformitätserklärung des Herstellers versehen ist und das CE-Zeichen trägt.

\"Medizinprodukt\"

Empfohlene Verwendung:

  • in der Rehabilitation
  • während Massagen (traditionell, Sport)
  • in der Korrekturgymnastik (insbesondere für Kinder)
  • zur Linderung von Verletzungen einzelner Körperteile
  • Zur Unterstützung von: Knien, Knöcheln, Kopf des Patienten
  • bei Übungen zur Entwicklung der motorischen Fähigkeiten von Kindern
  • in Schönheitssalons
  • in Kinderspielzimmern

Materialspezifikationen:

Abdeckung: PVC-beschichtetes Material, das für medizinische Geräte vorgesehen ist und daher sehr leicht zu reinigen und zu desinfizieren ist:

  • Material gemäß REACH-Verordnung, zertifiziert mit dem STANDARD 100 Zertifikat von OEKO-TEX®.
  • Enthält keine Phthalate
  • feuerfest
  • resistent gegenüber physiologischen Flüssigkeiten (Blut, Urin, Schweiß) und Alkohol
  • UV-beständig, daher auch für den Einsatz im Freien geeignet.
  • kratzfest
  • ölbeständig

\"ERREICHEN\"\"Öko-Tex\"Enthält\"Feuerfest\"\"Alkoholbeständig\"\"UV-beständig\"\"Für\"Kratzfest\"\"Ölbeständig\"

Füllung: mittelharter Polyurethanschaum mit erhöhter Verformungsbeständigkeit:

  • besitzt ein Hygienezertifikat, ausgestellt vom Institut für Maritime und Tropenmedizin in Gdynia
  • zertifiziert mit dem STANDARD 100 by OEKO-TEX® Zertifikat – Produktklasse I, ausgestellt vom Textilforschungsinstitut in Łódź
  • Hergestellt aus hochwertigen Rohstoffen, die die Ozonschicht nicht schädigen.

\"Öko-Tex\"Hygienezertifikat\"\"Hygienezertifikat\"

" + } + 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..5f3a787 --- /dev/null +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -0,0 +1,28 @@ +info: + name: translate-product-description + type: http + seq: 21 + +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/bruno/b2b_daniel/storage/copy.yml b/bruno/b2b_daniel/storage/copy.yml new file mode 100644 index 0000000..8161fc0 --- /dev/null +++ b/bruno/b2b_daniel/storage/copy.yml @@ -0,0 +1,19 @@ +info: + name: copy + type: http + seq: 7 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/copy/folder1/test.txt?dest_path=/folder/a.txt + params: + - name: dest_path + value: /folder/a.txt + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/storage/move.yml b/bruno/b2b_daniel/storage/move.yml new file mode 100644 index 0000000..7fb51e5 --- /dev/null +++ b/bruno/b2b_daniel/storage/move.yml @@ -0,0 +1,19 @@ +info: + name: move + type: http + seq: 8 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/move/folder?dest_path=/folder1/test.txt + params: + - name: dest_path + value: /folder1/test.txt + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 From 1bab7f642f809f48e030361dc4f45b7ce30b0998 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 3 Apr 2026 11:44:15 +0200 Subject: [PATCH 08/25] typo --- app/service/storageService/storageService.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index 28b78ed..6fc9d41 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -52,6 +52,7 @@ func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error { return responseErrors.ErrFileDoesNotExist } + _, err = s.storageRepo.EntryInfo(dest_abs_path) if err == nil { return responseErrors.ErrNameTaken } else if os.IsNotExist(err) { @@ -66,6 +67,7 @@ func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error { return responseErrors.ErrFileDoesNotExist } + _, err = s.storageRepo.EntryInfo(dest_abs_path) if err == nil { return responseErrors.ErrNameTaken } else if os.IsNotExist(err) { From f6b321b602b4fc4fb8268ca6db52a532267997e7 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 3 Apr 2026 13:55:57 +0200 Subject: [PATCH 09/25] a few fixes for user teleportation --- app/delivery/middleware/auth.go | 14 ++++---------- app/service/emailService/email.go | 3 ++- app/utils/const_data/consts.go | 1 + app/utils/i18n/i18n.go | 3 ++- app/utils/localeExtractor/localeExtractor.go | 8 ++++++++ 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index c5a87cc..14fc0df 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -8,6 +8,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/authService" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "github.com/gofiber/fiber/v3" ) @@ -115,21 +116,14 @@ func AuthMiddleware() fiber.Handler { // RequireAdmin creates admin-only middleware func RequireAdmin() fiber.Handler { return func(c fiber.Ctx) error { - user := c.Locals("user") - if user == nil { + originalUserRole, ok := localeExtractor.GetOriginalUserRole(c) + if !ok { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "not authenticated", }) } - userSession, ok := user.(*model.UserSession) - if !ok { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "invalid user session", - }) - } - - if userSession.Role != model.RoleAdmin { + if originalUserRole != model.RoleAdmin { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "admin access required", }) diff --git a/app/service/emailService/email.go b/app/service/emailService/email.go index 6b1e082..29cc9bb 100644 --- a/app/service/emailService/email.go +++ b/app/service/emailService/email.go @@ -10,6 +10,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/templ/emails" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/view" ) @@ -133,6 +134,6 @@ func (s *EmailService) passwordResetEmailTemplate(name, resetURL string, langID // newUserAdminNotificationTemplate returns the HTML template for admin notification func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, baseURL string) string { buf := bytes.Buffer{} - emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: 2, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) + emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) return buf.String() } diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index b3790c8..05f23e8 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -4,6 +4,7 @@ package constdata const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const SHOP_ID = 1 const SHOP_DEFAULT_LANGUAGE = 1 +const ADMIN_NOTIFICATION_LANGUAGE = 2 // CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1 const CATEGORY_TREE_ROOT_ID = 2 diff --git a/app/utils/i18n/i18n.go b/app/utils/i18n/i18n.go index 3dfec66..5f3b6a0 100644 --- a/app/utils/i18n/i18n.go +++ b/app/utils/i18n/i18n.go @@ -8,6 +8,7 @@ import ( "sync" "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "github.com/gofiber/fiber/v3" ) @@ -177,7 +178,7 @@ func (s *TranslationsStore) ReloadTranslations(translations []model.Translation) // T_ is meant to be used to translate error messages and other system communicates. func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string { - if langID, ok := c.Locals("langID").(uint); ok { + if langID, ok := localeExtractor.GetLangID(c); ok { parts := strings.Split(string(key), ".") if len(parts) >= 2 { diff --git a/app/utils/localeExtractor/localeExtractor.go b/app/utils/localeExtractor/localeExtractor.go index 735397c..4b641d9 100644 --- a/app/utils/localeExtractor/localeExtractor.go +++ b/app/utils/localeExtractor/localeExtractor.go @@ -21,3 +21,11 @@ func GetUserID(c fiber.Ctx) (uint, bool) { } return user_locale.User.ID, true } + +func GetOriginalUserRole(c fiber.Ctx) (model.CustomerRole, bool) { + user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) + if !ok || user_locale.OriginalUser == nil { + return "", false + } + return user_locale.OriginalUser.Role, true +} From 7264a11ba67b8a8911da5a0c0c3fbaa874e9fee4 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 3 Apr 2026 14:58:50 +0200 Subject: [PATCH 10/25] sanitize and save URL slugs --- app/model/productDescription.go | 2 +- .../productDescriptionRepo.go | 10 +-- .../productTranslationService.go | 25 ++++++- .../sanitizeURLSlug.go | 69 +++++++++++++++++++ app/utils/const_data/consts.go | 25 +++++++ app/utils/responseErrors/responseErrors.go | 4 ++ bruno/b2b-daniel/save-product-description.yml | 39 +++++++++++ .../translate-product-description.yml | 28 ++++++++ 8 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 app/service/productTranslationService/sanitizeURLSlug.go create mode 100644 bruno/b2b-daniel/save-product-description.yml create mode 100644 bruno/b2b-daniel/translate-product-description.yml 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 ae26f6b..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 + @@ -74,10 +74,10 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid // If it doesn't exist, returns an error. func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error { - record := model.ProductDescription{ - ProductID: productID, - ShopID: constdata.SHOP_ID, - LangID: productid_lang, + record := dbmodel.PsProductLang{ + IDProduct: int32(productID), + IDShop: int32(constdata.SHOP_ID), + IDLang: int32(productid_lang), } err := db.Get(). diff --git a/app/service/productTranslationService/productTranslationService.go b/app/service/productTranslationService/productTranslationService.go index 1b0a747..0ad8cd7 100644 --- a/app/service/productTranslationService/productTranslationService.go +++ b/app/service/productTranslationService/productTranslationService.go @@ -89,13 +89,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++ { @@ -136,20 +147,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 b3790c8..cbd5657 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -12,3 +12,28 @@ const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" 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/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index c4247ea..d20c173 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -42,6 +42,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") @@ -136,6 +137,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): @@ -195,6 +198,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/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
  • w rehabilitacji
  • \n
  • podczas masaży (tradycyjnych, sportowych)
  • \n
  • w gimnastyce korekcyjnej (w tym zwłaszcza dzieci)
  • \n
  • w łagodzeniu urazów poszczególnych części ciała
  • \n
  • dla podparcia: kolan, kostek, głowy pacjenta
  • \n
  • w ćwiczeniach rozwijających motorykę dzieci
  • \n
  • w salonach kosmetycznych
  • \n
  • w salach zabaw dla dzieci
  • \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
  • materiał zgodny z rozporządzeniem REACH, posiada atest Certyfikat STANDARD 100 by OEKO-TEX ®
  • \n
  • nie zawiera ftalanów
  • \n
  • ognioodporny
  • \n
  • odporny na płyny fizjologiczne (krew, mocz, pot) oraz na alkohol
  • \n
  • odporny na UV, przez co może być także używany na zewnątrz
  • \n
  • odporny na zadrapania
  • \n
  • olejoodporny
  • \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
  • posiada ATEST HIGIENICZNY wydany przez Instytut Medycyny Morskiej i Tropikalnej w Gdyni
  • \n
  • posiada atest Certyfikat STANDARD 100 by OEKO-TEX ® – klasa produktów I wydany przez Instytut Włókiennictwa w Łodzi
  • \n
  • produkowana z surowców o podwyższonej jakości, nie powodujących zubożenia warstwy ozonowej 
  • \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
    \r\n
  • Trwałe przebarwienia powstałe wskutek kontaktu z odzieżą zawierającą aktywne, migrujące barwniki (np. jeans, zamsz itp.)
  • \r\n
  • Ślady z długopisu, tuszu, mazaków itp. zawierające aktywne barwniki
  • \r\n
  • Uszkodzenia wywołane przez wysoką temperaturę, płyny żrące, ogień
  • \r\n
  • Uszkodzenia mechaniczne spowodowane przez zwierzęta domowe i innych użytkowników
  • \r\n
  • Wady powstałe wskutek niewłaściwej konserwacji
  • \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 From 76ca2a2eed98f89f080e70165e755b6c64ca44b1 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Fri, 3 Apr 2026 15:58:35 +0200 Subject: [PATCH 11/25] chore: adapt code to new teleport feature --- app/delivery/web/api/restricted/customer.go | 92 +++++++++---------- app/model/customer.go | 9 ++ app/model/role.go | 2 +- app/repos/customerRepo/customerRepo.go | 2 +- bruno/api_v1/customer/Customer (me).yml | 6 +- .../20260302163123_create_tables_data.sql | 9 +- 6 files changed, 65 insertions(+), 55 deletions(-) diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index 039efcb..da8a7e5 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -4,9 +4,9 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" - "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/customerService" "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" @@ -28,37 +28,34 @@ func CustomerHandlerRoutes(r fiber.Router) fiber.Router { handler := NewCustomerHandler() r.Get("", handler.customerData) - r.Get("/list", handler.listCustomers) + // r.Get("/list", handler.listCustomers) return r } func (h *customerHandler) customerData(fc fiber.Ctx) error { var customerId uint + + user, ok := localeExtractor.GetCustomer(fc) + if !ok || user == nil { + return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) + } + customerIdStr := fc.Query("id") if customerIdStr != "" { - user, ok := fc.Locals("user").(*model.UserSession) - if !ok { - return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) - } id, err := strconv.ParseUint(customerIdStr, 10, 64) if err != nil { return fiber.ErrBadRequest } - if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) { + if user.ID != uint(id) && !user.HasPermission(perms.UserReadAny) { return fc.Status(fiber.StatusForbidden). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) } customerId = uint(id) } else { - id, ok := fc.Locals("userID").(uint) - if !ok { - return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) - } - customerId = id + customerId = user.ID } customer, err := h.service.GetById(customerId) @@ -70,40 +67,41 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error { return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) } -func (h *customerHandler) listCustomers(fc fiber.Ctx) error { - var customerId uint - customerIdStr := fc.Query("id") - if customerIdStr != "" { - user, ok := fc.Locals("user").(*model.UserSession) - if !ok { - return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) - } - id, err := strconv.ParseUint(customerIdStr, 10, 64) - if err != nil { - return fiber.ErrBadRequest - } +// func (h *customerHandler) listCustomers(fc fiber.Ctx) error { +// var customerId uint +// customerIdStr := fc.Query("id") +// if customerIdStr != "" { - if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) { - return fc.Status(fiber.StatusForbidden). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) - } +// user, ok := localeExtractor.GetCustomer(fc) +// if !ok || user == nil { +// return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). +// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) +// } +// id, err := strconv.ParseUint(customerIdStr, 10, 64) +// if err != nil { +// return fiber.ErrBadRequest +// } - customerId = uint(id) - } else { - id, ok := fc.Locals("userID").(uint) - if !ok { - return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) - } - customerId = id - } +// if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) { +// return fc.Status(fiber.StatusForbidden). +// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) +// } - customer, err := h.service.GetById(customerId) - if err != nil { - return fc.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) - } +// customerId = uint(id) +// } else { +// id, ok := fc.Locals("userID").(uint) +// if !ok { +// return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). +// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) +// } +// customerId = id +// } - return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) -} +// customer, err := h.service.GetById(customerId) +// if err != nil { +// return fc.Status(responseErrors.GetErrorStatus(err)). +// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) +// } + +// return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) +// } diff --git a/app/model/customer.go b/app/model/customer.go index d036e5b..ccf2fe5 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -34,6 +34,15 @@ type Customer struct { DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } +func (u *Customer) HasPermission(permission perms.Permission) bool { + for _, p := range u.Role.Permissions { + if p.Name == permission { + return true + } + } + return false +} + // AuthProvider represents the authentication provider type AuthProvider string diff --git a/app/model/role.go b/app/model/role.go index 3c663b5..2ea0789 100644 --- a/app/model/role.go +++ b/app/model/role.go @@ -3,7 +3,7 @@ package model type Role struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:64" json:"name"` - Permissions []Permission `gorm:"many2many:b2b_role_permissions;" json:"-"` + Permissions []Permission `gorm:"many2many:b2b_role_permissions;" json:"permissions"` } func (Role) TableName() string { diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index 058d5fd..b46890f 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -19,7 +19,7 @@ func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) { var customer model.Customer err := db.DB. - Preload("Role"). + Preload("Role.Permissions"). First(&customer, id). Error diff --git a/bruno/api_v1/customer/Customer (me).yml b/bruno/api_v1/customer/Customer (me).yml index 253bead..891919e 100644 --- a/bruno/api_v1/customer/Customer (me).yml +++ b/bruno/api_v1/customer/Customer (me).yml @@ -5,11 +5,7 @@ info: http: method: GET - url: "{{bas_url}}/restricted/customer?id=1" - params: - - name: id - value: "1" - type: query + url: "{{bas_url}}/restricted/customer" auth: inherit settings: diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql index ce62f1b..dafebf7 100644 --- a/i18n/migrations/20260302163123_create_tables_data.sql +++ b/i18n/migrations/20260302163123_create_tables_data.sql @@ -35,5 +35,12 @@ INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('2', 'user.write.any'); INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('3', 'user.delete.any'); INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('4', 'currency.write'); - +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '1'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '2'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '3'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '4'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '1'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '2'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '3'); +INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '4'); -- +goose Down \ No newline at end of file From 813d1f48791d30f2a0e61f78dee043fdae9146f8 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 7 Apr 2026 09:28:39 +0200 Subject: [PATCH 12/25] feat: add customer list, modify pagination utils --- app/delivery/web/api/restricted/customer.go | 61 ++++++++----------- app/model/customer.go | 2 +- app/repos/customerRepo/customerRepo.go | 12 ++++ app/service/authService/auth.go | 1 - app/service/authService/google_oauth.go | 1 - .../customerService/customerService.go | 6 ++ app/utils/query/find/find.go | 18 +----- bruno/api_v1/customer/Customer list.yml | 15 +++++ 8 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 bruno/api_v1/customer/Customer list.yml diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index da8a7e5..6b3ea60 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -4,10 +4,12 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" + "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/customerService" "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" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "github.com/gofiber/fiber/v3" @@ -28,7 +30,7 @@ func CustomerHandlerRoutes(r fiber.Router) fiber.Router { handler := NewCustomerHandler() r.Get("", handler.customerData) - // r.Get("/list", handler.listCustomers) + r.Get("/list", handler.listCustomers) return r } @@ -67,41 +69,28 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error { return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) } -// func (h *customerHandler) listCustomers(fc fiber.Ctx) error { -// var customerId uint -// customerIdStr := fc.Query("id") -// if customerIdStr != "" { +func (h *customerHandler) listCustomers(fc fiber.Ctx) error { + p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListProducts) + if err != nil { + return fc.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) + } -// user, ok := localeExtractor.GetCustomer(fc) -// if !ok || user == nil { -// return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). -// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) -// } -// id, err := strconv.ParseUint(customerIdStr, 10, 64) -// if err != nil { -// return fiber.ErrBadRequest -// } + user, ok := localeExtractor.GetCustomer(fc) + if !ok || user == nil { + return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) + } + if !user.HasPermission(perms.UserReadAny) { + return fc.Status(fiber.StatusForbidden). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) + } -// if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) { -// return fc.Status(fiber.StatusForbidden). -// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) -// } + customer, err := h.service.Find(user.LangID, p, filt) + if err != nil { + return fc.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) + } -// customerId = uint(id) -// } else { -// id, ok := fc.Locals("userID").(uint) -// if !ok { -// return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). -// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) -// } -// customerId = id -// } - -// customer, err := h.service.GetById(customerId) -// if err != nil { -// return fc.Status(responseErrors.GetErrorStatus(err)). -// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) -// } - -// return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) -// } + return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) +} diff --git a/app/model/customer.go b/app/model/customer.go index ccf2fe5..9421862 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -15,7 +15,7 @@ type Customer struct { FirstName string `gorm:"size:100" json:"first_name"` LastName string `gorm:"size:100" json:"last_name"` RoleID uint `gorm:"column:role_id;not null;default:1" json:"-"` - Role Role `gorm:"foreignKey:RoleID" json:"role"` + Role *Role `gorm:"foreignKey:RoleID" json:"role,omitempty"` Provider AuthProvider `gorm:"type:varchar(20);default:'local'" json:"provider"` ProviderID string `gorm:"size:255" json:"provider_id,omitempty"` // ID from OAuth provider AvatarURL string `gorm:"size:500" json:"avatar_url,omitempty"` diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index b46890f..7a979bb 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -3,10 +3,13 @@ package customerRepo import ( "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/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" ) type UICustomerRepo interface { Get(id uint) (*model.Customer, error) + Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) } type CustomerRepo struct{} @@ -25,3 +28,12 @@ func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) { return &customer, err } + +func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) { + found, err := find.Paginate[model.Customer](langId, p, db.DB. + Model(&model.Customer{}). + Scopes(filt.All()...), + ) + + return &found, err +} diff --git a/app/service/authService/auth.go b/app/service/authService/auth.go index dc4a35b..ba1fa67 100644 --- a/app/service/authService/auth.go +++ b/app/service/authService/auth.go @@ -153,7 +153,6 @@ func (s *AuthService) Register(req *model.RegisterRequest) error { Password: string(hashedPassword), FirstName: req.FirstName, LastName: req.LastName, - Role: model.Role{}, Provider: model.ProviderLocal, IsActive: false, EmailVerified: false, diff --git a/app/service/authService/google_oauth.go b/app/service/authService/google_oauth.go index c26da16..d8c1820 100644 --- a/app/service/authService/google_oauth.go +++ b/app/service/authService/google_oauth.go @@ -150,7 +150,6 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model. Provider: model.ProviderGoogle, ProviderID: info.ID, AvatarURL: info.Picture, - Role: model.Role{}, IsActive: true, EmailVerified: true, LangID: 2, // default is english diff --git a/app/service/customerService/customerService.go b/app/service/customerService/customerService.go index 7af553c..dbaeb24 100644 --- a/app/service/customerService/customerService.go +++ b/app/service/customerService/customerService.go @@ -3,6 +3,8 @@ package customerService import ( "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/repos/customerRepo" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" ) type CustomerService struct { @@ -18,3 +20,7 @@ func New() *CustomerService { func (s *CustomerService) GetById(id uint) (*model.Customer, error) { return s.repo.Get(id) } + +func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) { + return s.repo.Find(langId, p, filt) +} diff --git a/app/utils/query/find/find.go b/app/utils/query/find/find.go index 7d810ec..57ef813 100644 --- a/app/utils/query/find/find.go +++ b/app/utils/query/find/find.go @@ -1,7 +1,6 @@ package find import ( - "errors" "reflect" "strings" @@ -28,18 +27,13 @@ type Found[T any] struct { Spec map[string]interface{} `json:"spec,omitempty"` } -// Wraps given query adding limit, offset clauses and SQL_CALC_FOUND_ROWS to it -// and running SELECT FOUND_ROWS() afterwards to fetch the total number -// (ignoring LIMIT) of results. The final results are wrapped into the -// [find.Found] type. func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error) { var items []T - var count uint64 + var count int64 - // stmt.Debug() + stmt.Count(&count) err := stmt. - Clauses(SqlCalcFound()). Offset(paging.Offset()). Limit(paging.Limit()). Find(&items). @@ -48,14 +42,6 @@ func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error return Found[T]{}, err } - countInterface, ok := stmt.Get(FOUND_ROWS_CTX_KEY) - if !ok { - return Found[T]{}, errors.New(FOUND_ROWS_CTX_KEY + " value was not found in the gorm db context") - } - if count, ok = countInterface.(uint64); !ok { - return Found[T]{}, errors.New("failed to cast value under " + FOUND_ROWS_CTX_KEY + " to uint64") - } - columnsSpec := GetColumnsSpec[T](langID) return Found[T]{ diff --git a/bruno/api_v1/customer/Customer list.yml b/bruno/api_v1/customer/Customer list.yml new file mode 100644 index 0000000..0d5bc26 --- /dev/null +++ b/bruno/api_v1/customer/Customer list.yml @@ -0,0 +1,15 @@ +info: + name: Customer list + type: http + seq: 3 + +http: + method: GET + url: "{{bas_url}}/restricted/customer/list" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 From 918729736785f544c847eed2a8a535fb4c72262d Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 7 Apr 2026 10:32:30 +0200 Subject: [PATCH 13/25] refactor: move lists to their representative repos --- app/delivery/web/api/restricted/customer.go | 9 +- app/delivery/web/api/restricted/list.go | 99 -------------- app/delivery/web/api/restricted/product.go | 34 +++++ app/delivery/web/init.go | 4 - app/model/customer.go | 1 - app/repos/customerRepo/customerRepo.go | 54 +++++++- app/repos/listRepo/listRepo.go | 121 ------------------ app/repos/productsRepo/productsRepo.go | 66 ++++++++++ .../customerService/customerService.go | 2 +- app/service/listService/listService.go | 26 ---- app/service/productService/productService.go | 7 + bruno/api_v1/product/Products List.yml | 5 +- repository/currencyRepo/currencyRepo.go | 53 -------- 13 files changed, 167 insertions(+), 314 deletions(-) delete mode 100644 app/delivery/web/api/restricted/list.go delete mode 100644 app/repos/listRepo/listRepo.go delete mode 100644 app/service/listService/listService.go delete mode 100644 repository/currencyRepo/currencyRepo.go diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index 6b3ea60..6f953b3 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -70,7 +70,7 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error { } func (h *customerHandler) listCustomers(fc fiber.Ctx) error { - p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListProducts) + p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers) if err != nil { return fc.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) @@ -94,3 +94,10 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error { return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK))) } + +var columnMappingListUsers map[string]string = map[string]string{ + "user_id": "users.id", + "email": "users.email", + "first_name": "users.first_name", + "second_name": "users.second_name", +} diff --git a/app/delivery/web/api/restricted/list.go b/app/delivery/web/api/restricted/list.go deleted file mode 100644 index c6b3116..0000000 --- a/app/delivery/web/api/restricted/list.go +++ /dev/null @@ -1,99 +0,0 @@ -package restricted - -import ( - "git.ma-al.com/goc_daniel/b2b/app/config" - "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" - "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" - "github.com/gofiber/fiber/v3" -) - -// ListHandler handles endpoints that list various things (e.g. products or users) -type ListHandler struct { - listService *listService.ListService - config *config.Config -} - -// NewListHandler creates a new ListHandler instance -func NewListHandler() *ListHandler { - listService := listService.New() - return &ListHandler{ - listService: listService, - config: config.Get(), - } -} - -func ListHandlerRoutes(r fiber.Router) fiber.Router { - handler := NewListHandler() - - r.Get("/list-products", handler.ListProducts) - r.Get("/list-users", handler.ListUsers) - - return r -} - -func (h *ListHandler) ListProducts(c fiber.Ctx) error { - paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - 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))) - } - - 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(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) -} - -var columnMappingListProducts map[string]string = map[string]string{ - "product_id": "ps.id_product", - "name": "pl.name", - "reference": "p.reference", - "category_name": "cl.name", - "category_id": "cp.id_category", - "quantity": "sa.quantity", -} - -func (h *ListHandler) ListUsers(c fiber.Ctx) error { - paging, filters, err := query_params.ParseFilters[model.Customer](c, columnMappingListUsers) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - 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))) - } - - 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(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) -} - -var columnMappingListUsers map[string]string = map[string]string{ - "user_id": "users.id", - "email": "users.email", - "first_name": "users.first_name", - "second_name": "users.second_name", - "role": "users.role", -} diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index d4fa8ce..ddd8677 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -4,10 +4,12 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/productService" "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" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "github.com/gofiber/fiber/v3" @@ -31,6 +33,7 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router { handler := NewProductsHandler() r.Get("/:id/:country_id/:quantity", handler.GetProductJson) + r.Get("/list", handler.ListProducts) return r } @@ -73,3 +76,34 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error { return c.JSON(response.Make(&productJson, 1, i18n.T_(c, response.Message_OK))) } + +func (h *ProductsHandler) ListProducts(c fiber.Ctx) error { + paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + 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))) + } + + list, err := h.productService.Find(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(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) +} + +var columnMappingListProducts map[string]string = map[string]string{ + "product_id": "ps.id_product", + "name": "pl.name", + "reference": "p.reference", + "category_name": "cl.name", + "category_id": "cp.id_category", + "quantity": "sa.quantity", +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index ad75e75..9d673f5 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -97,10 +97,6 @@ func (s *Server) Setup() error { productTranslation := s.restricted.Group("/product-translation") restricted.ProductTranslationHandlerRoutes(productTranslation) - // lists of things routes (restricted) - list := s.restricted.Group("/list") - restricted.ListHandlerRoutes(list) - product := s.restricted.Group("/product") restricted.ProductsHandlerRoutes(product) diff --git a/app/model/customer.go b/app/model/customer.go index 9421862..77102ad 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -177,5 +177,4 @@ type UserInList struct { Email string `gorm:"column:email" json:"email"` FirstName string `gorm:"column:first_name" json:"first_name"` LastName string `gorm:"column:last_name" json:"last_name"` - Role string `gorm:"column:role" json:"role"` } diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index 7a979bb..9f325c2 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -9,7 +9,7 @@ import ( type UICustomerRepo interface { Get(id uint) (*model.Customer, error) - Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) + Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) } type CustomerRepo struct{} @@ -29,11 +29,57 @@ func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) { return &customer, err } -func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) { - found, err := find.Paginate[model.Customer](langId, p, db.DB. - Model(&model.Customer{}). +func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) { + found, err := find.Paginate[model.UserInList](langId, p, db.DB. + Table("b2b_customers AS users"). + Select(` + users.id AS id, + users.email AS email, + users.first_name AS first_name, + users.last_name AS last_name + `). Scopes(filt.All()...), ) return &found, err } + +// func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) { +// var list []model.UserInList +// var total int64 + +// query := db.Get(). +// Table("b2b_customers AS users"). +// Select(` +// users.id AS id, +// users.email AS email, +// users.first_name AS first_name, +// users.last_name AS last_name, +// users.role AS role +// `) + +// // Apply all filters +// if filt != nil { +// filt.ApplyAll(query) +// } + +// // run counter first as query is without limit and offset +// err := query.Count(&total).Error +// if err != nil { +// return find.Found[model.UserInList]{}, err +// } + +// err = query. +// Order("users.id DESC"). +// Limit(p.Limit()). +// Offset(p.Offset()). +// Find(&list).Error +// if err != nil { +// return find.Found[model.UserInList]{}, err +// } + +// return find.Found[model.UserInList]{ +// Items: list, +// Count: uint(total), +// }, nil +// } diff --git a/app/repos/listRepo/listRepo.go b/app/repos/listRepo/listRepo.go deleted file mode 100644 index d31ebda..0000000 --- a/app/repos/listRepo/listRepo.go +++ /dev/null @@ -1,121 +0,0 @@ -package listRepo - -import ( - "git.ma-al.com/goc_daniel/b2b/app/config" - "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/dbmodel" - "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" - "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" - "git.ma-al.com/goc_marek/gormcol" - "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" -) - -type UIListRepo interface { - ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) - ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) -} - -type ListRepo struct{} - -func New() UIListRepo { - return &ListRepo{} -} - -func (repo *ListRepo) ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { - var list []model.ProductInList - var total int64 - - query := db.Get(). - Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). - Select(` - ps.id_product AS product_id, - pl.name AS name, - pl.link_rewrite AS link_rewrite, - CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, - cl.name AS category_name, - p.reference AS reference, - COALESCE(v.variants_number, 0) AS variants_number, - sa.quantity AS quantity - `, config.Get().Image.ImagePrefix). - Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product"). - Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang). - Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1"). - Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang). - Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). - Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). - Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). - Where("ps.active = ?", 1). - Group("ps.id_product"). - Clauses(exclause.With{CTEs: []exclause.CTE{ - { - Name: "variants", - Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")}, - }, - }}) - - // Apply all filters - if filt != nil { - filt.ApplyAll(query) - } - - // run counter first as query is without limit and offset - err := query.Count(&total).Error - if err != nil { - return find.Found[model.ProductInList]{}, err - } - - err = query. - Order("ps.id_product DESC"). - Limit(p.Limit()). - Offset(p.Offset()). - Find(&list).Error - if err != nil { - return find.Found[model.ProductInList]{}, err - } - - return find.Found[model.ProductInList]{ - Items: list, - Count: uint(total), - }, nil -} - -func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) { - var list []model.UserInList - var total int64 - - query := db.Get(). - Table("b2b_customers AS users"). - Select(` - users.id AS id, - users.email AS email, - users.first_name AS first_name, - users.last_name AS last_name, - users.role AS role - `) - - // Apply all filters - if filt != nil { - filt.ApplyAll(query) - } - - // run counter first as query is without limit and offset - err := query.Count(&total).Error - if err != nil { - return find.Found[model.UserInList]{}, err - } - - err = query. - Order("users.id DESC"). - Limit(p.Limit()). - Offset(p.Offset()). - Find(&list).Error - if err != nil { - return find.Found[model.UserInList]{}, err - } - - return find.Found[model.UserInList]{ - Items: list, - Count: uint(total), - }, nil -} diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 7c6c08f..341b348 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -4,11 +4,19 @@ import ( "encoding/json" "fmt" + "git.ma-al.com/goc_daniel/b2b/app/config" "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/dbmodel" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" + "git.ma-al.com/goc_marek/gormcol" + "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" ) type UIProductsRepo interface { GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) + Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) } type ProductsRepo struct{} @@ -37,3 +45,61 @@ func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_custo raw := json.RawMessage(productStr) return &raw, nil } + +func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { + var list []model.ProductInList + var total int64 + + query := db.Get(). + Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). + Select(` + ps.id_product AS product_id, + pl.name AS name, + pl.link_rewrite AS link_rewrite, + CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, + cl.name AS category_name, + p.reference AS reference, + COALESCE(v.variants_number, 0) AS variants_number, + sa.quantity AS quantity + `, config.Get().Image.ImagePrefix). + Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product"). + Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang). + Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1"). + Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang). + Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). + Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). + Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). + Where("ps.active = ?", 1). + Group("ps.id_product"). + Clauses(exclause.With{CTEs: []exclause.CTE{ + { + Name: "variants", + Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")}, + }, + }}). + Order("ps.id_product DESC") + + // Apply all filters + if filt != nil { + filt.ApplyAll(query) + } + + // run counter first as query is without limit and offset + err := query.Count(&total).Error + if err != nil { + return find.Found[model.ProductInList]{}, err + } + + err = query. + Limit(p.Limit()). + Offset(p.Offset()). + Find(&list).Error + if err != nil { + return find.Found[model.ProductInList]{}, err + } + + return find.Found[model.ProductInList]{ + Items: list, + Count: uint(total), + }, nil +} diff --git a/app/service/customerService/customerService.go b/app/service/customerService/customerService.go index dbaeb24..f9f2f4a 100644 --- a/app/service/customerService/customerService.go +++ b/app/service/customerService/customerService.go @@ -21,6 +21,6 @@ func (s *CustomerService) GetById(id uint) (*model.Customer, error) { return s.repo.Get(id) } -func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Customer], error) { +func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) { return s.repo.Find(langId, p, filt) } diff --git a/app/service/listService/listService.go b/app/service/listService/listService.go deleted file mode 100644 index d3d168b..0000000 --- a/app/service/listService/listService.go +++ /dev/null @@ -1,26 +0,0 @@ -package listService - -import ( - "git.ma-al.com/goc_daniel/b2b/app/model" - "git.ma-al.com/goc_daniel/b2b/app/repos/listRepo" - "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" - "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" -) - -type ListService struct { - listRepo listRepo.UIListRepo -} - -func New() *ListService { - return &ListService{ - listRepo: listRepo.New(), - } -} - -func (s *ListService) ListProducts(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { - return s.listRepo.ListProducts(id_lang, p, filters) -} - -func (s *ListService) ListUsers(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.UserInList], error) { - return s.listRepo.ListUsers(id_lang, p, filters) -} diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index 66245f1..1a1620e 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -3,8 +3,11 @@ package productService import ( "encoding/json" + "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" ) type ProductService struct { @@ -25,3 +28,7 @@ func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_ return products, nil } + +func (s *ProductService) Find(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { + return s.productsRepo.Find(id_lang, p, filters) +} diff --git a/bruno/api_v1/product/Products List.yml b/bruno/api_v1/product/Products List.yml index cc07f08..6763495 100644 --- a/bruno/api_v1/product/Products List.yml +++ b/bruno/api_v1/product/Products List.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: "{{bas_url}}/restricted/list/list-products?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62" + url: "{{bas_url}}/restricted/product/list?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62" params: - name: p value: "1" @@ -25,9 +25,6 @@ http: body: type: json data: "" - auth: - type: bearer - token: "{{token}}" settings: encodeUrl: true diff --git a/repository/currencyRepo/currencyRepo.go b/repository/currencyRepo/currencyRepo.go deleted file mode 100644 index 97b1b5e..0000000 --- a/repository/currencyRepo/currencyRepo.go +++ /dev/null @@ -1,53 +0,0 @@ -package currencyRepo - -import ( - "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/query/filters" - "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" -) - -type UICurrencyRepo interface { - CreateConversionRate(currencyRate *model.CurrencyRate) error - Get(id uint) (*model.Currency, error) -} - -type CurrencyRepo struct{} - -func New() UICurrencyRepo { - return &CurrencyRepo{} -} - -func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error { - return db.DB.Debug().Create(currencyRate).Error -} - -func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) { - var currency model.Currency - - err := db.DB.Table("b2b_currencies c"). - Select("c.*, r.conversion_rate"). - Joins(` - LEFT JOIN b2b_currency_rates r - ON r.b2b_id_currency = c.id - AND r.created_at = ( - SELECT MAX(created_at) - FROM b2b_currency_rates - WHERE b2b_id_currency = c.id - ) - `). - Where("c.id = ?", id). - Scan(¤cy).Error - - return ¤cy, err -} - -func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) { - - found, err := find.Paginate[model.Currency](langId, p, db.DB. - Model(&model.Currency{}). - Scopes(filt.All()...), - ) - - return &found, err -} From 2e645f3368a03fcdfeb4a4c5547041ab611bcce5 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 7 Apr 2026 13:36:43 +0200 Subject: [PATCH 14/25] fix: google provider auth --- app/delivery/web/api/restricted/customer.go | 8 +- app/repos/customerRepo/customerRepo.go | 81 +++++++++++++++++++++ app/repos/rolesRepo/rolesRepo.go | 22 ++++++ app/service/authService/auth.go | 18 +++-- app/service/authService/google_oauth.go | 30 ++++++-- 5 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 app/repos/rolesRepo/rolesRepo.go diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index 6f953b3..7c04b7e 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -96,8 +96,8 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error { } var columnMappingListUsers map[string]string = map[string]string{ - "user_id": "users.id", - "email": "users.email", - "first_name": "users.first_name", - "second_name": "users.second_name", + "user_id": "users.id", + "email": "users.email", + "first_name": "users.first_name", + "last_name": "users.last_name", } diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index 9f325c2..668785f 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -9,7 +9,11 @@ import ( type UICustomerRepo interface { Get(id uint) (*model.Customer, error) + GetByEmail(email string) (*model.Customer, error) + GetByExternalProviderId(provider model.AuthProvider, id string) (*model.Customer, error) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) + Save(customer *model.Customer) error + Create(customer *model.Customer) error } type CustomerRepo struct{} @@ -29,6 +33,30 @@ func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) { return &customer, err } +func (repo *CustomerRepo) GetByEmail(email string) (*model.Customer, error) { + var customer model.Customer + + err := db.DB. + Preload("Role.Permissions"). + Where("email = ?", email). + First(&customer). + Error + + return &customer, err +} + +func (repo *CustomerRepo) GetByExternalProviderId(provider model.AuthProvider, id string) (*model.Customer, error) { + var customer model.Customer + + err := db.DB. + Preload("Role.Permissions"). + Where("provider = ? AND provider_id = ?", provider, id). + First(&customer). + Error + + return &customer, err +} + func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) { found, err := find.Paginate[model.UserInList](langId, p, db.DB. Table("b2b_customers AS users"). @@ -44,6 +72,59 @@ func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.Filters return &found, err } +func (repo *CustomerRepo) Save(customer *model.Customer) error { + return db.DB.Save(customer).Error +} + +func (repo *CustomerRepo) Create(customer *model.Customer) error { + return db.DB.Create(customer).Error +} + +// func (repo *CustomerRepo) Search( +// customerId uint, +// partnerCode string, +// p find.Paging, +// filt *filters.FiltersList, +// search string, +// ) (found find.Found[model.UserInList], err error) { +// words := strings.Fields(search) +// if len(words) > 5 { +// words = words[:5] +// } + +// query := ctx.DB(). +// Model(&model.Customer{}). +// Select("customer.id AS id, customer.first_name as first_name, customer.last_name as last_name, customer.phone_number AS phone_number, customer.email AS email, count(distinct investment_plan_contract.id) as iiplan_purchases, count(distinct `order`.id) as single_purchases, entity.name as entity_name"). +// Where("customer.id <> ?", customerId). +// Where("(customer.id IN (SELECT id FROM customer WHERE partner_code IN (WITH RECURSIVE partners AS (SELECT code AS dst FROM partner WHERE code = ? UNION SELECT code FROM partner JOIN partners ON partners.dst = partner.superior_code) SELECT dst FROM partners)) OR customer.recommender_code = ?)", partnerCode, partnerCode). +// Scopes(view.CustomerListQuery()) + +// var conditions []string +// var args []interface{} +// for _, word := range words { + +// conditions = append(conditions, ` +// (LOWER(first_name) LIKE ? OR +// LOWER(last_name) LIKE ? OR +// phone_number LIKE ? OR +// LOWER(email) LIKE ?) +// `) + +// for i := 0; i < 4; i++ { +// args = append(args, "%"+strings.ToLower(word)+"%") +// } +// } + +// finalQuery := strings.Join(conditions, " AND ") + +// query = query.Where(finalQuery, args...). +// Scopes(filt.All()...) + +// found, err = find.Paginate[V](ctx, p, query) + +// return found, errs.Recorded(span, err) +// } + // func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) { // var list []model.UserInList // var total int64 diff --git a/app/repos/rolesRepo/rolesRepo.go b/app/repos/rolesRepo/rolesRepo.go new file mode 100644 index 0000000..e87e10f --- /dev/null +++ b/app/repos/rolesRepo/rolesRepo.go @@ -0,0 +1,22 @@ +package roleRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" +) + +type UIRolesRepo interface { + Get(id uint) (*model.Role, error) +} + +type RolesRepo struct{} + +func New() UIRolesRepo { + return &RolesRepo{} +} + +func (r *RolesRepo) Get(id uint) (*model.Role, error) { + var role model.Role + err := db.DB.First(&role, id).Error + return &role, err +} diff --git a/app/service/authService/auth.go b/app/service/authService/auth.go index ba1fa67..ebc9e32 100644 --- a/app/service/authService/auth.go +++ b/app/service/authService/auth.go @@ -11,6 +11,8 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/config" "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/repos/customerRepo" + roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo" "git.ma-al.com/goc_daniel/b2b/app/service/emailService" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" @@ -35,17 +37,21 @@ type JWTClaims struct { // AuthService handles authentication operations type AuthService struct { - db *gorm.DB - config *config.AuthConfig - email *emailService.EmailService + db *gorm.DB + config *config.AuthConfig + email *emailService.EmailService + customerRepo customerRepo.UICustomerRepo + roleRepo roleRepo.UIRolesRepo } // NewAuthService creates a new AuthService instance func NewAuthService() *AuthService { svc := &AuthService{ - db: db.Get(), - config: &config.Get().Auth, - email: emailService.NewEmailService(), + db: db.Get(), + config: &config.Get().Auth, + email: emailService.NewEmailService(), + customerRepo: customerRepo.New(), + roleRepo: roleRepo.New(), } // Auto-migrate the refresh_tokens table if svc.db != nil { diff --git a/app/service/authService/google_oauth.go b/app/service/authService/google_oauth.go index d8c1820..d517c6d 100644 --- a/app/service/authService/google_oauth.go +++ b/app/service/authService/google_oauth.go @@ -108,26 +108,32 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st // findOrCreateGoogleUser finds an existing user by Google provider ID or email, // or creates a new one. func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.Customer, error) { - var user model.Customer + var user *model.Customer // Try to find by provider + provider_id - err := s.db.Where("provider = ? AND provider_id = ?", model.ProviderGoogle, info.ID).First(&user).Error + user, err := s.customerRepo.GetByExternalProviderId(model.ProviderGoogle, info.ID) if err == nil { // Update avatar in case it changed user.AvatarURL = info.Picture - s.db.Save(&user) - return &user, nil + err = s.customerRepo.Save(user) + if err != nil { + return nil, err + } + return user, nil } // Try to find by email (user may have registered locally before) - err = s.db.Where("email = ?", info.Email).First(&user).Error + user, err = s.customerRepo.GetByEmail(info.Email) if err == nil { // Link Google provider to existing account user.Provider = model.ProviderGoogle user.ProviderID = info.ID user.AvatarURL = info.Picture user.IsActive = true - s.db.Save(&user) + err = s.customerRepo.Save(user) + if err != nil { + return nil, err + } // If email has not been verified yet, send email to admin. if !user.EmailVerified { @@ -139,7 +145,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model. } user.EmailVerified = true - return &user, nil + return user, nil } // Create new user @@ -148,6 +154,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model. FirstName: info.GivenName, LastName: info.FamilyName, Provider: model.ProviderGoogle, + RoleID: 1, // user ProviderID: info.ID, AvatarURL: info.Picture, IsActive: true, @@ -156,7 +163,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model. CountryID: 2, // default is England } - if err := s.db.Create(&newUser).Error; err != nil { + if err := s.customerRepo.Create(&newUser); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } @@ -169,6 +176,13 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model. } } + var role *model.Role + role, err = s.roleRepo.Get(newUser.RoleID) + if err != nil { + return nil, err + } + newUser.Role = role + return &newUser, nil } From d56650ae5da258176f5f64aa35950b9ab2dbad9f Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 7 Apr 2026 14:42:45 +0200 Subject: [PATCH 15/25] feat: searching on customer list --- app/delivery/web/api/restricted/customer.go | 22 +++++++--- app/repos/customerRepo/customerRepo.go | 43 ++++++++++++++++--- .../customerService/customerService.go | 4 +- app/utils/query/find/find.go | 8 ++-- bruno/api_v1/customer/Customer list.yml | 6 ++- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index 7c04b7e..6e1a41c 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -70,12 +70,6 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error { } func (h *customerHandler) listCustomers(fc fiber.Ctx) error { - p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers) - if err != nil { - return fc.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) - } - user, ok := localeExtractor.GetCustomer(fc) if !ok || user == nil { return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). @@ -86,7 +80,21 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) } - customer, err := h.service.Find(user.LangID, p, filt) + p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers) + if err != nil { + return fc.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) + } + + search := fc.Query("search") + if search != "" { + if !user.HasPermission(perms.UserReadAny) { + return fc.Status(fiber.StatusForbidden). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden))) + } + } + + customer, err := h.service.Find(user.LangID, p, filt, search) if err != nil { return fc.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index 668785f..18dea15 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -1,6 +1,8 @@ package customerRepo import ( + "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/query/filters" @@ -11,7 +13,7 @@ type UICustomerRepo interface { Get(id uint) (*model.Customer, error) GetByEmail(email string) (*model.Customer, error) GetByExternalProviderId(provider model.AuthProvider, id string) (*model.Customer, error) - Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) + Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) Save(customer *model.Customer) error Create(customer *model.Customer) error } @@ -57,17 +59,46 @@ func (repo *CustomerRepo) GetByExternalProviderId(provider model.AuthProvider, i return &customer, err } -func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) { - found, err := find.Paginate[model.UserInList](langId, p, db.DB. +func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) { + + query := db.DB. Table("b2b_customers AS users"). Select(` users.id AS id, users.email AS email, users.first_name AS first_name, users.last_name AS last_name - `). - Scopes(filt.All()...), - ) + `) + + if search != "" { + words := strings.Fields(search) + if len(words) > 5 { + words = words[:5] + } + var conditions []string + var args []interface{} + for _, word := range words { + + conditions = append(conditions, ` + (LOWER(first_name) LIKE ? OR + LOWER(last_name) LIKE ? OR + LOWER(email) LIKE ?) + `) + + for range 3 { + args = append(args, "%"+strings.ToLower(word)+"%") + } + } + + conditionsQuery := strings.Join(conditions, " AND ") + + query = query.Where(conditionsQuery, args...) + + } + + query = query.Scopes(filt.All()...) + + found, err := find.Paginate[model.UserInList](langId, p, query) return &found, err } diff --git a/app/service/customerService/customerService.go b/app/service/customerService/customerService.go index f9f2f4a..bce463d 100644 --- a/app/service/customerService/customerService.go +++ b/app/service/customerService/customerService.go @@ -21,6 +21,6 @@ func (s *CustomerService) GetById(id uint) (*model.Customer, error) { return s.repo.Get(id) } -func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.UserInList], error) { - return s.repo.Find(langId, p, filt) +func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) { + return s.repo.Find(langId, p, filt, search) } diff --git a/app/utils/query/find/find.go b/app/utils/query/find/find.go index 57ef813..487c1d1 100644 --- a/app/utils/query/find/find.go +++ b/app/utils/query/find/find.go @@ -42,14 +42,14 @@ func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error return Found[T]{}, err } - columnsSpec := GetColumnsSpec[T](langID) + // columnsSpec := GetColumnsSpec[T](langID) return Found[T]{ Items: items, Count: uint(count), - Spec: map[string]interface{}{ - "columns": columnsSpec, - }, + // Spec: map[string]interface{}{ + // "columns": columnsSpec, + // }, }, err } diff --git a/bruno/api_v1/customer/Customer list.yml b/bruno/api_v1/customer/Customer list.yml index 0d5bc26..11c286b 100644 --- a/bruno/api_v1/customer/Customer list.yml +++ b/bruno/api_v1/customer/Customer list.yml @@ -5,7 +5,11 @@ info: http: method: GET - url: "{{bas_url}}/restricted/customer/list" + url: "{{bas_url}}/restricted/customer/list?search=" + params: + - name: search + value: "" + type: query auth: inherit settings: From 7eee0bd03229e511f563098fbcd0d1e9524afe75 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 8 Apr 2026 13:09:19 +0200 Subject: [PATCH 16/25] rebuilt storage --- app/delivery/middleware/auth.go | 68 ++++ .../web/api/restricted/productTranslation.go | 13 + app/delivery/web/api/restricted/search.go | 7 + app/delivery/web/api/restricted/storage.go | 133 ++------ app/delivery/web/api/webdav/storage.go | 198 ++++++++++++ app/delivery/web/init.go | 35 +- app/model/customer.go | 2 + app/repos/storageRepo/storageRepo.go | 144 +++++++-- app/service/authService/auth.go | 13 + app/service/storageService/storageService.go | 306 ++++++++++++------ app/utils/const_data/consts.go | 5 + app/utils/responseErrors/responseErrors.go | 18 +- .../{storage => storage-old}/copy.yml | 0 .../create-folder.yml | 0 .../{storage => storage-old}/delete-file.yml | 0 .../delete-folder.yml | 0 .../download-file.yml | 0 .../{storage => storage-old}/folder.yml | 2 +- .../{storage => storage-old}/list-content.yml | 0 .../{storage => storage-old}/move.yml | 0 .../{storage => storage-old}/upload-file.yml | 0 .../create-new-webdav-token.yml | 15 + .../b2b_daniel/storage-restricted/folder.yml | 7 + go.mod | 4 +- go.sum | 6 + .../20260302163122_create_tables.sql | 4 + storage/folder/a.txt | 1 - storage/folder1/test | 0 storage/folder1/test.txt | 1 - storage/test.txt | 1 - 30 files changed, 723 insertions(+), 260 deletions(-) create mode 100644 app/delivery/web/api/webdav/storage.go rename bruno/b2b_daniel/{storage => storage-old}/copy.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/create-folder.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/delete-file.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/delete-folder.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/download-file.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/folder.yml (73%) rename bruno/b2b_daniel/{storage => storage-old}/list-content.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/move.yml (100%) rename bruno/b2b_daniel/{storage => storage-old}/upload-file.yml (100%) create mode 100644 bruno/b2b_daniel/storage-restricted/create-new-webdav-token.yml create mode 100644 bruno/b2b_daniel/storage-restricted/folder.yml delete mode 100644 storage/folder/a.txt delete mode 100644 storage/folder1/test delete mode 100644 storage/folder1/test.txt delete mode 100644 storage/test.txt diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 14fc0df..8d5a906 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -1,8 +1,10 @@ package middleware import ( + "encoding/base64" "strconv" "strings" + "time" "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/model" @@ -133,6 +135,72 @@ func RequireAdmin() fiber.Handler { } } +// Webdav +func Webdav() fiber.Handler { + authService := authService.NewAuthService() + + return func(c fiber.Ctx) error { + authHeader := c.Get("Authorization") + if authHeader == "" { + c.Set("WWW-Authenticate", `Basic realm="webdav"`) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "authorization token required", + }) + } + + if !strings.HasPrefix(authHeader, "Basic ") { + c.Set("WWW-Authenticate", `Basic realm="webdav"`) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "invalid authorization token", + }) + } + + encoded := strings.TrimPrefix(authHeader, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + c.Set("WWW-Authenticate", `Basic realm="webdav"`) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "invalid authorization token", + }) + } + + credentials := strings.SplitN(string(decoded), ":", 2) + rawToken := "" + if len(credentials) == 1 { + rawToken = credentials[0] + } else if len(credentials) == 2 { + rawToken = credentials[1] + } + if len(rawToken) != constdata.NBYTES_IN_WEBDAV_TOKEN*2 { + c.Set("WWW-Authenticate", `Basic realm="webdav"`) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "invalid authorization token", + }) + } + + // we identify user based on this token. + user, err := authService.GetUserByWebdavToken(rawToken) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "user not found", + }) + } + + if user.WebdavExpires != nil && user.WebdavExpires.Before(time.Now()) { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "invalid or expired token", + }) + } + + var userLocale model.UserLocale + userLocale.OriginalUser = user + userLocale.User = user + c.Locals(constdata.USER_LOCALE, &userLocale) + + return c.Next() + } +} + // GetConfig returns the app config func GetConfig() *config.Config { return config.Get() diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go index ea6f906..58b378b 100644 --- a/app/delivery/web/api/restricted/productTranslation.go +++ b/app/delivery/web/api/restricted/productTranslation.go @@ -4,6 +4,7 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/model" "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" @@ -79,6 +80,12 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } + userRole, ok := localeExtractor.GetOriginalUserRole(c) + if !ok || userRole != model.RoleAdmin { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) + } + productID_attribute := c.Query("productID") productID, err := strconv.Atoi(productID_attribute) if err != nil { @@ -116,6 +123,12 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } + userRole, ok := localeExtractor.GetOriginalUserRole(c) + if !ok || userRole != model.RoleAdmin { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) + } + productID_attribute := c.Query("productID") productID, err := strconv.Atoi(productID_attribute) if err != nil { diff --git a/app/delivery/web/api/restricted/search.go b/app/delivery/web/api/restricted/search.go index 8881853..0a7bef3 100644 --- a/app/delivery/web/api/restricted/search.go +++ b/app/delivery/web/api/restricted/search.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" + "git.ma-al.com/goc_daniel/b2b/app/model" "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" @@ -43,6 +44,12 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } + userRole, ok := localeExtractor.GetOriginalUserRole(c) + if !ok || userRole != model.RoleAdmin { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) + } + err := h.meiliService.CreateIndex(id_lang) if err != nil { fmt.Printf("CreateIndex error: %v\n", err) diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index f337547..a8a09b7 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -4,8 +4,10 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/storageService" "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" @@ -27,63 +29,16 @@ func NewStorageHandler() *StorageHandler { func StorageHandlerRoutes(r fiber.Router) fiber.Router { handler := NewStorageHandler() + // for all users r.Get("/list-content/*", handler.ListContent) r.Get("/download-file/*", handler.DownloadFile) - r.Get("/move/*", handler.Move) - r.Get("/copy/*", handler.Copy) - r.Post("/upload-file/*", handler.UploadFile) - r.Get("/create-folder/*", handler.CreateFolder) - r.Delete("/delete-file/*", handler.DeleteFile) - r.Delete("/delete-folder/*", handler.DeleteFolder) + // for admins only + r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken) return r } -func (h *StorageHandler) Move(c fiber.Ctx) error { - src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - err = h.storageService.Move(src_abs_path, dest_abs_path) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) -} - -func (h *StorageHandler) Copy(c fiber.Ctx) error { - src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - err = h.storageService.Copy(src_abs_path, dest_abs_path) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) -} - // accepted path looks like e.g. "/folder1/" or "folder1" func (h *StorageHandler) ListContent(c fiber.Ctx) error { // relative path defaults to root directory @@ -122,72 +77,24 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { return c.SendStream(f, int(filesize)) } -func (h *StorageHandler) UploadFile(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) +func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error { + 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))) + } + + userRole, ok := localeExtractor.GetOriginalUserRole(c) + if !ok || userRole != model.RoleAdmin { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) + } + + new_token, err := h.storageService.NewWebdavToken(userID) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - f, err := c.FormFile("document") - if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrMissingFileFieldDocument)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument))) - } - - err = h.storageService.UploadFile(c, abs_path, f) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) -} - -func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - err = h.storageService.CreateFolder(abs_path, c.Query("name")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) -} - -func (h *StorageHandler) DeleteFile(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - err = h.storageService.DeleteFile(abs_path) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) -} - -func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error { - abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - err = h.storageService.DeleteFolder(abs_path) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(&new_token, 0, i18n.T_(c, response.Message_OK))) } diff --git a/app/delivery/web/api/webdav/storage.go b/app/delivery/web/api/webdav/storage.go new file mode 100644 index 0000000..8a01d0d --- /dev/null +++ b/app/delivery/web/api/webdav/storage.go @@ -0,0 +1,198 @@ +package webdav + +import ( + "bytes" + "io" + "net/http" + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/service/storageService" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" +) + +type StorageHandler struct { + storageService *storageService.StorageService + config *config.Config +} + +func NewStorageHandler() *StorageHandler { + return &StorageHandler{ + storageService: storageService.New(), + config: config.Get(), + } +} + +func StorageHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewStorageHandler() + + // for webdav use only + r.Get("/*", handler.Get) + r.Head("/*", handler.Get) + r.Put("/*", handler.Put) + r.Delete("/*", handler.Delete) + r.Add([]string{"MKCOL"}, "/*", handler.Mkcol) + r.Add([]string{"PROPFIND"}, "/*", handler.Propfind) + r.Add([]string{"PROPPATCH"}, "/*", handler.Proppatch) + r.Add([]string{"MOVE"}, "/*", handler.Move) + r.Add([]string{"COPY"}, "/*", handler.Copy) + + return r +} + +func (h *StorageHandler) Get(c fiber.Ctx) error { + // fmt.Println("GET") + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + info, err := h.storageService.EntryInfo(absPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + if info.IsDir() { + xml, err := h.storageService.Propfind(h.config.Storage.RootFolder, absPath, "1") + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + c.Set("Content-Type", `application/xml; charset="utf-8"`) + return c.Status(http.StatusMultiStatus).SendString(xml) + + } else { + f, filename, filesize, err := h.storageService.DownloadFilePrep(absPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + c.Attachment(filename) + c.Set("Content-Length", strconv.FormatInt(filesize, 10)) + c.Set("Content-Type", "application/octet-stream") + return c.SendStream(f, int(filesize)) + } +} + +func (h *StorageHandler) Put(c fiber.Ctx) error { + // fmt.Println("PUT") + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + var src io.Reader + if bodyStream := c.Request().BodyStream(); bodyStream != nil { + defer c.Request().CloseBodyStream() + src = bodyStream + } else { + src = bytes.NewReader(c.Body()) + } + + err = h.storageService.Put(absPath, src) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + return c.SendStatus(http.StatusCreated) +} + +func (h *StorageHandler) Delete(c fiber.Ctx) error { + // fmt.Println("DELETE") + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + if absPath == h.config.Storage.RootFolder { + return c.SendStatus(responseErrors.GetErrorStatus(responseErrors.ErrAccessDenied)) + } + + err = h.storageService.Delete(absPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + return c.SendStatus(http.StatusNoContent) +} + +func (h *StorageHandler) Mkcol(c fiber.Ctx) error { + // fmt.Println("Mkcol") + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + err = h.storageService.Mkcol(absPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + return c.SendStatus(http.StatusCreated) +} + +func (h *StorageHandler) Propfind(c fiber.Ctx) error { + // fmt.Println("PROPFIND") + absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + xml, err := h.storageService.Propfind(h.config.Storage.RootFolder, absPath, "1") + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + c.Set("Content-Type", `application/xml; charset="utf-8"`) + return c.Status(http.StatusMultiStatus).SendString(xml) +} + +func (h *StorageHandler) Proppatch(c fiber.Ctx) error { + return c.SendStatus(http.StatusNotImplemented) // 501 +} + +func (h *StorageHandler) Move(c fiber.Ctx) error { + // fmt.Println("MOVE") + srcAbsPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + dest := c.Get("Destination") + if dest == "" { + return c.SendStatus(http.StatusBadRequest) + } + destAbsPath, err := h.storageService.ObtainDestPath(h.config.Storage.RootFolder, dest) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + err = h.storageService.Move(srcAbsPath, destAbsPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + return c.SendStatus(http.StatusCreated) +} + +func (h *StorageHandler) Copy(c fiber.Ctx) error { + // fmt.Println("COPY") + srcAbsPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*")) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + dest := c.Get("Destination") + if dest == "" { + return c.SendStatus(http.StatusBadRequest) + } + destAbsPath, err := h.storageService.ObtainDestPath(h.config.Storage.RootFolder, dest) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + + err = h.storageService.Copy(srcAbsPath, destAbsPath) + if err != nil { + return c.SendStatus(responseErrors.GetErrorStatus(err)) + } + return c.SendStatus(http.StatusCreated) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index c48a778..2139073 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -14,6 +14,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/public" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/restricted" + "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/webdav" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/general" "github.com/gofiber/fiber/v3" @@ -25,6 +26,7 @@ import ( type Server struct { app *fiber.App cfg *config.Config + webdav fiber.Router api fiber.Router public fiber.Router restricted fiber.Router @@ -42,12 +44,23 @@ func (s *Server) Cfg() *config.Config { // New creates a new server instance func New() *Server { - return &Server{ - app: fiber.New(fiber.Config{ - ErrorHandler: customErrorHandler, - }), - cfg: config.Get(), - } + var s Server + + app := + fiber.New(fiber.Config{ + ErrorHandler: customErrorHandler, + BodyLimit: 50 * 1024 * 1024, // 50 MB + StreamRequestBody: true, + RequestMethods: []string{ + fiber.MethodGet, fiber.MethodHead, fiber.MethodPost, fiber.MethodPut, + fiber.MethodDelete, fiber.MethodConnect, fiber.MethodOptions, + fiber.MethodTrace, fiber.MethodPatch, "MKCOL", "PROPFIND", "PROPPATCH", "MOVE", "COPY", + }, + }) + + s.app = app + s.cfg = config.Get() + return &s } // Setup configures the server with routes and middleware @@ -76,6 +89,8 @@ func (s *Server) Setup() error { s.public = s.api.Group("/public") s.restricted = s.api.Group("/restricted") s.restricted.Use(middleware.AuthMiddleware()) + s.webdav = s.api.Group("/webdav") + s.webdav.Use(middleware.Webdav()) // initialize language endpoints (general) api.NewLangHandler().InitLanguage(s.api, s.cfg) @@ -115,9 +130,11 @@ func (s *Server) Setup() error { carts := s.restricted.Group("/carts") restricted.CartsHandlerRoutes(carts) - // storage (restricted) - storage := s.restricted.Group("/storage") - restricted.StorageHandlerRoutes(storage) + // storage (uses various authorization means) + restrictedStorage := s.restricted.Group("/storage") + webdavStorage := s.webdav.Group("/storage") + restricted.StorageHandlerRoutes(restrictedStorage) + webdav.StorageHandlerRoutes(webdavStorage) s.api.All("*", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) diff --git a/app/model/customer.go b/app/model/customer.go index 3934dcd..60164ae 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -23,6 +23,8 @@ type Customer struct { EmailVerificationExpires *time.Time `json:"-"` PasswordResetToken string `gorm:"size:255" json:"-"` PasswordResetExpires *time.Time `json:"-"` + WebdavToken string `gorm:"size:255" json:"-"` + WebdavExpires *time.Time `json:"-"` LastPasswordResetRequest *time.Time `json:"-"` LastLoginAt *time.Time `json:"last_login_at,omitempty"` LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go index 08441a6..69dc906 100644 --- a/app/repos/storageRepo/storageRepo.go +++ b/app/repos/storageRepo/storageRepo.go @@ -2,23 +2,24 @@ package storageRepo import ( "io" - "mime/multipart" "os" + "path/filepath" + "time" + "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" - "github.com/gofiber/fiber/v3" ) type UIStorageRepo interface { + SaveWebdavToken(user_id uint, hash_token string, expires_at *time.Time) error EntryInfo(abs_path string) (os.FileInfo, error) ListContent(abs_path string) (*[]model.EntryInList, error) + OpenFile(abs_path string) (*os.File, error) + Put(abs_path string, src io.Reader) error + Delete(abs_path string) error + Mkcol(abs_path string) error Move(src_abs_path string, dest_abs_path string) error Copy(src_abs_path string, dest_abs_path string) error - OpenFile(abs_path string) (*os.File, error) - UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error - CreateFolder(abs_path string) error - DeleteFile(abs_path string) error - DeleteFolder(abs_path string) error } type StorageRepo struct{} @@ -27,6 +28,17 @@ func New() UIStorageRepo { return &StorageRepo{} } +func (r *StorageRepo) SaveWebdavToken(user_id uint, hash_token string, expires_at *time.Time) error { + return db.DB. + Table("b2b_customers"). + Where("id = ?", user_id). + Updates(map[string]interface{}{ + "webdav_token": hash_token, + "webdav_expires": expires_at, + }). + Error +} + func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) { return os.Stat(abs_path) } @@ -50,51 +62,117 @@ func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) return &entries_in_list, nil } +func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { + return os.Open(abs_path) +} + +func (r *StorageRepo) Put(abs_path string, src io.Reader) error { + // Write to a temp file in the same directory, then atomically rename. + tmp, err := os.CreateTemp(filepath.Dir(abs_path), ".put-*") + if err != nil { + return err + } + tmp_name := tmp.Name() + cleanup_tmp := true + defer func() { + _ = tmp.Close() + if cleanup_tmp { + _ = os.Remove(tmp_name) + } + }() + + _, err = io.Copy(tmp, src) + if err != nil { + return err + } + + err = tmp.Sync() + if err != nil { + return err + } + err = tmp.Close() + if err != nil { + return err + } + + err = os.Chmod(tmp_name, 0o644) + if err != nil { + return err + } + + err = os.Rename(tmp_name, abs_path) + if err != nil { + return err + } + + cleanup_tmp = false + return nil +} + +func (r *StorageRepo) Delete(abs_path string) error { + return os.RemoveAll(abs_path) +} + +func (r *StorageRepo) Mkcol(abs_path string) error { + return os.Mkdir(abs_path, 0755) +} + func (r *StorageRepo) Move(src_abs_path string, dest_abs_path string) error { return os.Rename(src_abs_path, dest_abs_path) } func (r *StorageRepo) Copy(src_abs_path string, dest_abs_path string) error { - in, err := os.Open(src_abs_path) - if err != nil { - return err - } - defer in.Close() - - info, err := in.Stat() + info, err := os.Stat(src_abs_path) if err != nil { return err } - out, err := os.OpenFile(dest_abs_path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if info.IsDir() { + return r.copyDir(src_abs_path, dest_abs_path) + } else { + return r.copyFile(src_abs_path, dest_abs_path) + } +} + +func (r *StorageRepo) copyFile(src_abs_path string, dest_abs_path string) error { + f, err := os.Open(src_abs_path) if err != nil { return err } - defer out.Close() + defer f.Close() - if _, err := io.Copy(out, in); err != nil { + err = r.Put(dest_abs_path, f) + return err +} + +func (r *StorageRepo) copyDir(src_abs_path string, dest_abs_path string) error { + if err := os.Mkdir(dest_abs_path, 0755); err != nil { return err } - return out.Sync() -} + entries, err := os.ReadDir(src_abs_path) + if err != nil { + return err + } -func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { - return os.Open(abs_path) -} + for _, entry := range entries { -func (r *StorageRepo) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error { - return c.SaveFile(f, abs_path) -} + entity_src_path := filepath.Join(src_abs_path, entry.Name()) + entity_dst_Path := filepath.Join(dest_abs_path, entry.Name()) -func (r *StorageRepo) CreateFolder(abs_path string) error { - return os.Mkdir(abs_path, 0755) -} + if entry.IsDir() { + err = r.copyDir(entity_src_path, entity_dst_Path) + if err != nil { + return err + } -func (r *StorageRepo) DeleteFile(abs_path string) error { - return os.Remove(abs_path) -} + } else { + err = r.copyFile(entity_src_path, entity_dst_Path) + if err != nil { + return err + } + } + } -func (r *StorageRepo) DeleteFolder(abs_path string) error { - return os.RemoveAll(abs_path) + return nil } diff --git a/app/service/authService/auth.go b/app/service/authService/auth.go index c873ce0..4b19a13 100644 --- a/app/service/authService/auth.go +++ b/app/service/authService/auth.go @@ -452,6 +452,19 @@ func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) { return &user, nil } +func (s *AuthService) GetUserByWebdavToken(rawToken string) (*model.Customer, error) { + tokenHash := hashToken(rawToken) + + var user model.Customer + if err := s.db.Where("webdav_token = ?", tokenHash).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, responseErrors.ErrUserNotFound + } + return nil, fmt.Errorf("database error: %w", err) + } + return &user, nil +} + // createRefreshToken generates a random opaque token, stores its hash in the DB, and returns the raw token. func (s *AuthService) createRefreshToken(userID uint) (string, error) { // Generate 32 random bytes → 64-char hex string diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index 6fc9d41..f5ffba8 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -1,15 +1,24 @@ package storageService import ( - "mime/multipart" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/xml" + "io" + "net/http" + "net/url" "os" + "path" "path/filepath" + "strconv" "strings" + "time" "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" - "github.com/gofiber/fiber/v3" ) type StorageService struct { @@ -22,14 +31,24 @@ func New() *StorageService { } } -func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) { - info, err := s.storageRepo.EntryInfo(abs_path) - if err != nil || !info.IsDir() { - return nil, responseErrors.ErrFolderDoesNotExist +func (s *StorageService) EntryInfo(abs_path string) (os.FileInfo, error) { + return s.storageRepo.EntryInfo(abs_path) +} + +func (s *StorageService) NewWebdavToken(user_id uint) (string, error) { + b := make([]byte, constdata.NBYTES_IN_WEBDAV_TOKEN) + + _, err := rand.Read(b) + if err != nil { + return "", err } - entries_in_list, err := s.storageRepo.ListContent(abs_path) - return entries_in_list, err + raw_token := hex.EncodeToString(b) + hash_token_bytes := sha256.Sum256([]byte(raw_token)) + hash_token := hex.EncodeToString(hash_token_bytes[:]) + expires_at := time.Now().Add(24 * time.Hour) + + return raw_token, s.storageRepo.SaveWebdavToken(user_id, hash_token, &expires_at) } func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) { @@ -46,118 +65,219 @@ func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, in return f, filepath.Base(abs_path), info.Size(), nil } +func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || !info.IsDir() { + return nil, responseErrors.ErrFolderDoesNotExist + } + + entries_in_list, err := s.storageRepo.ListContent(abs_path) + return entries_in_list, err +} + +func (s *StorageService) Propfind(root string, abs_path string, depth string) (string, error) { + href := href(root, abs_path) + + max_depth := 0 + switch depth { + case "0": + max_depth = 0 + case "1": + max_depth = 1 + case "infinity": + max_depth = 32 + default: + max_depth = 0 + } + + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil { + return "", err + } + + xml := `` + + `` + + if info.IsDir() { + href = ensureTrailingSlash(href) + next_xml, err := buildDirPropResponse(abs_path, href, info, max_depth) + if err != nil { + return "", err + } + xml += next_xml + } else { + xml += buildFilePropResponse(href, info) + } + + xml += `` + + return xml, nil +} + +func (s *StorageService) Put(abs_path string, src io.Reader) error { + return s.storageRepo.Put(abs_path, src) +} + +func (s *StorageService) Delete(abs_path string) error { + return s.storageRepo.Delete(abs_path) +} + +func (s *StorageService) Mkcol(abs_path string) error { + _, err := s.storageRepo.EntryInfo(abs_path) + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.Mkcol(abs_path) + } else { + return err + } +} + func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error { - _, err := s.storageRepo.EntryInfo(src_abs_path) - if err != nil { - return responseErrors.ErrFileDoesNotExist - } - - _, err = s.storageRepo.EntryInfo(dest_abs_path) - if err == nil { - return responseErrors.ErrNameTaken - } else if os.IsNotExist(err) { - return s.storageRepo.Move(src_abs_path, dest_abs_path) - } else { - return err - } + return s.storageRepo.Move(src_abs_path, dest_abs_path) } + func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error { - _, err := s.storageRepo.EntryInfo(src_abs_path) + return s.storageRepo.Copy(src_abs_path, dest_abs_path) +} + +func buildFilePropResponse(href string, info os.FileInfo) string { + name := info.Name() + return "" + + "" + + "" + xmlEscape(href) + "" + + "" + + "" + + "" + xmlEscape(name) + "" + + "" + strconv.FormatInt(info.Size(), 10) + "" + + "" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "" + + "" + + "" + + "HTTP/1.1 200 OK" + + "" + + "" +} + +func buildDirPropResponse(abs_path string, href string, info os.FileInfo, max_depth int) (string, error) { + name := info.Name() + + xml := "" + + "" + + "" + xmlEscape(ensureTrailingSlash(href)) + "" + + "" + + "" + + "" + xmlEscape(name) + "" + + "" + + "" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "" + + "" + + "HTTP/1.1 200 OK" + + "" + + "" + + if max_depth <= 0 { + return xml, nil + } + + entries, err := os.ReadDir(abs_path) if err != nil { - return responseErrors.ErrFileDoesNotExist + return "", err } - _, err = s.storageRepo.EntryInfo(dest_abs_path) - if err == nil { - return responseErrors.ErrNameTaken - } else if os.IsNotExist(err) { - return s.storageRepo.Copy(src_abs_path, dest_abs_path) - } else { - return err + for _, entry := range entries { + child_abs_path := filepath.Join(abs_path, entry.Name()) + child_href := path.Join(href, entry.Name()) + + child_info, err := entry.Info() + if err != nil { + return "", err + } + + var xml_next string + if entry.IsDir() { + xml_next, err = buildDirPropResponse(child_abs_path, ensureTrailingSlash(child_href), child_info, max_depth-1) + } else { + xml_next = buildFilePropResponse(child_href, child_info) + } + + if err != nil { + return "", err + } + xml += xml_next } + + return xml, nil } -func (s *StorageService) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error { - info, err := s.storageRepo.EntryInfo(abs_path) - if err != nil || !info.IsDir() { - return responseErrors.ErrFolderDoesNotExist +func ensureTrailingSlash(s string) string { + if s == "/" { + return s } - - name := f.Filename - if name == "" || name == "." || name == ".." || filepath.Base(name) != name { - return responseErrors.ErrBadAttribute - } - abs_file_path, err := s.AbsPath(abs_path, name) - if err != nil { - return err - } - if abs_file_path == abs_path { - return responseErrors.ErrBadAttribute - } - - info, err = s.storageRepo.EntryInfo(abs_file_path) - if err == nil { - return responseErrors.ErrNameTaken - } else if os.IsNotExist(err) { - return s.storageRepo.UploadFile(c, abs_file_path, f) - } else { - return err + if !strings.HasSuffix(s, "/") { + return s + "/" } + return s } -func (s *StorageService) CreateFolder(abs_path string, name string) error { - info, err := s.storageRepo.EntryInfo(abs_path) - if err != nil || !info.IsDir() { - return responseErrors.ErrFolderDoesNotExist - } - - if name == "" || name == "." || name == ".." || filepath.Base(name) != name { - return responseErrors.ErrBadAttribute - } - abs_folder_path, err := s.AbsPath(abs_path, name) - if err != nil { - return err - } - if abs_folder_path == abs_path { - return responseErrors.ErrBadAttribute - } - - info, err = s.storageRepo.EntryInfo(abs_folder_path) - if err == nil { - return responseErrors.ErrNameTaken - } else if os.IsNotExist(err) { - return s.storageRepo.CreateFolder(abs_folder_path) - } else { - return err - } +func xmlEscape(s string) string { + var b strings.Builder + xml.EscapeText(&b, []byte(s)) + return b.String() } -func (s *StorageService) DeleteFile(abs_path string) error { - info, err := s.storageRepo.EntryInfo(abs_path) - if err != nil || info.IsDir() { - return responseErrors.ErrFileDoesNotExist +// Returns href based on file's absolute path. Doesn't validate abs_path +func href(root string, abs_path string) string { + rel, _ := filepath.Rel(root, abs_path) + + if rel == "." { + return constdata.WEBDAV_HREF_ROOT + "/" } - return s.storageRepo.DeleteFile(abs_path) -} + rel = filepath.ToSlash(rel) -func (s *StorageService) DeleteFolder(abs_path string) error { - info, err := s.storageRepo.EntryInfo(abs_path) - if err != nil || !info.IsDir() { - return responseErrors.ErrFolderDoesNotExist + parts := strings.Split(rel, "/") + for i, p := range parts { + parts[i] = url.PathEscape(p) } - return s.storageRepo.DeleteFolder(abs_path) + return strings.TrimRight(constdata.WEBDAV_HREF_ROOT, "/") + "/" + strings.Join(parts, "/") } // AbsPath extracts an absolute path and validates it -func (s *StorageService) AbsPath(root string, relativePath string) (string, error) { - clean_name := filepath.Clean(relativePath) +func (s *StorageService) AbsPath(root string, relative_path string) (string, error) { + decoded, err := url.PathUnescape(relative_path) + if err != nil { + return "", err + } + + clean_name := filepath.Clean(decoded) full_path := filepath.Join(root, clean_name) - if full_path != root && !strings.HasPrefix(full_path, root+string(os.PathSeparator)) { + if full_path != root && !strings.HasPrefix(full_path, root+"/") { return "", responseErrors.ErrAccessDenied } return full_path, nil } + +// ObtainDestPath extracts the absolute path based on URL absolute path +func (s *StorageService) ObtainDestPath(root string, dest_path string) (string, error) { + idx := strings.Index(dest_path, constdata.WEBDAV_TRIMMED_ROOT) + if idx == -1 { + return "", responseErrors.ErrAccessDenied + } + prefix_removed := dest_path[idx+len(constdata.WEBDAV_TRIMMED_ROOT):] + + decoded, err := url.PathUnescape(prefix_removed) + if err != nil { + return "", err + } + + clean_dest_path := filepath.Clean(decoded) + if clean_dest_path == "" { + return root, nil + } else if strings.HasPrefix(clean_dest_path, "/") { + return root + "/" + strings.TrimPrefix(clean_dest_path, "/"), nil + } else { + return "", responseErrors.ErrAccessDenied + } +} diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 05f23e8..f71ed51 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -13,3 +13,8 @@ const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" const USER_LOCALE = "user" + +// WEBDAV +const NBYTES_IN_WEBDAV_TOKEN = 32 +const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage" +const WEBDAV_TRIMMED_ROOT = "localhost:3000/api/v1/webdav/storage" diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 2ea09e5..d81ecbb 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -9,13 +9,14 @@ import ( var ( // Typed errors for request validation and authentication - ErrInvalidBody = errors.New("invalid request body") - ErrNotAuthenticated = errors.New("not authenticated") - ErrUserNotFound = errors.New("user not found") - ErrUserInactive = errors.New("user account is inactive") - ErrInvalidToken = errors.New("invalid token") - ErrTokenExpired = errors.New("token has expired") - ErrTokenRequired = errors.New("token is required") + ErrInvalidBody = errors.New("invalid request body") + ErrNotAuthenticated = errors.New("not authenticated") + ErrUserNotFound = errors.New("user not found") + ErrUserInactive = errors.New("user account is inactive") + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("token has expired") + ErrTokenRequired = errors.New("token is required") + ErrAdminAccessRequired = errors.New("admin access is required") // Typed errors for logging in and registering ErrInvalidCredentials = errors.New("invalid email or password") @@ -118,6 +119,8 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.err_token_required") case errors.Is(err, ErrRefreshTokenRequired): return i18n.T_(c, "error.err_refresh_token_required") + case errors.Is(err, ErrAdminAccessRequired): + return i18n.T_(c, "error.err_admin_access_required") case errors.Is(err, ErrBadLangID): return i18n.T_(c, "error.err_bad_lang_id") case errors.Is(err, ErrBadCountryID): @@ -202,6 +205,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrEmailPasswordRequired), errors.Is(err, ErrTokenRequired), errors.Is(err, ErrRefreshTokenRequired), + errors.Is(err, ErrAdminAccessRequired), errors.Is(err, ErrBadLangID), errors.Is(err, ErrBadCountryID), errors.Is(err, ErrPasswordsDoNotMatch), diff --git a/bruno/b2b_daniel/storage/copy.yml b/bruno/b2b_daniel/storage-old/copy.yml similarity index 100% rename from bruno/b2b_daniel/storage/copy.yml rename to bruno/b2b_daniel/storage-old/copy.yml diff --git a/bruno/b2b_daniel/storage/create-folder.yml b/bruno/b2b_daniel/storage-old/create-folder.yml similarity index 100% rename from bruno/b2b_daniel/storage/create-folder.yml rename to bruno/b2b_daniel/storage-old/create-folder.yml diff --git a/bruno/b2b_daniel/storage/delete-file.yml b/bruno/b2b_daniel/storage-old/delete-file.yml similarity index 100% rename from bruno/b2b_daniel/storage/delete-file.yml rename to bruno/b2b_daniel/storage-old/delete-file.yml diff --git a/bruno/b2b_daniel/storage/delete-folder.yml b/bruno/b2b_daniel/storage-old/delete-folder.yml similarity index 100% rename from bruno/b2b_daniel/storage/delete-folder.yml rename to bruno/b2b_daniel/storage-old/delete-folder.yml diff --git a/bruno/b2b_daniel/storage/download-file.yml b/bruno/b2b_daniel/storage-old/download-file.yml similarity index 100% rename from bruno/b2b_daniel/storage/download-file.yml rename to bruno/b2b_daniel/storage-old/download-file.yml diff --git a/bruno/b2b_daniel/storage/folder.yml b/bruno/b2b_daniel/storage-old/folder.yml similarity index 73% rename from bruno/b2b_daniel/storage/folder.yml rename to bruno/b2b_daniel/storage-old/folder.yml index 70062a4..852efec 100644 --- a/bruno/b2b_daniel/storage/folder.yml +++ b/bruno/b2b_daniel/storage-old/folder.yml @@ -1,5 +1,5 @@ info: - name: storage + name: storage-old type: folder seq: 1 diff --git a/bruno/b2b_daniel/storage/list-content.yml b/bruno/b2b_daniel/storage-old/list-content.yml similarity index 100% rename from bruno/b2b_daniel/storage/list-content.yml rename to bruno/b2b_daniel/storage-old/list-content.yml diff --git a/bruno/b2b_daniel/storage/move.yml b/bruno/b2b_daniel/storage-old/move.yml similarity index 100% rename from bruno/b2b_daniel/storage/move.yml rename to bruno/b2b_daniel/storage-old/move.yml diff --git a/bruno/b2b_daniel/storage/upload-file.yml b/bruno/b2b_daniel/storage-old/upload-file.yml similarity index 100% rename from bruno/b2b_daniel/storage/upload-file.yml rename to bruno/b2b_daniel/storage-old/upload-file.yml diff --git a/bruno/b2b_daniel/storage-restricted/create-new-webdav-token.yml b/bruno/b2b_daniel/storage-restricted/create-new-webdav-token.yml new file mode 100644 index 0000000..4340fda --- /dev/null +++ b/bruno/b2b_daniel/storage-restricted/create-new-webdav-token.yml @@ -0,0 +1,15 @@ +info: + name: create-new-webdav-token + type: http + seq: 1 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/create-new-webdav-token + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/storage-restricted/folder.yml b/bruno/b2b_daniel/storage-restricted/folder.yml new file mode 100644 index 0000000..ec9eca7 --- /dev/null +++ b/bruno/b2b_daniel/storage-restricted/folder.yml @@ -0,0 +1,7 @@ +info: + name: storage-restricted + type: folder + seq: 9 + +request: + auth: inherit diff --git a/go.mod b/go.mod index 62c8aad..1c184da 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,8 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -98,7 +100,7 @@ require ( github.com/valyala/fasthttp v1.69.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xyproto/randomstring v1.2.0 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index d208fb1..81fe849 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= @@ -134,6 +136,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -154,6 +158,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index bfe5401..ae553dd 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -69,6 +69,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers ( email_verification_expires DATETIME(6) NULL, password_reset_token VARCHAR(255) NULL, password_reset_expires DATETIME(6) NULL, + webdav_token VARCHAR(255) NULL, + webdav_expires DATETIME(6) NULL, last_password_reset_request DATETIME(6) NULL, last_login_at DATETIME(6) NULL, lang_id BIGINT NULL DEFAULT 2, @@ -84,6 +86,8 @@ ON b2b_customers (email); CREATE INDEX IF NOT EXISTS idx_customers_deleted_at ON b2b_customers (deleted_at); +CREATE INDEX IF NOT EXISTS idx_customers_webdav_token +ON b2b_customers (webdav_token); -- customer_carts CREATE TABLE IF NOT EXISTS b2b_customer_carts ( diff --git a/storage/folder/a.txt b/storage/folder/a.txt deleted file mode 100644 index 273c1a9..0000000 --- a/storage/folder/a.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test. \ No newline at end of file diff --git a/storage/folder1/test b/storage/folder1/test deleted file mode 100644 index e69de29..0000000 diff --git a/storage/folder1/test.txt b/storage/folder1/test.txt deleted file mode 100644 index 273c1a9..0000000 --- a/storage/folder1/test.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test. \ No newline at end of file diff --git a/storage/test.txt b/storage/test.txt deleted file mode 100644 index 273c1a9..0000000 --- a/storage/test.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test. \ No newline at end of file From 578d8c6cac5ce646acd015881c6231ce6f814368 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 8 Apr 2026 13:20:07 +0200 Subject: [PATCH 17/25] merged with current main --- app/delivery/middleware/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index b8837c4..756e79f 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -125,7 +125,7 @@ func RequireAdmin() fiber.Handler { }) } - if originalUserRole != model.RoleAdmin { + if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "admin access required", }) From 569a805a133bbdc109edfc8645ab80bbc4ea260a Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 8 Apr 2026 13:23:05 +0200 Subject: [PATCH 18/25] small fix --- app/delivery/web/api/restricted/productTranslation.go | 4 ++-- app/delivery/web/api/restricted/search.go | 2 +- app/delivery/web/api/restricted/storage.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go index 58b378b..3dc16bd 100644 --- a/app/delivery/web/api/restricted/productTranslation.go +++ b/app/delivery/web/api/restricted/productTranslation.go @@ -81,7 +81,7 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error { } userRole, ok := localeExtractor.GetOriginalUserRole(c) - if !ok || userRole != model.RoleAdmin { + if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) } @@ -124,7 +124,7 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err } userRole, ok := localeExtractor.GetOriginalUserRole(c) - if !ok || userRole != model.RoleAdmin { + if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) } diff --git a/app/delivery/web/api/restricted/search.go b/app/delivery/web/api/restricted/search.go index 0a7bef3..843c956 100644 --- a/app/delivery/web/api/restricted/search.go +++ b/app/delivery/web/api/restricted/search.go @@ -45,7 +45,7 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { } userRole, ok := localeExtractor.GetOriginalUserRole(c) - if !ok || userRole != model.RoleAdmin { + if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) } diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index a8a09b7..910aae1 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -85,7 +85,7 @@ func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error { } userRole, ok := localeExtractor.GetOriginalUserRole(c) - if !ok || userRole != model.RoleAdmin { + if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired))) } From 1083ab7a61a6099b7d17e81b010e40eafd777b08 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 9 Apr 2026 12:21:56 +0200 Subject: [PATCH 19/25] added addresses endpoints --- app/delivery/web/api/restricted/addresses.go | 157 ++++++++++++++++++ app/delivery/web/init.go | 4 + app/model/address.go | 79 +++++++++ app/repos/addressesRepo/addressesRepo.go | 91 ++++++++++ .../addressesService/addressesService.go | 152 +++++++++++++++++ app/utils/const_data/consts.go | 2 + app/utils/responseErrors/responseErrors.go | 47 ++++-- bo/components.d.ts | 1 - .../b2b_daniel/addresses/add-new-address.yml | 31 ++++ bruno/b2b_daniel/addresses/delete-address.yml | 19 +++ bruno/b2b_daniel/addresses/folder.yml | 7 + bruno/b2b_daniel/addresses/get-template.yml | 19 +++ bruno/b2b_daniel/addresses/modify-address.yml | 33 ++++ .../addresses/retrieve-addresses.yml | 15 ++ .../storage-restricted/download-file.yml | 15 ++ .../storage-restricted/list-content.yml | 15 ++ .../20260302163122_create_tables.sql | 12 ++ 17 files changed, 684 insertions(+), 15 deletions(-) create mode 100644 app/delivery/web/api/restricted/addresses.go create mode 100644 app/model/address.go create mode 100644 app/repos/addressesRepo/addressesRepo.go create mode 100644 app/service/addressesService/addressesService.go create mode 100644 bruno/b2b_daniel/addresses/add-new-address.yml create mode 100644 bruno/b2b_daniel/addresses/delete-address.yml create mode 100644 bruno/b2b_daniel/addresses/folder.yml create mode 100644 bruno/b2b_daniel/addresses/get-template.yml create mode 100644 bruno/b2b_daniel/addresses/modify-address.yml create mode 100644 bruno/b2b_daniel/addresses/retrieve-addresses.yml create mode 100644 bruno/b2b_daniel/storage-restricted/download-file.yml create mode 100644 bruno/b2b_daniel/storage-restricted/list-content.yml diff --git a/app/delivery/web/api/restricted/addresses.go b/app/delivery/web/api/restricted/addresses.go new file mode 100644 index 0000000..903f011 --- /dev/null +++ b/app/delivery/web/api/restricted/addresses.go @@ -0,0 +1,157 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/service/addressesService" + "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" + "github.com/gofiber/fiber/v3" +) + +type AddressesHandler struct { + addressesService *addressesService.AddressesService +} + +func NewAddressesHandler() *AddressesHandler { + addressesService := addressesService.New() + return &AddressesHandler{ + addressesService: addressesService, + } +} + +func AddressesHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewAddressesHandler() + + r.Get("/get-template", handler.GetTemplate) + r.Post("/add-new-address", handler.AddNewAddress) + r.Post("/modify-address", handler.ModifyAddress) + r.Get("/retrieve-addresses", handler.RetrieveAddressesInfo) + r.Delete("/delete-address", handler.DeleteAddress) + + return r +} + +func (h *AddressesHandler) GetTemplate(c fiber.Ctx) error { + country_id_attribute := c.Query("country_id") + country_id, err := strconv.Atoi(country_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + template, err := h.addressesService.GetTemplate(uint(country_id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&template, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *AddressesHandler) AddNewAddress(c fiber.Ctx) error { + 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))) + } + + address_info := string(c.Body()) + if address_info == "" { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + country_id_attribute := c.Query("country_id") + country_id, err := strconv.Atoi(country_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *AddressesHandler) ModifyAddress(c fiber.Ctx) error { + 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))) + } + + address_id_attribute := c.Query("address_id") + address_id, err := strconv.Atoi(address_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + address_info := string(c.Body()) + if address_info == "" { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + country_id_attribute := c.Query("country_id") + country_id, err := strconv.Atoi(country_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error { + 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))) + } + + addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error { + 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))) + } + + address_id_attribute := c.Query("address_id") + address_id, err := strconv.Atoi(address_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + err = h.addressesService.DeleteAddress(userID, uint(address_id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 51d9f51..2162d66 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -132,6 +132,10 @@ func (s *Server) Setup() error { carts := s.restricted.Group("/carts") restricted.CartsHandlerRoutes(carts) + // addresses (restricted) + addresses := s.restricted.Group("/addresses") + restricted.AddressesHandlerRoutes(addresses) + // storage (uses various authorization means) restrictedStorage := s.restricted.Group("/storage") webdavStorage := s.webdav.Group("/storage") diff --git a/app/model/address.go b/app/model/address.go new file mode 100644 index 0000000..a84056a --- /dev/null +++ b/app/model/address.go @@ -0,0 +1,79 @@ +package model + +type Address struct { + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` + AddressInfo string `gorm:"column:address_info;not null" json:"address_info"` + CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` +} + +func (Address) TableName() string { + return "b2b_addresses" +} + +type AddressUnparsed struct { + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` + AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"` + CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` +} + +type AddressField interface { +} + +// Address template in Poland +type AddressPL struct { + PostalCode string `json:"postal_code"` // format: 00-000 + City string `json:"city"` // e.g. Kraków + Voivodeship string `json:"voivodeship"` // e.g. małopolskie (optional but useful) + + Street string `json:"street"` // e.g. Marszałkowska + BuildingNo string `json:"building_no"` // e.g. 10, 221B, 12A + ApartmentNo string `json:"apartment_no"` // e.g. 5, 12B + + AddressLine2 string `json:"address_line2"` // optional extra info + + Recipient string `json:"recipient"` // name/company +} + +// Address template in Great Britain +type AddressGB struct { + PostalCode string `json:"postal_code"` // e.g. SW1A 1AA + PostTown string `json:"post_town"` // e.g. London + County string `json:"county"` // optional + + Thoroughfare string `json:"thoroughfare"` // street name, e.g. Baker Street + BuildingNo string `json:"building_no"` // e.g. 221B + BuildingName string `json:"building_name"` // e.g. Flatiron House + SubBuilding string `json:"sub_building"` // e.g. Flat 5, Apt 2 + + AddressLine2 string `json:"address_line2"` + Recipient string `json:"recipient"` +} + +// Address template in Czech Republic +type AddressCZ struct { + PostalCode string `json:"postal_code"` // usually 110 00 or 11000 + City string `json:"city"` // e.g. Praha + Region string `json:"region"` + + Street string `json:"street"` // may be omitted in some village-style addresses + HouseNumber string `json:"house_number"` // descriptive / conscription no. + OrientationNumber string `json:"orientation_number"` // optional, often after slash + + AddressLine2 string `json:"address_line2"` + Recipient string `json:"recipient"` +} + +// Address template in Germany +type AddressDE struct { + PostalCode string `json:"postal_code"` // e.g. 10115 + City string `json:"city"` // e.g. Berlin + State string `json:"state"` // Bundesland, optional + + Street string `json:"street"` // e.g. Unter den Linden + HouseNumber string `json:"house_number"` // e.g. 77, 12a + + AddressLine2 string `json:"address_line2"` // extra details + Recipient string `json:"recipient"` +} diff --git a/app/repos/addressesRepo/addressesRepo.go b/app/repos/addressesRepo/addressesRepo.go new file mode 100644 index 0000000..5f674a0 --- /dev/null +++ b/app/repos/addressesRepo/addressesRepo.go @@ -0,0 +1,91 @@ +package addressesRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" +) + +type UIAddressesRepo interface { + UserHasAddress(user_id uint, address_id uint) (uint, error) + UserAddressesAmt(user_id uint) (uint, error) + AddNewAddress(user_id uint, address_info string, country_id uint) error + UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error + RetrieveAddresses(user_id uint) (*[]model.Address, error) + DeleteAddress(user_id uint, address_id uint) error +} + +type AddressesRepo struct{} + +func New() UIAddressesRepo { + return &AddressesRepo{} +} + +func (repo *AddressesRepo) UserHasAddress(user_id uint, address_id uint) (uint, error) { + var amt uint + + err := db.DB. + Table("b2b_addresses"). + Select("COUNT(*) AS amt"). + Where("id = ? AND b2b_customer_id = ?", address_id, user_id). + Scan(&amt). + Error + + return amt, err +} + +func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) { + var amt uint + + err := db.DB. + Table("b2b_addresses"). + Select("COUNT(*) AS amt"). + Where("b2b_customer_id = ?", user_id). + Scan(&amt). + Error + + return amt, err +} + +func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { + address := model.Address{ + CustomerID: user_id, + AddressInfo: address_info, + CountryID: country_id, + } + + return db.DB. + Create(&address). + Error +} + +func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error { + address := model.Address{ + ID: address_id, + CustomerID: user_id, + AddressInfo: address_info, + CountryID: country_id, + } + + return db.DB. + Where("id = ? AND b2b_customer_id = ?", address_id, user_id). + Updates(&address). + Error +} + +func (repo *AddressesRepo) RetrieveAddresses(user_id uint) (*[]model.Address, error) { + var addresses []model.Address + + err := db.DB. + Where("b2b_customer_id = ?", user_id). + Find(&addresses). + Error + + return &addresses, err +} + +func (repo *AddressesRepo) DeleteAddress(user_id uint, address_id uint) error { + return db.DB. + Where("id = ? AND b2b_customer_id = ?", address_id, user_id). + Delete(&model.Address{}). + Error +} diff --git a/app/service/addressesService/addressesService.go b/app/service/addressesService/addressesService.go new file mode 100644 index 0000000..b077486 --- /dev/null +++ b/app/service/addressesService/addressesService.go @@ -0,0 +1,152 @@ +package addressesService + +import ( + "encoding/json" + "fmt" + "strings" + + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/addressesRepo" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" +) + +type AddressesService struct { + repo addressesRepo.UIAddressesRepo +} + +func New() *AddressesService { + return &AddressesService{ + repo: addressesRepo.New(), + } +} + +func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) { + switch country_id { + + case 1: // Poland + return model.AddressPL{}, nil + + case 2: // Great Britain + return model.AddressGB{}, nil + + case 3: // Czech Republic + return model.AddressCZ{}, nil + + case 4: // Germany + return model.AddressDE{}, nil + + default: + return nil, responseErrors.ErrInvalidCountryID + } +} + +func (s *AddressesService) AddNewAddress(user_id uint, address_info string, country_id uint) error { + amt, err := s.repo.UserAddressesAmt(user_id) + if err != nil { + return err + } else if amt >= constdata.MAX_AMOUNT_OF_ADDRESSES_PER_USER { + return responseErrors.ErrMaxAmtOfAddressesReached + } + + _, err = s.validateAddressJson(address_info, country_id) + if err != nil { + return err + } + + return s.repo.AddNewAddress(user_id, address_info, country_id) +} + +// country_id = 0 means that country_id remains unchanged +func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_info string, country_id uint) error { + amt, err := s.repo.UserHasAddress(user_id, address_id) + if err != nil { + return err + } else if amt != 1 { + return responseErrors.ErrUserHasNoSuchAddress + } + + _, err = s.validateAddressJson(address_info, country_id) + if err != nil { + return err + } + + return s.repo.UpdateAddress(user_id, address_id, address_info, country_id) +} + +func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { + parsed_addresses, err := s.repo.RetrieveAddresses(user_id) + if err != nil { + return nil, err + } + + var unparsed_addresses []model.AddressUnparsed + + for i := 0; i < len(*parsed_addresses); i++ { + var next_address model.AddressUnparsed + next_address.ID = (*parsed_addresses)[i].ID + next_address.CustomerID = (*parsed_addresses)[i].CustomerID + next_address.CountryID = (*parsed_addresses)[i].CountryID + + next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID) + // log such errors + if err != nil { + fmt.Printf("err: %v\n", err) + } + + unparsed_addresses = append(unparsed_addresses, next_address) + } + + return &unparsed_addresses, nil +} + +func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { + amt, err := s.repo.UserHasAddress(user_id, address_id) + if err != nil { + return err + } else if amt != 1 { + return responseErrors.ErrUserHasNoSuchAddress + } + + return s.repo.DeleteAddress(user_id, address_id) +} + +// validateAddressJson makes sure that the info string represents a valid json of address in given country +func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) { + dec := json.NewDecoder(strings.NewReader(info)) + dec.DisallowUnknownFields() + + switch country_id { + + case 1: // Poland + var address model.AddressPL + if err := dec.Decode(&address); err != nil { + return address, responseErrors.ErrInvalidAddressJSON + } + return address, nil + + case 2: // Great Britain + var address model.AddressGB + if err := dec.Decode(&address); err != nil { + return address, responseErrors.ErrInvalidAddressJSON + } + return address, nil + + case 3: // Czech Republic + var address model.AddressCZ + if err := dec.Decode(&address); err != nil { + return address, responseErrors.ErrInvalidAddressJSON + } + return address, nil + + case 4: // Germany + var address model.AddressDE + if err := dec.Decode(&address); err != nil { + return address, responseErrors.ErrInvalidAddressJSON + } + return address, nil + + default: + return nil, responseErrors.ErrInvalidCountryID + } +} diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 1ed8a7c..aa62f27 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -12,6 +12,8 @@ const CATEGORY_TREE_ROOT_ID = 2 const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" +const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10 + const USER_LOCALE = "user" // WEBDAV diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index b3fe72f..28802e1 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -72,6 +72,12 @@ var ( // Typed errors for data parsing ErrJSONBody = errors.New("invalid JSON body") + + // Typed errors for addresses + ErrMaxAmtOfAddressesReached = errors.New("maximal amount of addresses per user reached") + ErrUserHasNoSuchAddress = errors.New("user has no such address") + ErrInvalidCountryID = errors.New("invalid country id") + ErrInvalidAddressJSON = errors.New("invalid address json") ) // Error represents an error with HTTP status code @@ -154,7 +160,7 @@ func GetErrorCode(c fiber.Ctx, err error) string { 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") + return i18n.T_(c, "error.err_invalid_url_slug") case errors.Is(err, ErrInvalidXHTML): return i18n.T_(c, "error.err_invalid_html") case errors.Is(err, ErrAIResponseFail): @@ -166,35 +172,44 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.err_bad_paging") case errors.Is(err, ErrNoRootFound): - return i18n.T_(c, "error.no_root_found") + return i18n.T_(c, "error.err_no_root_found") case errors.Is(err, ErrCircularDependency): - return i18n.T_(c, "error.circular_dependency") + return i18n.T_(c, "error.err_circular_dependency") case errors.Is(err, ErrStartCategoryNotFound): - return i18n.T_(c, "error.start_category_not_found") + return i18n.T_(c, "error.err_start_category_not_found") case errors.Is(err, ErrRootNeverReached): - return i18n.T_(c, "error.root_never_reached") + return i18n.T_(c, "error.err_root_never_reached") case errors.Is(err, ErrMaxAmtOfCartsReached): - return i18n.T_(c, "error.max_amt_of_carts_reached") + return i18n.T_(c, "error.err_max_amt_of_carts_reached") case errors.Is(err, ErrUserHasNoSuchCart): - return i18n.T_(c, "error.user_has_no_such_cart") + return i18n.T_(c, "error.err_user_has_no_such_cart") case errors.Is(err, ErrProductOrItsVariationDoesNotExist): - return i18n.T_(c, "error.product_or_its_variation_does_not_exist") + return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") case errors.Is(err, ErrAccessDenied): - return i18n.T_(c, "error.access_denied") + return i18n.T_(c, "error.err_access_denied") case errors.Is(err, ErrFolderDoesNotExist): - return i18n.T_(c, "error.folder_does_not_exist") + return i18n.T_(c, "error.err_folder_does_not_exist") case errors.Is(err, ErrFileDoesNotExist): - return i18n.T_(c, "error.file_does_not_exist") + return i18n.T_(c, "error.err_file_does_not_exist") case errors.Is(err, ErrNameTaken): - return i18n.T_(c, "error.name_taken") + return i18n.T_(c, "error.err_name_taken") case errors.Is(err, ErrMissingFileFieldDocument): - return i18n.T_(c, "error.missing_file_field_document") + return i18n.T_(c, "error.err_missing_file_field_document") case errors.Is(err, ErrJSONBody): return i18n.T_(c, "error.err_json_body") + case errors.Is(err, ErrMaxAmtOfAddressesReached): + return i18n.T_(c, "error.err_max_amt_of_addresses_reached") + case errors.Is(err, ErrUserHasNoSuchAddress): + return i18n.T_(c, "error.err_user_has_no_such_address") + case errors.Is(err, ErrInvalidCountryID): + return i18n.T_(c, "error.err_invalid_country_id") + case errors.Is(err, ErrInvalidAddressJSON): + return i18n.T_(c, "error.err_invalid_address_json") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -246,7 +261,11 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrFileDoesNotExist), errors.Is(err, ErrNameTaken), errors.Is(err, ErrMissingFileFieldDocument), - errors.Is(err, ErrJSONBody): + errors.Is(err, ErrJSONBody), + errors.Is(err, ErrMaxAmtOfAddressesReached), + errors.Is(err, ErrUserHasNoSuchAddress), + errors.Is(err, ErrInvalidCountryID), + errors.Is(err, ErrInvalidAddressJSON): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/bo/components.d.ts b/bo/components.d.ts index 51b00ed..06b4ea1 100644 --- a/bo/components.d.ts +++ b/bo/components.d.ts @@ -13,7 +13,6 @@ declare module 'vue' { export interface GlobalComponents { CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] - CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] diff --git a/bruno/b2b_daniel/addresses/add-new-address.yml b/bruno/b2b_daniel/addresses/add-new-address.yml new file mode 100644 index 0000000..9c1abc1 --- /dev/null +++ b/bruno/b2b_daniel/addresses/add-new-address.yml @@ -0,0 +1,31 @@ +info: + name: add-new-address + type: http + seq: 1 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/addresses/add-new-address?country_id=1 + params: + - name: country_id + value: "1" + type: query + body: + type: json + data: |- + { + "postal_code": "31-154", + "city": "Kraków", + "voivodeship": "małopolskie", + "street": "Długa", + "building_no": "5", + "apartment_no": "7", + "recipient": "Jan Kowalski" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/addresses/delete-address.yml b/bruno/b2b_daniel/addresses/delete-address.yml new file mode 100644 index 0000000..dc9d33c --- /dev/null +++ b/bruno/b2b_daniel/addresses/delete-address.yml @@ -0,0 +1,19 @@ +info: + name: delete-address + type: http + seq: 4 + +http: + method: DELETE + url: http://localhost:3000/api/v1/restricted/addresses/delete-address?address_id=1 + params: + - name: address_id + value: "1" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/addresses/folder.yml b/bruno/b2b_daniel/addresses/folder.yml new file mode 100644 index 0000000..aaa37e8 --- /dev/null +++ b/bruno/b2b_daniel/addresses/folder.yml @@ -0,0 +1,7 @@ +info: + name: addresses + type: folder + seq: 10 + +request: + auth: inherit diff --git a/bruno/b2b_daniel/addresses/get-template.yml b/bruno/b2b_daniel/addresses/get-template.yml new file mode 100644 index 0000000..4105fb8 --- /dev/null +++ b/bruno/b2b_daniel/addresses/get-template.yml @@ -0,0 +1,19 @@ +info: + name: get-template + type: http + seq: 5 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/addresses/get-template?country_id=3 + params: + - name: country_id + value: "3" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/addresses/modify-address.yml b/bruno/b2b_daniel/addresses/modify-address.yml new file mode 100644 index 0000000..aadd02d --- /dev/null +++ b/bruno/b2b_daniel/addresses/modify-address.yml @@ -0,0 +1,33 @@ +info: + name: modify-address + type: http + seq: 2 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/addresses/modify-address?country_id=1&address_id=1 + params: + - name: country_id + value: "1" + type: query + - name: address_id + value: "1" + type: query + body: + type: json + data: |- + { + "postal_code": "31-154", + "city": "Kraków", + "voivodeship": "śląskie", + "street": "Długa", + "building_no": "5", + "recipient": "Adam Adamowicz" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/addresses/retrieve-addresses.yml b/bruno/b2b_daniel/addresses/retrieve-addresses.yml new file mode 100644 index 0000000..e490024 --- /dev/null +++ b/bruno/b2b_daniel/addresses/retrieve-addresses.yml @@ -0,0 +1,15 @@ +info: + name: retrieve-addresses + type: http + seq: 3 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/addresses/retrieve-addresses + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/storage-restricted/download-file.yml b/bruno/b2b_daniel/storage-restricted/download-file.yml new file mode 100644 index 0000000..13553de --- /dev/null +++ b/bruno/b2b_daniel/storage-restricted/download-file.yml @@ -0,0 +1,15 @@ +info: + name: download-file + type: http + seq: 3 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/download-file/dest/src/cccc.txt + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/storage-restricted/list-content.yml b/bruno/b2b_daniel/storage-restricted/list-content.yml new file mode 100644 index 0000000..7c94250 --- /dev/null +++ b/bruno/b2b_daniel/storage-restricted/list-content.yml @@ -0,0 +1,15 @@ +info: + name: list-content + type: http + seq: 2 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/list-content/dest/src + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index d975294..a82cea5 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -210,6 +210,18 @@ ON `b2b_countries` ( `ps_id_country` ASC ); +-- addresses +CREATE TABLE IF NOT EXISTS b2b_addresses ( + id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, + b2b_customer_id BIGINT UNSIGNED NOT NULL, + address_info TEXT NOT NULL, + b2b_country_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_b2b_addresses_b2b_customers FOREIGN KEY (b2b_customer_id) REFERENCES b2b_customers (id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_b2b_addresses_b2b_countries FOREIGN KEY (b2b_country_id) REFERENCES b2b_countries (id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB; + + CREATE TABLE b2b_specific_price ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, From 393de36cb2b9fac750ad96dd9ae1dfb7839209b8 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 9 Apr 2026 14:49:50 +0200 Subject: [PATCH 20/25] favorites --- app/delivery/web/api/restricted/product.go | 8 ++- app/model/product.go | 1 + app/repos/productsRepo/productsRepo.go | 56 +++++++++++++------ app/service/productService/productService.go | 4 +- .../20260302163122_create_tables.sql | 10 ++++ i18n/migrations/20260319163200_procedures.sql | 6 ++ 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index ddd8677..2c9d894 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -90,7 +90,13 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - list, err := h.productService.Find(id_lang, paging, filters) + 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))) + } + + list, err := h.productService.Find(id_lang, userID, paging, filters) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/model/product.go b/app/model/product.go index fa47790..06b599e 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -70,6 +70,7 @@ type ProductInList struct { Reference string `gorm:"column:reference" json:"reference"` VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"` Quantity int64 `gorm:"column:quantity" json:"quantity"` + IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"` } type ProductFilters struct { diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 341b348..9e0ab1c 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -16,7 +16,7 @@ import ( type UIProductsRepo interface { GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) - Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) + Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) } type ProductsRepo struct{} @@ -37,7 +37,6 @@ func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_custo return nil, err } - // Optional: validate it's valid JSON if !json.Valid([]byte(productStr)) { return nil, fmt.Errorf("invalid json returned from stored procedure") } @@ -46,37 +45,60 @@ func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_custo return &raw, nil } -func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { +func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { var list []model.ProductInList var total int64 query := db.Get(). Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). Select(` - ps.id_product AS product_id, - pl.name AS name, - pl.link_rewrite AS link_rewrite, - CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, - cl.name AS category_name, - p.reference AS reference, - COALESCE(v.variants_number, 0) AS variants_number, - sa.quantity AS quantity - `, config.Get().Image.ImagePrefix). + ps.id_product AS product_id, + pl.name AS name, + pl.link_rewrite AS link_rewrite, + CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, + cl.name AS category_name, + p.reference AS reference, + COALESCE(v.variants_number, 0) AS variants_number, + sa.quantity AS quantity, + COALESCE(f.is_favorite, 0) AS is_favorite + `, config.Get().Image.ImagePrefix). Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product"). Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang). Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1"). Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang). Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). + Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product"). Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). Where("ps.active = ?", 1). Group("ps.id_product"). - Clauses(exclause.With{CTEs: []exclause.CTE{ - { - Name: "variants", - Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")}, + Clauses(exclause.With{ + CTEs: []exclause.CTE{ + { + Name: "variants", + Subquery: exclause.Subquery{ + DB: db.Get(). + Model(&dbmodel.PsProductAttributeShop{}). + Select("id_product", "COUNT(*) AS variants_number"). + Group("id_product"), + }, + }, + + { + Name: "favorites", + Subquery: exclause.Subquery{ + DB: db.Get(). + Table("b2b_favorites"). + Select(` + product_id AS id_product, + COUNT(*) > 0 AS is_favorite + `). + Where("user_id = ?", userID). + Group("product_id"), + }, + }, }, - }}). + }). Order("ps.id_product DESC") // Apply all filters diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index 1a1620e..f5d7ca1 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -29,6 +29,6 @@ func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_ return products, nil } -func (s *ProductService) Find(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { - return s.productsRepo.Find(id_lang, p, filters) +func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { + return s.productsRepo.Find(id_lang, userID, p, filters) } diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index a82cea5..ba4469a 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -151,6 +151,16 @@ CREATE TABLE IF NOT EXISTS b2b_carts_products ( CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id); +-- favorites +CREATE TABLE IF NOT EXISTS b2b_favorites ( + user_id BIGINT UNSIGNED NOT NULL, + product_id INT UNSIGNED NOT NULL, + PRIMARY KEY (user_id, product_id), + CONSTRAINT fk_favorites_customer FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_favorites_product FOREIGN KEY (product_id) REFERENCES ps_product(id_product) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + + -- refresh_tokens CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, diff --git a/i18n/migrations/20260319163200_procedures.sql b/i18n/migrations/20260319163200_procedures.sql index 8f7d5ab..743ca43 100644 --- a/i18n/migrations/20260319163200_procedures.sql +++ b/i18n/migrations/20260319163200_procedures.sql @@ -132,6 +132,12 @@ JSON_OBJECT( m.name, 'category', cl.name, + /* ================= FAVORITE ================= */ + 'is_favorite', + EXISTS( + SELECT 1 FROM b2b_favorites f + WHERE f.user_id = p_id_customer AND f.product_id = p_id_product + ), /* ================= IMAGE ================= */ 'cover_image', JSON_OBJECT( From f1f5daa82b3753606e17698eb38a28be652453ea Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 9 Apr 2026 14:53:56 +0200 Subject: [PATCH 21/25] and add filtering by is_favorite --- app/delivery/web/api/restricted/product.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index 2c9d894..5ab5b36 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -112,4 +112,5 @@ var columnMappingListProducts map[string]string = map[string]string{ "category_name": "cl.name", "category_id": "cp.id_category", "quantity": "sa.quantity", + "is_favorite": "ps.is_favorite", } From 0a5ce5d9c2e32a1c3f0962d7428d9dee01c2d4a4 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 09:13:13 +0200 Subject: [PATCH 22/25] ... --- app/delivery/web/api/restricted/product.go | 50 +++++++++++++++++++ app/model/product.go | 9 ++++ app/repos/productsRepo/productsRepo.go | 16 ++++++ app/service/productService/productService.go | 8 +++ app/utils/responseErrors/responseErrors.go | 16 +++++- bruno/api_v1/product/Add To Favorites.yml | 15 ++++++ .../api_v1/product/Remove Form Favorites.yml | 15 ++++++ go.mod | 6 +-- go.sum | 6 --- 9 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 bruno/api_v1/product/Add To Favorites.yml create mode 100644 bruno/api_v1/product/Remove Form Favorites.yml diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index 5ab5b36..096d5ec 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -34,6 +34,8 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/:id/:country_id/:quantity", handler.GetProductJson) r.Get("/list", handler.ListProducts) + r.Post("/favorite/:product_id", handler.AddToFavorites) + r.Delete("/favorite/:product_id", handler.RemoveFromFavorites) return r } @@ -114,3 +116,51 @@ var columnMappingListProducts map[string]string = map[string]string{ "quantity": "sa.quantity", "is_favorite": "ps.is_favorite", } + +func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error { + productIDStr := c.Params("product_id") + + productID, err := strconv.Atoi(productIDStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + 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))) + } + + err = h.productService.AddToFavorites(userID, uint(productID)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error { + productIDStr := c.Params("product_id") + + productID, err := strconv.Atoi(productIDStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + 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))) + } + + err = h.productService.RemoveFromFavorites(userID, uint(productID)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/model/product.go b/app/model/product.go index 06b599e..e862259 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -86,3 +86,12 @@ type ProductFilters struct { } type FeatVal = map[uint][]uint + +type B2bFavorite struct { + UserID uint `gorm:"column:user_id;not null;primaryKey" json:"user_id"` + ProductID uint `gorm:"column:product_id;not null;primaryKey" json:"product_id"` +} + +func (*B2bFavorite) TableName() string { + return "b2b_favorites" +} diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 9e0ab1c..7699c10 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -17,6 +17,8 @@ import ( type UIProductsRepo interface { GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) + AddToFavorites(userID uint, productID uint) error + RemoveFromFavorites(userID uint, productID uint) error } type ProductsRepo struct{} @@ -125,3 +127,17 @@ func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filter Count: uint(total), }, nil } + +func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error { + fav := model.B2bFavorite{ + UserID: userID, + ProductID: productID, + } + return db.Get().Create(&fav).Error +} + +func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error { + return db.Get(). + Where("user_id = ? AND product_id = ?", userID, productID). + Delete(&model.B2bFavorite{}).Error +} diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index f5d7ca1..03a7132 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -32,3 +32,11 @@ func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_ func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { return s.productsRepo.Find(id_lang, userID, p, filters) } + +func (s *ProductService) AddToFavorites(userID uint, productID uint) error { + return s.productsRepo.AddToFavorites(userID, productID) +} + +func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error { + return s.productsRepo.RemoveFromFavorites(userID, productID) +} diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 28802e1..6b3c548 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -49,8 +49,11 @@ var ( ErrAIResponseFail = errors.New("AI responded with failure") ErrAIBadOutput = errors.New("AI response does not obey the format") - // Typed errors for product list handler - ErrBadPaging = errors.New("bad or missing paging attribute value in header") + // Typed errors for product handler + ErrBadPaging = errors.New("bad or missing paging attribute value in header") + ErrProductNotFound = errors.New("product with provided id does not exist") + ErrAlreadyInFavorites = errors.New("the product already is in your favorites") + ErrNotInFavorites = errors.New("the product already is not in your favorites") // Typed errors for menu handler ErrNoRootFound = errors.New("no root found in categories table") @@ -170,6 +173,12 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrBadPaging): return i18n.T_(c, "error.err_bad_paging") + case errors.Is(err, ErrProductNotFound): + return i18n.T_(c, "error.err_product_not_found") + case errors.Is(err, ErrAlreadyInFavorites): + return i18n.T_(c, "error.err_already_in_favorites") + case errors.Is(err, ErrNotInFavorites): + return i18n.T_(c, "error.err_already_not_in_favorites") case errors.Is(err, ErrNoRootFound): return i18n.T_(c, "error.err_no_root_found") @@ -249,6 +258,9 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrBadPaging), + errors.Is(err, ErrProductNotFound), + errors.Is(err, ErrAlreadyInFavorites), + errors.Is(err, ErrNotInFavorites), errors.Is(err, ErrNoRootFound), errors.Is(err, ErrCircularDependency), errors.Is(err, ErrStartCategoryNotFound), diff --git a/bruno/api_v1/product/Add To Favorites.yml b/bruno/api_v1/product/Add To Favorites.yml new file mode 100644 index 0000000..71b3d9a --- /dev/null +++ b/bruno/api_v1/product/Add To Favorites.yml @@ -0,0 +1,15 @@ +info: + name: Add To Favorites + type: http + seq: 3 + +http: + method: POST + url: "{{bas_url}}/restricted/product/favorite/51" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/api_v1/product/Remove Form Favorites.yml b/bruno/api_v1/product/Remove Form Favorites.yml new file mode 100644 index 0000000..a76feb6 --- /dev/null +++ b/bruno/api_v1/product/Remove Form Favorites.yml @@ -0,0 +1,15 @@ +info: + name: Remove Form Favorites + type: http + seq: 4 + +http: + method: DELETE + url: "{{bas_url}}/restricted/product/favorite/1" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/go.mod b/go.mod index 1c184da..6141322 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,6 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -100,10 +98,10 @@ require ( github.com/valyala/fasthttp v1.69.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xyproto/randomstring v1.2.0 // indirect - golang.org/x/net v0.52.0 + golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/text v0.35.0 gopkg.in/warnings.v0 v0.1.2 // indirect gorm.io/driver/mysql v1.6.0 ) diff --git a/go.sum b/go.sum index 81fe849..d208fb1 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= -github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= @@ -136,8 +134,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -158,8 +154,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= From f7f56c29284bb8d1c6344cdb6ec3631ba9fb5b77 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 09:33:44 +0200 Subject: [PATCH 23/25] catching errors --- app/repos/productsRepo/productsRepo.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 7699c10..010fc2c 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -10,6 +10,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_marek/gormcol" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" ) @@ -129,6 +130,29 @@ func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filter } func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error { + var count int64 + err := db.Get(). + Table(dbmodel.TableNamePsProduct). + Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID). + Count(&count).Error + if err != nil { + return err + } + if count == 0 { + return responseErrors.ErrProductNotFound + } + + err = db.Get(). + Table("b2b_favorites"). + Where("user_id = ? AND product_id = ?", userID, productID). + Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return responseErrors.ErrAlreadyInFavorites + } + fav := model.B2bFavorite{ UserID: userID, ProductID: productID, From 61ccd32c4a822a19c936851b8e630a60841d2dfc Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 09:43:49 +0200 Subject: [PATCH 24/25] catching errors again --- app/repos/productsRepo/productsRepo.go | 44 +++++++++---------- app/service/productService/productService.go | 33 ++++++++++++++ bruno/api_v1/product/Add To Favorites.yml | 2 +- .../api_v1/product/Remove Form Favorites.yml | 2 +- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 010fc2c..da6409b 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -10,7 +10,6 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" - "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_marek/gormcol" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" ) @@ -20,6 +19,8 @@ type UIProductsRepo interface { Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) AddToFavorites(userID uint, productID uint) error RemoveFromFavorites(userID uint, productID uint) error + ExistsInFavorites(userID uint, productID uint) (int64, error) + ProductInDatabase(productID uint) (int64, error) } type ProductsRepo struct{} @@ -130,29 +131,6 @@ func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filter } func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error { - var count int64 - err := db.Get(). - Table(dbmodel.TableNamePsProduct). - Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID). - Count(&count).Error - if err != nil { - return err - } - if count == 0 { - return responseErrors.ErrProductNotFound - } - - err = db.Get(). - Table("b2b_favorites"). - Where("user_id = ? AND product_id = ?", userID, productID). - Count(&count).Error - if err != nil { - return err - } - if count > 0 { - return responseErrors.ErrAlreadyInFavorites - } - fav := model.B2bFavorite{ UserID: userID, ProductID: productID, @@ -165,3 +143,21 @@ func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error Where("user_id = ? AND product_id = ?", userID, productID). Delete(&model.B2bFavorite{}).Error } + +func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (int64, error) { + var count int64 + err := db.Get(). + Table("b2b_favorites"). + Where("user_id = ? AND product_id = ?", userID, productID). + Count(&count).Error + return count, err +} + +func (repo *ProductsRepo) ProductInDatabase(productID uint) (int64, error) { + var count int64 + err := db.Get(). + Table(dbmodel.TableNamePsProduct). + Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID). + Count(&count).Error + return count, err +} diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index 03a7132..ae3ddd6 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -8,6 +8,7 @@ import ( constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" ) type ProductService struct { @@ -34,9 +35,41 @@ func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filt } func (s *ProductService) AddToFavorites(userID uint, productID uint) error { + count, err := s.productsRepo.ProductInDatabase(productID) + if err != nil { + return err + } + if count <= 0 { + return responseErrors.ErrProductNotFound + } + + count, err = s.productsRepo.ExistsInFavorites(userID, productID) + if err != nil { + return err + } + if count >= 1 { + return responseErrors.ErrAlreadyInFavorites + } + return s.productsRepo.AddToFavorites(userID, productID) } func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error { + count, err := s.productsRepo.ProductInDatabase(productID) + if err != nil { + return err + } + if count <= 0 { + return responseErrors.ErrProductNotFound + } + + count, err = s.productsRepo.ExistsInFavorites(userID, productID) + if err != nil { + return err + } + if count <= 0 { + return responseErrors.ErrNotInFavorites + } + return s.productsRepo.RemoveFromFavorites(userID, productID) } diff --git a/bruno/api_v1/product/Add To Favorites.yml b/bruno/api_v1/product/Add To Favorites.yml index 71b3d9a..29a660d 100644 --- a/bruno/api_v1/product/Add To Favorites.yml +++ b/bruno/api_v1/product/Add To Favorites.yml @@ -5,7 +5,7 @@ info: http: method: POST - url: "{{bas_url}}/restricted/product/favorite/51" + url: "{{bas_url}}/restricted/product/favorite/53" auth: inherit settings: diff --git a/bruno/api_v1/product/Remove Form Favorites.yml b/bruno/api_v1/product/Remove Form Favorites.yml index a76feb6..2b388c2 100644 --- a/bruno/api_v1/product/Remove Form Favorites.yml +++ b/bruno/api_v1/product/Remove Form Favorites.yml @@ -5,7 +5,7 @@ info: http: method: DELETE - url: "{{bas_url}}/restricted/product/favorite/1" + url: "{{bas_url}}/restricted/product/favorite/51" auth: inherit settings: From c5832c0cf587addbcc4c6532cbd744509cefbe38 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 09:57:07 +0200 Subject: [PATCH 25/25] minor change --- app/repos/productsRepo/productsRepo.go | 12 ++++++------ app/service/productService/productService.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index da6409b..4450b52 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -19,8 +19,8 @@ type UIProductsRepo interface { Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) AddToFavorites(userID uint, productID uint) error RemoveFromFavorites(userID uint, productID uint) error - ExistsInFavorites(userID uint, productID uint) (int64, error) - ProductInDatabase(productID uint) (int64, error) + ExistsInFavorites(userID uint, productID uint) (bool, error) + ProductInDatabase(productID uint) (bool, error) } type ProductsRepo struct{} @@ -144,20 +144,20 @@ func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error Delete(&model.B2bFavorite{}).Error } -func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (int64, error) { +func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (bool, error) { var count int64 err := db.Get(). Table("b2b_favorites"). Where("user_id = ? AND product_id = ?", userID, productID). Count(&count).Error - return count, err + return count >= 1, err } -func (repo *ProductsRepo) ProductInDatabase(productID uint) (int64, error) { +func (repo *ProductsRepo) ProductInDatabase(productID uint) (bool, error) { var count int64 err := db.Get(). Table(dbmodel.TableNamePsProduct). Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID). Count(&count).Error - return count, err + return count >= 1, err } diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index ae3ddd6..de6d70e 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -35,19 +35,19 @@ func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filt } func (s *ProductService) AddToFavorites(userID uint, productID uint) error { - count, err := s.productsRepo.ProductInDatabase(productID) + exists, err := s.productsRepo.ProductInDatabase(productID) if err != nil { return err } - if count <= 0 { + if !exists { return responseErrors.ErrProductNotFound } - count, err = s.productsRepo.ExistsInFavorites(userID, productID) + exists, err = s.productsRepo.ExistsInFavorites(userID, productID) if err != nil { return err } - if count >= 1 { + if exists { return responseErrors.ErrAlreadyInFavorites } @@ -55,19 +55,19 @@ func (s *ProductService) AddToFavorites(userID uint, productID uint) error { } func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error { - count, err := s.productsRepo.ProductInDatabase(productID) + exists, err := s.productsRepo.ProductInDatabase(productID) if err != nil { return err } - if count <= 0 { + if !exists { return responseErrors.ErrProductNotFound } - count, err = s.productsRepo.ExistsInFavorites(userID, productID) + exists, err = s.productsRepo.ExistsInFavorites(userID, productID) if err != nil { return err } - if count <= 0 { + if !exists { return responseErrors.ErrNotInFavorites }