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