From 7eee0bd03229e511f563098fbcd0d1e9524afe75 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Wed, 8 Apr 2026 13:09:19 +0200 Subject: [PATCH] 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