From b2acb8c922c53ce69f8dd02c00ceef65df536b8c Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 1 Apr 2026 13:30:54 +0200 Subject: [PATCH 01/11] 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 02/11] 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

\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

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

\n

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

\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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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 7eee0bd03229e511f563098fbcd0d1e9524afe75 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 8 Apr 2026 13:09:19 +0200 Subject: [PATCH 09/11] 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 10/11] 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 11/11] 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))) }