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/.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/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/middleware/auth.go b/app/delivery/middleware/auth.go
index 312cbe5..756e79f 100644
--- a/app/delivery/middleware/auth.go
+++ b/app/delivery/middleware/auth.go
@@ -1,13 +1,16 @@
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"
"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 +118,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 model.CustomerRole(userSession.RoleName) != model.RoleAdmin {
+ if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required",
})
@@ -139,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/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/api/restricted/product.go b/app/delivery/web/api/restricted/product.go
index 7eb11cf..0db9699 100644
--- a/app/delivery/web/api/restricted/product.go
+++ b/app/delivery/web/api/restricted/product.go
@@ -36,6 +36,8 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
r.Get("/list", handler.ListProducts)
r.Get("/list-variants/:product_id", handler.ListProductVariants)
+ r.Post("/favorite/:product_id", handler.AddToFavorites)
+ r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
return r
}
@@ -92,7 +94,7 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
- list, err := h.productService.Find(customer.LangID, paging, filters, customer, 1, constdata.SHOP_ID)
+ list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, 1, constdata.SHOP_ID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -108,6 +110,55 @@ 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",
+}
+
+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)))
}
func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go
index ea6f906..3dc16bd 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 || model.CustomerRole(userRole.Name) != 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 || model.CustomerRole(userRole.Name) != 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..843c956 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 || model.CustomerRole(userRole.Name) != 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
new file mode 100644
index 0000000..910aae1
--- /dev/null
+++ b/app/delivery/web/api/restricted/storage.go
@@ -0,0 +1,100 @@
+package restricted
+
+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"
+ "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 all users
+ r.Get("/list-content/*", handler.ListContent)
+ r.Get("/download-file/*", handler.DownloadFile)
+
+ // for admins only
+ r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken)
+
+ 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
+ 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)))
+ }
+
+ entries_in_list, err := h.storageService.ListContent(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(entries_in_list, 0, i18n.T_(c, response.Message_OK)))
+}
+
+func (h *StorageHandler) DownloadFile(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)))
+ }
+
+ 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)))
+ }
+
+ 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) 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 || model.CustomerRole(userRole.Name) != 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)))
+ }
+
+ 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 d3a0cc3..29fcd71 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)
@@ -119,8 +134,18 @@ func (s *Server) Setup() error {
specificPrice := s.restricted.Group("/specific-price")
restricted.SpecificPriceHandlerRoutes(specificPrice)
+ // 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")
+ restricted.StorageHandlerRoutes(restrictedStorage)
+ webdav.StorageHandlerRoutes(webdavStorage)
restricted.CurrencyHandlerRoutes(s.restricted)
+
s.api.All("*", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
})
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/model/customer.go b/app/model/customer.go
index e05cc57..cc4b9f1 100644
--- a/app/model/customer.go
+++ b/app/model/customer.go
@@ -25,6 +25,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/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/model/product.go b/app/model/product.go
index 5ead295..f2bb5f9 100644
--- a/app/model/product.go
+++ b/app/model/product.go
@@ -72,6 +72,7 @@ type ProductInList struct {
Quantity int64 `gorm:"column:quantity" json:"quantity"`
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
+ IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
}
type ProductFilters struct {
@@ -87,3 +88,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/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/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go
index 2255832..5f0e1a9 100644
--- a/app/repos/productsRepo/productsRepo.go
+++ b/app/repos/productsRepo/productsRepo.go
@@ -16,11 +16,15 @@ 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 uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error)
GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error)
GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error)
GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error)
GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error)
+ AddToFavorites(userID uint, productID uint) error
+ RemoveFromFavorites(userID uint, productID uint) error
+ ExistsInFavorites(userID uint, productID uint) (bool, error)
+ ProductInDatabase(productID uint) (bool, error)
}
type ProductsRepo struct{}
@@ -100,34 +104,57 @@ func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_c
return results, err
}
-func (repo *ProductsRepo) Find(langID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
+func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
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 = ?", langID).
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 = ?", langID).
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")
query = query.Scopes(filt.All()...)
@@ -189,3 +216,35 @@ func (repo *ProductsRepo) PopulateProductPrice(product *model.ProductInList, tar
return 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
+}
+
+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 >= 1, err
+}
+
+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 >= 1, err
+}
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..69dc906
--- /dev/null
+++ b/app/repos/storageRepo/storageRepo.go
@@ -0,0 +1,178 @@
+package storageRepo
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "time"
+
+ "git.ma-al.com/goc_daniel/b2b/app/db"
+ "git.ma-al.com/goc_daniel/b2b/app/model"
+)
+
+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
+}
+
+type StorageRepo struct{}
+
+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)
+}
+
+func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) {
+ entries, err := os.ReadDir(abs_path)
+ 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(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 {
+ info, err := os.Stat(src_abs_path)
+ if err != nil {
+ return err
+ }
+
+ 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 f.Close()
+
+ 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
+ }
+
+ entries, err := os.ReadDir(src_abs_path)
+ if err != nil {
+ return err
+ }
+
+ for _, entry := range entries {
+
+ entity_src_path := filepath.Join(src_abs_path, entry.Name())
+ entity_dst_Path := filepath.Join(dest_abs_path, entry.Name())
+
+ if entry.IsDir() {
+ err = r.copyDir(entity_src_path, entity_dst_Path)
+ if err != nil {
+ return err
+ }
+
+ } else {
+ err = r.copyFile(entity_src_path, entity_dst_Path)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
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/service/authService/auth.go b/app/service/authService/auth.go
index 8916225..8f8c63a 100644
--- a/app/service/authService/auth.go
+++ b/app/service/authService/auth.go
@@ -458,6 +458,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/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/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/productService/productService.go b/app/service/productService/productService.go
index d16c075..3296d88 100644
--- a/app/service/productService/productService.go
+++ b/app/service/productService/productService.go
@@ -9,6 +9,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"
"git.ma-al.com/goc_daniel/b2b/app/view"
)
@@ -62,6 +63,7 @@ func (s *ProductService) Get(
func (s *ProductService) Find(
idLang uint,
+ userID uint,
p find.Paging,
filters *filters.FiltersList,
customer *model.Customer,
@@ -73,7 +75,7 @@ func (s *ProductService) Find(
return nil, errors.New("customer is nil or missing fields")
}
- found, err := s.productsRepo.Find(idLang, p, filters)
+ found, err := s.productsRepo.Find(idLang, userID, p, filters)
if err != nil {
return nil, err
}
@@ -122,4 +124,45 @@ func (s *ProductService) GetProductAttributes(
}
return variants, nil
+
+}
+
+func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
+ exists, err := s.productsRepo.ProductInDatabase(productID)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ return responseErrors.ErrProductNotFound
+ }
+
+ exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return responseErrors.ErrAlreadyInFavorites
+ }
+
+ return s.productsRepo.AddToFavorites(userID, productID)
+}
+
+func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error {
+ exists, err := s.productsRepo.ProductInDatabase(productID)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ return responseErrors.ErrProductNotFound
+ }
+
+ exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ return responseErrors.ErrNotInFavorites
+ }
+
+ return s.productsRepo.RemoveFromFavorites(userID, productID)
}
diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go
new file mode 100644
index 0000000..f5ffba8
--- /dev/null
+++ b/app/service/storageService/storageService.go
@@ -0,0 +1,283 @@
+package storageService
+
+import (
+ "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"
+)
+
+type StorageService struct {
+ storageRepo storageRepo.UIStorageRepo
+}
+
+func New() *StorageService {
+ return &StorageService{
+ storageRepo: storageRepo.New(),
+ }
+}
+
+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
+ }
+
+ 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) {
+ info, err := s.storageRepo.EntryInfo(abs_path)
+ if err != nil || info.IsDir() {
+ return nil, "", 0, responseErrors.ErrFileDoesNotExist
+ }
+
+ f, err := s.storageRepo.OpenFile(abs_path)
+ if err != nil {
+ return nil, "", 0, err
+ }
+
+ 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 := `` +
+ `
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.
\nThese 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.
\nSupport 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.
\nCustomize 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\nThis 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
In rehabilitation and physical therapy
\nDuring traditional and sports massages
\nIn corrective gymnastics (especially for children)
\nFor the alleviation of injuries to various body parts
\nFor comfortable support of the knees, ankles, and patient's head
\nIn exercises developing children's motor skills
\nIn beauty salons and SPA centers
\nIn children's playrooms
\nCover Material: PVC-coated material specifically designated for medical devices, making it extremely easy to clean and disinfect:
\nREACH Regulation Compliant
\nSTANDARD 100 by OEKO-TEX ® Certified
\nPhthalate-Free (Non-Toxic PVC)
\nFlame Retardant (Fire Resistant)
\nResistant to Physiological Fluids (blood, urine, sweat) and Alcohol (Ideal for Clinics)
\nUV Resistant (Suitable for Outdoor Use)
\nScratch Resistant
\nOil Resistant
\n








Filling: Medium-firm polyurethane foam with enhanced resistance to deformation.
\nHolds a HYGIENE CERTIFICATE
\nCertified with STANDARD 100 by OEKO-TEX® – Product Class I.
\nManufactured using high-quality raw materials that do not contribute to ozone depletion.
\n

