product-procedures #59

Merged
goc_daniel merged 4 commits from product-procedures into main 2026-04-13 13:32:28 +00:00
87 changed files with 2227 additions and 94 deletions
Showing only changes of commit 2e61cde742 - Show all commits

4
.env
View File

@@ -48,6 +48,10 @@ EMAIL_FROM=test@ma-al.com
EMAIL_FROM_NAME=Gitea Manager EMAIL_FROM_NAME=Gitea Manager
EMAIL_ADMIN=goc_marek@ma-al.pl EMAIL_ADMIN=goc_marek@ma-al.pl
# STORAGE
STORAGE_ROOT=./storage
I18N_LANGS=en,pl,cs I18N_LANGS=en,pl,cs
PDF_SERVER_URL=http://localhost:8000 PDF_SERVER_URL=http://localhost:8000

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ i18n/*.json
*_templ.go *_templ.go
tmp/main tmp/main
test.go test.go
storage/*
!storage/.gitkeep

View File

@@ -2,8 +2,10 @@ package config
import ( import (
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -24,7 +26,8 @@ type Config struct {
GoogleTranslate GoogleTranslateConfig GoogleTranslate GoogleTranslateConfig
Image ImageConfig Image ImageConfig
Cors CorsConfig Cors CorsConfig
MailiSearch MeiliSearchConfig MeiliSearch MeiliSearchConfig
Storage StorageConfig
} }
type I18n struct { type I18n struct {
@@ -95,6 +98,10 @@ type EmailConfig struct {
Enabled bool `env:"EMAIL_ENABLED,false"` Enabled bool `env:"EMAIL_ENABLED,false"`
} }
type StorageConfig struct {
RootFolder string `env:"STORAGE_ROOT"`
}
type PdfPrinter struct { type PdfPrinter struct {
ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"` ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"`
} }
@@ -155,7 +162,7 @@ func load() *Config {
err = loadEnv(&cfg.OAuth.Google) err = loadEnv(&cfg.OAuth.Google)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for outh google : ", err.Error(), "") slog.Error("not possible to load env variables for oauth google : ", err.Error(), "")
} }
err = loadEnv(&cfg.App) err = loadEnv(&cfg.App)
@@ -170,12 +177,12 @@ func load() *Config {
err = loadEnv(&cfg.I18n) err = loadEnv(&cfg.I18n)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for email : ", err.Error(), "") slog.Error("not possible to load env variables for i18n : ", err.Error(), "")
} }
err = loadEnv(&cfg.Pdf) err = loadEnv(&cfg.Pdf)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for email : ", err.Error(), "") slog.Error("not possible to load env variables for pdf : ", err.Error(), "")
} }
err = loadEnv(&cfg.GoogleTranslate) err = loadEnv(&cfg.GoogleTranslate)
@@ -185,19 +192,25 @@ func load() *Config {
err = loadEnv(&cfg.Image) err = loadEnv(&cfg.Image)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for google translate : ", err.Error(), "") slog.Error("not possible to load env variables for image : ", err.Error(), "")
} }
err = loadEnv(&cfg.Cors) err = loadEnv(&cfg.Cors)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for google translate : ", err.Error(), "") slog.Error("not possible to load env variables for cors : ", err.Error(), "")
} }
err = loadEnv(&cfg.MailiSearch) err = loadEnv(&cfg.MeiliSearch)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for google translate : ", err.Error(), "") slog.Error("not possible to load env variables for meili search : ", err.Error(), "")
} }
err = loadEnv(&cfg.Storage)
if err != nil {
slog.Error("not possible to load env variables for storage : ", err.Error(), "")
}
cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder)
return cfg return cfg
} }
@@ -308,6 +321,22 @@ func setValue(field reflect.Value, val string, key string) error {
return nil return nil
} }
func ResolveRelativePath(relativePath string) string {
// get working directory (where program was started)
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
// convert to absolute path
absPath := relativePath
if !filepath.IsAbs(absPath) {
absPath = filepath.Join(wd, absPath)
}
return filepath.Clean(absPath)
}
func parseEnvTag(tag string) (key string, def *string) { func parseEnvTag(tag string) (key string, def *string) {
if tag == "" { if tag == "" {
return "", nil return "", nil

View File

@@ -1,13 +1,16 @@
package middleware package middleware
import ( import (
"encoding/base64"
"strconv" "strconv"
"strings" "strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/config" "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/model"
"git.ma-al.com/goc_daniel/b2b/app/service/authService" "git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -115,21 +118,14 @@ func AuthMiddleware() fiber.Handler {
// RequireAdmin creates admin-only middleware // RequireAdmin creates admin-only middleware
func RequireAdmin() fiber.Handler { func RequireAdmin() fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
user := c.Locals("user") originalUserRole, ok := localeExtractor.GetOriginalUserRole(c)
if user == nil { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated", "error": "not authenticated",
}) })
} }
userSession, ok := user.(*model.UserSession) if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
if !ok {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "invalid user session",
})
}
if model.CustomerRole(userSession.RoleName) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required", "error": "admin access required",
}) })
@@ -139,6 +135,72 @@ func RequireAdmin() fiber.Handler {
} }
} }
// Webdav
func Webdav() fiber.Handler {
authService := authService.NewAuthService()
return func(c fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "authorization token required",
})
}
if !strings.HasPrefix(authHeader, "Basic ") {
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid authorization token",
})
}
encoded := strings.TrimPrefix(authHeader, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid authorization token",
})
}
credentials := strings.SplitN(string(decoded), ":", 2)
rawToken := ""
if len(credentials) == 1 {
rawToken = credentials[0]
} else if len(credentials) == 2 {
rawToken = credentials[1]
}
if len(rawToken) != constdata.NBYTES_IN_WEBDAV_TOKEN*2 {
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid authorization token",
})
}
// we identify user based on this token.
user, err := authService.GetUserByWebdavToken(rawToken)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "user not found",
})
}
if user.WebdavExpires != nil && user.WebdavExpires.Before(time.Now()) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid or expired token",
})
}
var userLocale model.UserLocale
userLocale.OriginalUser = user
userLocale.User = user
c.Locals(constdata.USER_LOCALE, &userLocale)
return c.Next()
}
}
// GetConfig returns the app config // GetConfig returns the app config
func GetConfig() *config.Config { func GetConfig() *config.Config {
return config.Get() return config.Get()

View File

@@ -0,0 +1,157 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type AddressesHandler struct {
addressesService *addressesService.AddressesService
}
func NewAddressesHandler() *AddressesHandler {
addressesService := addressesService.New()
return &AddressesHandler{
addressesService: addressesService,
}
}
func AddressesHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewAddressesHandler()
r.Get("/get-template", handler.GetTemplate)
r.Post("/add-new-address", handler.AddNewAddress)
r.Post("/modify-address", handler.ModifyAddress)
r.Get("/retrieve-addresses", handler.RetrieveAddressesInfo)
r.Delete("/delete-address", handler.DeleteAddress)
return r
}
func (h *AddressesHandler) GetTemplate(c fiber.Ctx) error {
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
template, err := h.addressesService.GetTemplate(uint(country_id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&template, 0, i18n.T_(c, response.Message_OK)))
}
func (h *AddressesHandler) AddNewAddress(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *AddressesHandler) ModifyAddress(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
address_id_attribute := c.Query("address_id")
address_id, err := strconv.Atoi(address_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK)))
}
func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
address_id_attribute := c.Query("address_id")
address_id, err := strconv.Atoi(address_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.addressesService.DeleteAddress(userID, uint(address_id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -36,6 +36,8 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/:id/:country_id/:quantity", handler.GetProductJson) r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
r.Get("/list", handler.ListProducts) r.Get("/list", handler.ListProducts)
r.Get("/list-variants/:product_id", handler.ListProductVariants) r.Get("/list-variants/:product_id", handler.ListProductVariants)
r.Post("/favorite/:product_id", handler.AddToFavorites)
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
return r return r
} }
@@ -92,7 +94,7 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
list, err := h.productService.Find(customer.LangID, paging, filters, customer, 1, constdata.SHOP_ID) list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, 1, constdata.SHOP_ID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -108,6 +110,55 @@ var columnMappingListProducts map[string]string = map[string]string{
"category_name": "cl.name", "category_name": "cl.name",
"category_id": "cp.id_category", "category_id": "cp.id_category",
"quantity": "sa.quantity", "quantity": "sa.quantity",
"is_favorite": "ps.is_favorite",
}
func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.AddToFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.RemoveFromFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
} }
func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error { func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {

View File

@@ -4,6 +4,7 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "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/service/productTranslationService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "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/localeExtractor"
@@ -79,6 +80,12 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { 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))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService" "git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService" searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -43,6 +44,12 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
err := h.meiliService.CreateIndex(id_lang) err := h.meiliService.CreateIndex(id_lang)
if err != nil { if err != nil {
fmt.Printf("CreateIndex error: %v\n", err) fmt.Printf("CreateIndex error: %v\n", err)

View File

@@ -0,0 +1,100 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type StorageHandler struct {
storageService *storageService.StorageService
config *config.Config
}
func NewStorageHandler() *StorageHandler {
return &StorageHandler{
storageService: storageService.New(),
config: config.Get(),
}
}
func StorageHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewStorageHandler()
// for all users
r.Get("/list-content/*", handler.ListContent)
r.Get("/download-file/*", handler.DownloadFile)
// for admins only
r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken)
return r
}
// accepted path looks like e.g. "/folder1/" or "folder1"
func (h *StorageHandler) ListContent(c fiber.Ctx) error {
// relative path defaults to root directory
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
entries_in_list, err := h.storageService.ListContent(abs_path)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(entries_in_list, 0, i18n.T_(c, response.Message_OK)))
}
func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
c.Attachment(filename)
c.Set("Content-Length", strconv.FormatInt(filesize, 10))
c.Set("Content-Type", "application/octet-stream")
return c.SendStream(f, int(filesize))
}
func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&new_token, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -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)
}

View File

@@ -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"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/public" "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/restricted"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/webdav"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/general" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/general"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@@ -25,6 +26,7 @@ import (
type Server struct { type Server struct {
app *fiber.App app *fiber.App
cfg *config.Config cfg *config.Config
webdav fiber.Router
api fiber.Router api fiber.Router
public fiber.Router public fiber.Router
restricted fiber.Router restricted fiber.Router
@@ -42,12 +44,23 @@ func (s *Server) Cfg() *config.Config {
// New creates a new server instance // New creates a new server instance
func New() *Server { func New() *Server {
return &Server{ var s Server
app: fiber.New(fiber.Config{
app :=
fiber.New(fiber.Config{
ErrorHandler: customErrorHandler, ErrorHandler: customErrorHandler,
}), BodyLimit: 50 * 1024 * 1024, // 50 MB
cfg: config.Get(), 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 // Setup configures the server with routes and middleware
@@ -76,6 +89,8 @@ func (s *Server) Setup() error {
s.public = s.api.Group("/public") s.public = s.api.Group("/public")
s.restricted = s.api.Group("/restricted") s.restricted = s.api.Group("/restricted")
s.restricted.Use(middleware.AuthMiddleware()) s.restricted.Use(middleware.AuthMiddleware())
s.webdav = s.api.Group("/webdav")
s.webdav.Use(middleware.Webdav())
// initialize language endpoints (general) // initialize language endpoints (general)
api.NewLangHandler().InitLanguage(s.api, s.cfg) api.NewLangHandler().InitLanguage(s.api, s.cfg)
@@ -119,8 +134,18 @@ func (s *Server) Setup() error {
specificPrice := s.restricted.Group("/specific-price") specificPrice := s.restricted.Group("/specific-price")
restricted.SpecificPriceHandlerRoutes(specificPrice) restricted.SpecificPriceHandlerRoutes(specificPrice)
// addresses (restricted)
addresses := s.restricted.Group("/addresses")
restricted.AddressesHandlerRoutes(addresses)
// storage (uses various authorization means)
restrictedStorage := s.restricted.Group("/storage")
webdavStorage := s.webdav.Group("/storage")
restricted.StorageHandlerRoutes(restrictedStorage)
webdav.StorageHandlerRoutes(webdavStorage)
restricted.CurrencyHandlerRoutes(s.restricted) restricted.CurrencyHandlerRoutes(s.restricted)
s.api.All("*", func(c fiber.Ctx) error { s.api.All("*", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound) return c.SendStatus(fiber.StatusNotFound)
}) })

79
app/model/address.go Normal file
View File

@@ -0,0 +1,79 @@
package model
type Address struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo string `gorm:"column:address_info;not null" json:"address_info"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
}
func (Address) TableName() string {
return "b2b_addresses"
}
type AddressUnparsed struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
}
type AddressField interface {
}
// Address template in Poland
type AddressPL struct {
PostalCode string `json:"postal_code"` // format: 00-000
City string `json:"city"` // e.g. Kraków
Voivodeship string `json:"voivodeship"` // e.g. małopolskie (optional but useful)
Street string `json:"street"` // e.g. Marszałkowska
BuildingNo string `json:"building_no"` // e.g. 10, 221B, 12A
ApartmentNo string `json:"apartment_no"` // e.g. 5, 12B
AddressLine2 string `json:"address_line2"` // optional extra info
Recipient string `json:"recipient"` // name/company
}
// Address template in Great Britain
type AddressGB struct {
PostalCode string `json:"postal_code"` // e.g. SW1A 1AA
PostTown string `json:"post_town"` // e.g. London
County string `json:"county"` // optional
Thoroughfare string `json:"thoroughfare"` // street name, e.g. Baker Street
BuildingNo string `json:"building_no"` // e.g. 221B
BuildingName string `json:"building_name"` // e.g. Flatiron House
SubBuilding string `json:"sub_building"` // e.g. Flat 5, Apt 2
AddressLine2 string `json:"address_line2"`
Recipient string `json:"recipient"`
}
// Address template in Czech Republic
type AddressCZ struct {
PostalCode string `json:"postal_code"` // usually 110 00 or 11000
City string `json:"city"` // e.g. Praha
Region string `json:"region"`
Street string `json:"street"` // may be omitted in some village-style addresses
HouseNumber string `json:"house_number"` // descriptive / conscription no.
OrientationNumber string `json:"orientation_number"` // optional, often after slash
AddressLine2 string `json:"address_line2"`
Recipient string `json:"recipient"`
}
// Address template in Germany
type AddressDE struct {
PostalCode string `json:"postal_code"` // e.g. 10115
City string `json:"city"` // e.g. Berlin
State string `json:"state"` // Bundesland, optional
Street string `json:"street"` // e.g. Unter den Linden
HouseNumber string `json:"house_number"` // e.g. 77, 12a
AddressLine2 string `json:"address_line2"` // extra details
Recipient string `json:"recipient"`
}

View File

@@ -25,6 +25,8 @@ type Customer struct {
EmailVerificationExpires *time.Time `json:"-"` EmailVerificationExpires *time.Time `json:"-"`
PasswordResetToken string `gorm:"size:255" json:"-"` PasswordResetToken string `gorm:"size:255" json:"-"`
PasswordResetExpires *time.Time `json:"-"` PasswordResetExpires *time.Time `json:"-"`
WebdavToken string `gorm:"size:255" json:"-"`
WebdavExpires *time.Time `json:"-"`
LastPasswordResetRequest *time.Time `json:"-"` LastPasswordResetRequest *time.Time `json:"-"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"` LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language

6
app/model/entry.go Normal file
View File

@@ -0,0 +1,6 @@
package model
type EntryInList struct {
Name string
IsFolder bool
}

View File

@@ -72,6 +72,7 @@ type ProductInList struct {
Quantity int64 `gorm:"column:quantity" json:"quantity"` Quantity int64 `gorm:"column:quantity" json:"quantity"`
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"` PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"` PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
} }
type ProductFilters struct { type ProductFilters struct {
@@ -87,3 +88,12 @@ type ProductFilters struct {
} }
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint
type B2bFavorite struct {
UserID uint `gorm:"column:user_id;not null;primaryKey" json:"user_id"`
ProductID uint `gorm:"column:product_id;not null;primaryKey" json:"product_id"`
}
func (*B2bFavorite) TableName() string {
return "b2b_favorites"
}

View File

@@ -0,0 +1,91 @@
package addressesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UIAddressesRepo interface {
UserHasAddress(user_id uint, address_id uint) (uint, error)
UserAddressesAmt(user_id uint) (uint, error)
AddNewAddress(user_id uint, address_info string, country_id uint) error
UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error
RetrieveAddresses(user_id uint) (*[]model.Address, error)
DeleteAddress(user_id uint, address_id uint) error
}
type AddressesRepo struct{}
func New() UIAddressesRepo {
return &AddressesRepo{}
}
func (repo *AddressesRepo) UserHasAddress(user_id uint, address_id uint) (uint, error) {
var amt uint
err := db.DB.
Table("b2b_addresses").
Select("COUNT(*) AS amt").
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
Scan(&amt).
Error
return amt, err
}
func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) {
var amt uint
err := db.DB.
Table("b2b_addresses").
Select("COUNT(*) AS amt").
Where("b2b_customer_id = ?", user_id).
Scan(&amt).
Error
return amt, err
}
func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error {
address := model.Address{
CustomerID: user_id,
AddressInfo: address_info,
CountryID: country_id,
}
return db.DB.
Create(&address).
Error
}
func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error {
address := model.Address{
ID: address_id,
CustomerID: user_id,
AddressInfo: address_info,
CountryID: country_id,
}
return db.DB.
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
Updates(&address).
Error
}
func (repo *AddressesRepo) RetrieveAddresses(user_id uint) (*[]model.Address, error) {
var addresses []model.Address
err := db.DB.
Where("b2b_customer_id = ?", user_id).
Find(&addresses).
Error
return &addresses, err
}
func (repo *AddressesRepo) DeleteAddress(user_id uint, address_id uint) error {
return db.DB.
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
Delete(&model.Address{}).
Error
}

View File

@@ -16,11 +16,15 @@ import (
type UIProductsRepo interface { type UIProductsRepo interface {
// GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) // GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) Find(id_lang uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error)
GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error) GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error)
GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error) GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error)
GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error) GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error)
GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error)
AddToFavorites(userID uint, productID uint) error
RemoveFromFavorites(userID uint, productID uint) error
ExistsInFavorites(userID uint, productID uint) (bool, error)
ProductInDatabase(productID uint) (bool, error)
} }
type ProductsRepo struct{} type ProductsRepo struct{}
@@ -100,7 +104,7 @@ func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_c
return results, err return results, err
} }
func (repo *ProductsRepo) Find(langID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) { func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
query := db.Get(). query := db.Get().
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(` Select(`
@@ -111,7 +115,8 @@ func (repo *ProductsRepo) Find(langID uint, p find.Paging, filt *filters.Filters
cl.name AS category_name, cl.name AS category_name,
p.reference AS reference, p.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number, COALESCE(v.variants_number, 0) AS variants_number,
sa.quantity AS quantity sa.quantity AS quantity,
COALESCE(f.is_favorite, 0) AS is_favorite
`, config.Get().Image.ImagePrefix). `, config.Get().Image.ImagePrefix).
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product"). Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID). Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID).
@@ -119,15 +124,37 @@ func (repo *ProductsRepo) Find(langID uint, p find.Paging, filt *filters.Filters
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", langID). Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", langID).
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product").
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). Joins("LEFT JOIN variants v ON v.id_product = ps.id_product").
Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product").
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0").
Where("ps.active = ?", 1). Where("ps.active = ?", 1).
Group("ps.id_product"). Group("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{ Clauses(exclause.With{
CTEs: []exclause.CTE{
{ {
Name: "variants", Name: "variants",
Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")}, Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductAttributeShop{}).
Select("id_product", "COUNT(*) AS variants_number").
Group("id_product"),
}, },
}}). },
{
Name: "favorites",
Subquery: exclause.Subquery{
DB: db.Get().
Table("b2b_favorites").
Select(`
product_id AS id_product,
COUNT(*) > 0 AS is_favorite
`).
Where("user_id = ?", userID).
Group("product_id"),
},
},
},
}).
Order("ps.id_product DESC") Order("ps.id_product DESC")
query = query.Scopes(filt.All()...) query = query.Scopes(filt.All()...)
@@ -189,3 +216,35 @@ func (repo *ProductsRepo) PopulateProductPrice(product *model.ProductInList, tar
return nil return nil
} }
func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {
fav := model.B2bFavorite{
UserID: userID,
ProductID: productID,
}
return db.Get().Create(&fav).Error
}
func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error {
return db.Get().
Where("user_id = ? AND product_id = ?", userID, productID).
Delete(&model.B2bFavorite{}).Error
}
func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (bool, error) {
var count int64
err := db.Get().
Table("b2b_favorites").
Where("user_id = ? AND product_id = ?", userID, productID).
Count(&count).Error
return count >= 1, err
}
func (repo *ProductsRepo) ProductInDatabase(productID uint) (bool, error) {
var count int64
err := db.Get().
Table(dbmodel.TableNamePsProduct).
Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID).
Count(&count).Error
return count >= 1, err
}

View File

@@ -32,12 +32,12 @@ func New() UISearchRepo {
} }
func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) { func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) {
url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MailiSearch.ServerURL, index) url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MeiliSearch.ServerURL, index)
return r.doRequest(http.MethodPost, url, body) return r.doRequest(http.MethodPost, url, body)
} }
func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) { func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) {
url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MailiSearch.ServerURL, index) url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MeiliSearch.ServerURL, index)
return r.doRequest(http.MethodGet, url, nil) return r.doRequest(http.MethodGet, url, nil)
} }
@@ -55,8 +55,8 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
if r.cfg.MailiSearch.ApiKey != "" { if r.cfg.MeiliSearch.ApiKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey))
} }
client := &http.Client{} client := &http.Client{}

View File

@@ -0,0 +1,178 @@
package storageRepo
import (
"io"
"os"
"path/filepath"
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UIStorageRepo interface {
SaveWebdavToken(user_id uint, hash_token string, expires_at *time.Time) error
EntryInfo(abs_path string) (os.FileInfo, error)
ListContent(abs_path string) (*[]model.EntryInList, error)
OpenFile(abs_path string) (*os.File, error)
Put(abs_path string, src io.Reader) error
Delete(abs_path string) error
Mkcol(abs_path string) error
Move(src_abs_path string, dest_abs_path string) error
Copy(src_abs_path string, dest_abs_path string) error
}
type StorageRepo struct{}
func New() UIStorageRepo {
return &StorageRepo{}
}
func (r *StorageRepo) SaveWebdavToken(user_id uint, hash_token string, expires_at *time.Time) error {
return db.DB.
Table("b2b_customers").
Where("id = ?", user_id).
Updates(map[string]interface{}{
"webdav_token": hash_token,
"webdav_expires": expires_at,
}).
Error
}
func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) {
return os.Stat(abs_path)
}
func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) {
entries, err := os.ReadDir(abs_path)
if err != nil {
return nil, err
}
var entries_in_list []model.EntryInList
for _, entry := range entries {
var next_entry_in_list model.EntryInList
next_entry_in_list.Name = entry.Name()
next_entry_in_list.IsFolder = entry.IsDir()
entries_in_list = append(entries_in_list, next_entry_in_list)
}
return &entries_in_list, nil
}
func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) {
return os.Open(abs_path)
}
func (r *StorageRepo) Put(abs_path string, src io.Reader) error {
// Write to a temp file in the same directory, then atomically rename.
tmp, err := os.CreateTemp(filepath.Dir(abs_path), ".put-*")
if err != nil {
return err
}
tmp_name := tmp.Name()
cleanup_tmp := true
defer func() {
_ = tmp.Close()
if cleanup_tmp {
_ = os.Remove(tmp_name)
}
}()
_, err = io.Copy(tmp, src)
if err != nil {
return err
}
err = tmp.Sync()
if err != nil {
return err
}
err = tmp.Close()
if err != nil {
return err
}
err = os.Chmod(tmp_name, 0o644)
if err != nil {
return err
}
err = os.Rename(tmp_name, abs_path)
if err != nil {
return err
}
cleanup_tmp = false
return nil
}
func (r *StorageRepo) Delete(abs_path string) error {
return os.RemoveAll(abs_path)
}
func (r *StorageRepo) Mkcol(abs_path string) error {
return os.Mkdir(abs_path, 0755)
}
func (r *StorageRepo) Move(src_abs_path string, dest_abs_path string) error {
return os.Rename(src_abs_path, dest_abs_path)
}
func (r *StorageRepo) Copy(src_abs_path string, dest_abs_path string) error {
info, err := os.Stat(src_abs_path)
if err != nil {
return err
}
if info.IsDir() {
return r.copyDir(src_abs_path, dest_abs_path)
} else {
return r.copyFile(src_abs_path, dest_abs_path)
}
}
func (r *StorageRepo) copyFile(src_abs_path string, dest_abs_path string) error {
f, err := os.Open(src_abs_path)
if err != nil {
return err
}
defer f.Close()
err = r.Put(dest_abs_path, f)
return err
}
func (r *StorageRepo) copyDir(src_abs_path string, dest_abs_path string) error {
if err := os.Mkdir(dest_abs_path, 0755); err != nil {
return err
}
entries, err := os.ReadDir(src_abs_path)
if err != nil {
return err
}
for _, entry := range entries {
entity_src_path := filepath.Join(src_abs_path, entry.Name())
entity_dst_Path := filepath.Join(dest_abs_path, entry.Name())
if entry.IsDir() {
err = r.copyDir(entity_src_path, entity_dst_Path)
if err != nil {
return err
}
} else {
err = r.copyFile(entity_src_path, entity_dst_Path)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,152 @@
package addressesService
import (
"encoding/json"
"fmt"
"strings"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/addressesRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type AddressesService struct {
repo addressesRepo.UIAddressesRepo
}
func New() *AddressesService {
return &AddressesService{
repo: addressesRepo.New(),
}
}
func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) {
switch country_id {
case 1: // Poland
return model.AddressPL{}, nil
case 2: // Great Britain
return model.AddressGB{}, nil
case 3: // Czech Republic
return model.AddressCZ{}, nil
case 4: // Germany
return model.AddressDE{}, nil
default:
return nil, responseErrors.ErrInvalidCountryID
}
}
func (s *AddressesService) AddNewAddress(user_id uint, address_info string, country_id uint) error {
amt, err := s.repo.UserAddressesAmt(user_id)
if err != nil {
return err
} else if amt >= constdata.MAX_AMOUNT_OF_ADDRESSES_PER_USER {
return responseErrors.ErrMaxAmtOfAddressesReached
}
_, err = s.validateAddressJson(address_info, country_id)
if err != nil {
return err
}
return s.repo.AddNewAddress(user_id, address_info, country_id)
}
// country_id = 0 means that country_id remains unchanged
func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_info string, country_id uint) error {
amt, err := s.repo.UserHasAddress(user_id, address_id)
if err != nil {
return err
} else if amt != 1 {
return responseErrors.ErrUserHasNoSuchAddress
}
_, err = s.validateAddressJson(address_info, country_id)
if err != nil {
return err
}
return s.repo.UpdateAddress(user_id, address_id, address_info, country_id)
}
func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) {
parsed_addresses, err := s.repo.RetrieveAddresses(user_id)
if err != nil {
return nil, err
}
var unparsed_addresses []model.AddressUnparsed
for i := 0; i < len(*parsed_addresses); i++ {
var next_address model.AddressUnparsed
next_address.ID = (*parsed_addresses)[i].ID
next_address.CustomerID = (*parsed_addresses)[i].CustomerID
next_address.CountryID = (*parsed_addresses)[i].CountryID
next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID)
// log such errors
if err != nil {
fmt.Printf("err: %v\n", err)
}
unparsed_addresses = append(unparsed_addresses, next_address)
}
return &unparsed_addresses, nil
}
func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
amt, err := s.repo.UserHasAddress(user_id, address_id)
if err != nil {
return err
} else if amt != 1 {
return responseErrors.ErrUserHasNoSuchAddress
}
return s.repo.DeleteAddress(user_id, address_id)
}
// validateAddressJson makes sure that the info string represents a valid json of address in given country
func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) {
dec := json.NewDecoder(strings.NewReader(info))
dec.DisallowUnknownFields()
switch country_id {
case 1: // Poland
var address model.AddressPL
if err := dec.Decode(&address); err != nil {
return address, responseErrors.ErrInvalidAddressJSON
}
return address, nil
case 2: // Great Britain
var address model.AddressGB
if err := dec.Decode(&address); err != nil {
return address, responseErrors.ErrInvalidAddressJSON
}
return address, nil
case 3: // Czech Republic
var address model.AddressCZ
if err := dec.Decode(&address); err != nil {
return address, responseErrors.ErrInvalidAddressJSON
}
return address, nil
case 4: // Germany
var address model.AddressDE
if err := dec.Decode(&address); err != nil {
return address, responseErrors.ErrInvalidAddressJSON
}
return address, nil
default:
return nil, responseErrors.ErrInvalidCountryID
}
}

View File

@@ -458,6 +458,19 @@ func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) {
return &user, nil 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. // 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) { func (s *AuthService) createRefreshToken(userID uint) (string, error) {
// Generate 32 random bytes → 64-char hex string // Generate 32 random bytes → 64-char hex string

View File

@@ -10,6 +10,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/service/langsService"
"git.ma-al.com/goc_daniel/b2b/app/templ/emails" "git.ma-al.com/goc_daniel/b2b/app/templ/emails"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/view" "git.ma-al.com/goc_daniel/b2b/app/view"
) )
@@ -133,6 +134,6 @@ func (s *EmailService) passwordResetEmailTemplate(name, resetURL string, langID
// newUserAdminNotificationTemplate returns the HTML template for admin notification // newUserAdminNotificationTemplate returns the HTML template for admin notification
func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, baseURL string) string { func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, baseURL string) string {
buf := bytes.Buffer{} buf := bytes.Buffer{}
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: 2, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
return buf.String() return buf.String()
} }

View File

@@ -27,8 +27,8 @@ type MeiliService struct {
func New() *MeiliService { func New() *MeiliService {
client := meilisearch.New( client := meilisearch.New(
config.Get().MailiSearch.ServerURL, config.Get().MeiliSearch.ServerURL,
meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey), meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey),
) )
return &MeiliService{ return &MeiliService{

View File

@@ -9,6 +9,7 @@ import (
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view" "git.ma-al.com/goc_daniel/b2b/app/view"
) )
@@ -62,6 +63,7 @@ func (s *ProductService) Get(
func (s *ProductService) Find( func (s *ProductService) Find(
idLang uint, idLang uint,
userID uint,
p find.Paging, p find.Paging,
filters *filters.FiltersList, filters *filters.FiltersList,
customer *model.Customer, customer *model.Customer,
@@ -73,7 +75,7 @@ func (s *ProductService) Find(
return nil, errors.New("customer is nil or missing fields") return nil, errors.New("customer is nil or missing fields")
} }
found, err := s.productsRepo.Find(idLang, p, filters) found, err := s.productsRepo.Find(idLang, userID, p, filters)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -122,4 +124,45 @@ func (s *ProductService) GetProductAttributes(
} }
return variants, nil return variants, nil
}
func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if exists {
return responseErrors.ErrAlreadyInFavorites
}
return s.productsRepo.AddToFavorites(userID, productID)
}
func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrNotInFavorites
}
return s.productsRepo.RemoveFromFavorites(userID, productID)
} }

View File

@@ -0,0 +1,283 @@
package storageService
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type StorageService struct {
storageRepo storageRepo.UIStorageRepo
}
func New() *StorageService {
return &StorageService{
storageRepo: storageRepo.New(),
}
}
func (s *StorageService) EntryInfo(abs_path string) (os.FileInfo, error) {
return s.storageRepo.EntryInfo(abs_path)
}
func (s *StorageService) NewWebdavToken(user_id uint) (string, error) {
b := make([]byte, constdata.NBYTES_IN_WEBDAV_TOKEN)
_, err := rand.Read(b)
if err != nil {
return "", err
}
raw_token := hex.EncodeToString(b)
hash_token_bytes := sha256.Sum256([]byte(raw_token))
hash_token := hex.EncodeToString(hash_token_bytes[:])
expires_at := time.Now().Add(24 * time.Hour)
return raw_token, s.storageRepo.SaveWebdavToken(user_id, hash_token, &expires_at)
}
func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) {
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil || info.IsDir() {
return nil, "", 0, responseErrors.ErrFileDoesNotExist
}
f, err := s.storageRepo.OpenFile(abs_path)
if err != nil {
return nil, "", 0, err
}
return f, filepath.Base(abs_path), info.Size(), nil
}
func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) {
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil || !info.IsDir() {
return nil, responseErrors.ErrFolderDoesNotExist
}
entries_in_list, err := s.storageRepo.ListContent(abs_path)
return entries_in_list, err
}
func (s *StorageService) Propfind(root string, abs_path string, depth string) (string, error) {
href := href(root, abs_path)
max_depth := 0
switch depth {
case "0":
max_depth = 0
case "1":
max_depth = 1
case "infinity":
max_depth = 32
default:
max_depth = 0
}
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil {
return "", err
}
xml := `<?xml version="1.0" encoding="utf-8"?>` +
`<D:multistatus xmlns:D="DAV:">`
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 += `</D:multistatus>`
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 {
return s.storageRepo.Move(src_abs_path, dest_abs_path)
}
func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error {
return s.storageRepo.Copy(src_abs_path, dest_abs_path)
}
func buildFilePropResponse(href string, info os.FileInfo) string {
name := info.Name()
return "" +
"<D:response>" +
"<D:href>" + xmlEscape(href) + "</D:href>" +
"<D:propstat>" +
"<D:prop>" +
"<D:displayname>" + xmlEscape(name) + "</D:displayname>" +
"<D:getcontentlength>" + strconv.FormatInt(info.Size(), 10) + "</D:getcontentlength>" +
"<D:getlastmodified>" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "</D:getlastmodified>" +
"<D:resourcetype/>" +
"</D:prop>" +
"<D:status>HTTP/1.1 200 OK</D:status>" +
"</D:propstat>" +
"</D:response>"
}
func buildDirPropResponse(abs_path string, href string, info os.FileInfo, max_depth int) (string, error) {
name := info.Name()
xml := "" +
"<D:response>" +
"<D:href>" + xmlEscape(ensureTrailingSlash(href)) + "</D:href>" +
"<D:propstat>" +
"<D:prop>" +
"<D:displayname>" + xmlEscape(name) + "</D:displayname>" +
"<D:resourcetype><D:collection/></D:resourcetype>" +
"<D:getlastmodified>" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "</D:getlastmodified>" +
"</D:prop>" +
"<D:status>HTTP/1.1 200 OK</D:status>" +
"</D:propstat>" +
"</D:response>"
if max_depth <= 0 {
return xml, nil
}
entries, err := os.ReadDir(abs_path)
if err != nil {
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 ensureTrailingSlash(s string) string {
if s == "/" {
return s
}
if !strings.HasSuffix(s, "/") {
return s + "/"
}
return s
}
func xmlEscape(s string) string {
var b strings.Builder
xml.EscapeText(&b, []byte(s))
return b.String()
}
// 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 + "/"
}
rel = filepath.ToSlash(rel)
parts := strings.Split(rel, "/")
for i, p := range parts {
parts[i] = url.PathEscape(p)
}
return strings.TrimRight(constdata.WEBDAV_HREF_ROOT, "/") + "/" + strings.Join(parts, "/")
}
// AbsPath extracts an absolute path and validates it
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+"/") {
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
}
}

View File

@@ -4,6 +4,7 @@ package constdata
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
const SHOP_ID = 1 const SHOP_ID = 1
const SHOP_DEFAULT_LANGUAGE = 1 const SHOP_DEFAULT_LANGUAGE = 1
const ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1 // CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
const CATEGORY_TREE_ROOT_ID = 2 const CATEGORY_TREE_ROOT_ID = 2
@@ -11,8 +12,15 @@ const CATEGORY_TREE_ROOT_ID = 2
const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart" const DEFAULT_NEW_CART_NAME = "new cart"
const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10
const USER_LOCALE = "user" 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"
// Slug sanitization // Slug sanitization
const NON_ALNUM_REGEX = `[^a-z0-9]+` const NON_ALNUM_REGEX = `[^a-z0-9]+`
const MULTI_DASH_REGEX = `-+` const MULTI_DASH_REGEX = `-+`

View File

@@ -8,6 +8,7 @@ import (
"sync" "sync"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -177,7 +178,7 @@ func (s *TranslationsStore) ReloadTranslations(translations []model.Translation)
// T_ is meant to be used to translate error messages and other system communicates. // T_ is meant to be used to translate error messages and other system communicates.
func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string { func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string {
if langID, ok := c.Locals("langID").(uint); ok { if langID, ok := localeExtractor.GetLangID(c); ok {
parts := strings.Split(string(key), ".") parts := strings.Split(string(key), ".")
if len(parts) >= 2 { if len(parts) >= 2 {

View File

@@ -22,6 +22,14 @@ func GetUserID(c fiber.Ctx) (uint, bool) {
return user_locale.User.ID, true return user_locale.User.ID, true
} }
func GetOriginalUserRole(c fiber.Ctx) (model.Role, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil || user_locale.OriginalUser.Role == nil {
return model.Role{}, false
}
return *user_locale.OriginalUser.Role, true
}
func GetCustomer(c fiber.Ctx) (*model.Customer, bool) { func GetCustomer(c fiber.Ctx) (*model.Customer, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil { if !ok || user_locale.User == nil {

View File

@@ -17,6 +17,7 @@ var (
ErrInvalidToken = errors.New("invalid token") ErrInvalidToken = errors.New("invalid token")
ErrTokenExpired = errors.New("token has expired") ErrTokenExpired = errors.New("token has expired")
ErrTokenRequired = errors.New("token is required") ErrTokenRequired = errors.New("token is required")
ErrAdminAccessRequired = errors.New("admin access required")
// Typed errors for logging in and registering // Typed errors for logging in and registering
ErrInvalidCredentials = errors.New("invalid email or password") ErrInvalidCredentials = errors.New("invalid email or password")
@@ -48,8 +49,11 @@ var (
ErrAIResponseFail = errors.New("AI responded with failure") ErrAIResponseFail = errors.New("AI responded with failure")
ErrAIBadOutput = errors.New("AI response does not obey the format") ErrAIBadOutput = errors.New("AI response does not obey the format")
// Typed errors for product list handler // Typed errors for product handler
ErrBadPaging = errors.New("bad or missing paging attribute value in header") ErrBadPaging = errors.New("bad or missing paging attribute value in header")
ErrProductNotFound = errors.New("product with provided id does not exist")
ErrAlreadyInFavorites = errors.New("the product already is in your favorites")
ErrNotInFavorites = errors.New("the product already is not in your favorites")
// Typed errors for menu handler // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
@@ -67,9 +71,21 @@ var (
ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage") ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage")
ErrPriceRequired = errors.New("price required when reduction_type is amount") ErrPriceRequired = errors.New("price required when reduction_type is amount")
ErrSpecificPriceNotFound = errors.New("price reduction not found") ErrSpecificPriceNotFound = errors.New("price reduction not found")
// Typed errors for storage
ErrAccessDenied = errors.New("access denied!")
ErrFolderDoesNotExist = errors.New("folder does not exist")
ErrFileDoesNotExist = errors.New("file does not exist")
ErrNameTaken = errors.New("name taken")
ErrMissingFileFieldDocument = errors.New("missing file field 'document'")
// Typed errors for data parsing // Typed errors for data parsing
ErrJSONBody = errors.New("invalid JSON body") ErrJSONBody = errors.New("invalid JSON body")
// Typed errors for addresses
ErrMaxAmtOfAddressesReached = errors.New("maximal amount of addresses per user reached")
ErrUserHasNoSuchAddress = errors.New("user has no such address")
ErrInvalidCountryID = errors.New("invalid country id")
ErrInvalidAddressJSON = errors.New("invalid address json")
) )
// Error represents an error with HTTP status code // Error represents an error with HTTP status code
@@ -124,6 +140,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
return i18n.T_(c, "error.err_token_required") return i18n.T_(c, "error.err_token_required")
case errors.Is(err, ErrRefreshTokenRequired): case errors.Is(err, ErrRefreshTokenRequired):
return i18n.T_(c, "error.err_refresh_token_required") 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): case errors.Is(err, ErrBadLangID):
return i18n.T_(c, "error.err_bad_lang_id") return i18n.T_(c, "error.err_bad_lang_id")
case errors.Is(err, ErrBadCountryID): case errors.Is(err, ErrBadCountryID):
@@ -150,7 +168,7 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadField): case errors.Is(err, ErrBadField):
return i18n.T_(c, "error.err_bad_field") return i18n.T_(c, "error.err_bad_field")
case errors.Is(err, ErrInvalidURLSlug): case errors.Is(err, ErrInvalidURLSlug):
return i18n.T_(c, "error.invalid_url_slug") return i18n.T_(c, "error.err_invalid_url_slug")
case errors.Is(err, ErrInvalidXHTML): case errors.Is(err, ErrInvalidXHTML):
return i18n.T_(c, "error.err_invalid_html") return i18n.T_(c, "error.err_invalid_html")
case errors.Is(err, ErrAIResponseFail): case errors.Is(err, ErrAIResponseFail):
@@ -160,22 +178,39 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadPaging): case errors.Is(err, ErrBadPaging):
return i18n.T_(c, "error.err_bad_paging") return i18n.T_(c, "error.err_bad_paging")
case errors.Is(err, ErrProductNotFound):
return i18n.T_(c, "error.err_product_not_found")
case errors.Is(err, ErrAlreadyInFavorites):
return i18n.T_(c, "error.err_already_in_favorites")
case errors.Is(err, ErrNotInFavorites):
return i18n.T_(c, "error.err_already_not_in_favorites")
case errors.Is(err, ErrNoRootFound): case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.no_root_found") return i18n.T_(c, "error.err_no_root_found")
case errors.Is(err, ErrCircularDependency): case errors.Is(err, ErrCircularDependency):
return i18n.T_(c, "error.circular_dependency") return i18n.T_(c, "error.err_circular_dependency")
case errors.Is(err, ErrStartCategoryNotFound): case errors.Is(err, ErrStartCategoryNotFound):
return i18n.T_(c, "error.start_category_not_found") return i18n.T_(c, "error.err_start_category_not_found")
case errors.Is(err, ErrRootNeverReached): case errors.Is(err, ErrRootNeverReached):
return i18n.T_(c, "error.root_never_reached") return i18n.T_(c, "error.err_root_never_reached")
case errors.Is(err, ErrMaxAmtOfCartsReached): case errors.Is(err, ErrMaxAmtOfCartsReached):
return i18n.T_(c, "error.max_amt_of_carts_reached") return i18n.T_(c, "error.err_max_amt_of_carts_reached")
case errors.Is(err, ErrUserHasNoSuchCart): case errors.Is(err, ErrUserHasNoSuchCart):
return i18n.T_(c, "error.user_has_no_such_cart") return i18n.T_(c, "error.err_user_has_no_such_cart")
case errors.Is(err, ErrProductOrItsVariationDoesNotExist): case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
return i18n.T_(c, "error.product_or_its_variation_does_not_exist") return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist")
case errors.Is(err, ErrAccessDenied):
return i18n.T_(c, "error.err_access_denied")
case errors.Is(err, ErrFolderDoesNotExist):
return i18n.T_(c, "error.err_folder_does_not_exist")
case errors.Is(err, ErrFileDoesNotExist):
return i18n.T_(c, "error.err_file_does_not_exist")
case errors.Is(err, ErrNameTaken):
return i18n.T_(c, "error.err_name_taken")
case errors.Is(err, ErrMissingFileFieldDocument):
return i18n.T_(c, "error.err_missing_file_field_document")
case errors.Is(err, ErrInvalidReductionType): case errors.Is(err, ErrInvalidReductionType):
return i18n.T_(c, "error.invalid_reduction_type") return i18n.T_(c, "error.invalid_reduction_type")
@@ -189,6 +224,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrJSONBody): case errors.Is(err, ErrJSONBody):
return i18n.T_(c, "error.err_json_body") return i18n.T_(c, "error.err_json_body")
case errors.Is(err, ErrMaxAmtOfAddressesReached):
return i18n.T_(c, "error.err_max_amt_of_addresses_reached")
case errors.Is(err, ErrUserHasNoSuchAddress):
return i18n.T_(c, "error.err_user_has_no_such_address")
case errors.Is(err, ErrInvalidCountryID):
return i18n.T_(c, "error.err_invalid_country_id")
case errors.Is(err, ErrInvalidAddressJSON):
return i18n.T_(c, "error.err_invalid_address_json")
default: default:
return i18n.T_(c, "error.err_internal_server_error") return i18n.T_(c, "error.err_internal_server_error")
} }
@@ -213,6 +257,7 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrEmailPasswordRequired), errors.Is(err, ErrEmailPasswordRequired),
errors.Is(err, ErrTokenRequired), errors.Is(err, ErrTokenRequired),
errors.Is(err, ErrRefreshTokenRequired), errors.Is(err, ErrRefreshTokenRequired),
errors.Is(err, ErrAdminAccessRequired),
errors.Is(err, ErrBadLangID), errors.Is(err, ErrBadLangID),
errors.Is(err, ErrBadCountryID), errors.Is(err, ErrBadCountryID),
errors.Is(err, ErrPasswordsDoNotMatch), errors.Is(err, ErrPasswordsDoNotMatch),
@@ -227,6 +272,9 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidURLSlug),
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrProductNotFound),
errors.Is(err, ErrAlreadyInFavorites),
errors.Is(err, ErrNotInFavorites),
errors.Is(err, ErrNoRootFound), errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrCircularDependency), errors.Is(err, ErrCircularDependency),
errors.Is(err, ErrStartCategoryNotFound), errors.Is(err, ErrStartCategoryNotFound),
@@ -237,7 +285,17 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidReductionType), errors.Is(err, ErrInvalidReductionType),
errors.Is(err, ErrPercentageRequired), errors.Is(err, ErrPercentageRequired),
errors.Is(err, ErrPriceRequired), errors.Is(err, ErrPriceRequired),
errors.Is(err, ErrJSONBody): errors.Is(err, ErrJSONBody),
errors.Is(err, ErrAccessDenied),
errors.Is(err, ErrFolderDoesNotExist),
errors.Is(err, ErrFileDoesNotExist),
errors.Is(err, ErrNameTaken),
errors.Is(err, ErrMissingFileFieldDocument),
errors.Is(err, ErrJSONBody),
errors.Is(err, ErrMaxAmtOfAddressesReached),
errors.Is(err, ErrUserHasNoSuchAddress),
errors.Is(err, ErrInvalidCountryID),
errors.Is(err, ErrInvalidAddressJSON):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrSpecificPriceNotFound): case errors.Is(err, ErrSpecificPriceNotFound):
return fiber.StatusNotFound return fiber.StatusNotFound

View File

@@ -93,4 +93,6 @@ type Product struct {
// Relations // Relations
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"` Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"`
Category string `gorm:"column:category" json:"category"` Category string `gorm:"column:category" json:"category"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
} }

1
bo/components.d.ts vendored
View File

@@ -13,7 +13,6 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']

View File

@@ -0,0 +1,15 @@
info:
name: Add To Favorites
type: http
seq: 3
http:
method: POST
url: "{{bas_url}}/restricted/product/favorite/53"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: Remove Form Favorites
type: http
seq: 4
http:
method: DELETE
url: "{{bas_url}}/restricted/product/favorite/51"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +1,7 @@
info: info:
name: translate-product-description name: translate-product-description
type: http type: http
seq: 20 seq: 21
http: http:
method: GET method: GET

View File

@@ -0,0 +1,31 @@
info:
name: add-new-address
type: http
seq: 1
http:
method: POST
url: http://localhost:3000/api/v1/restricted/addresses/add-new-address?country_id=1
params:
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "małopolskie",
"street": "Długa",
"building_no": "5",
"apartment_no": "7",
"recipient": "Jan Kowalski"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,19 @@
info:
name: delete-address
type: http
seq: 4
http:
method: DELETE
url: http://localhost:3000/api/v1/restricted/addresses/delete-address?address_id=1
params:
- name: address_id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: addresses
type: folder
seq: 10
request:
auth: inherit

View File

@@ -0,0 +1,19 @@
info:
name: get-template
type: http
seq: 5
http:
method: GET
url: http://localhost:3000/api/v1/restricted/addresses/get-template?country_id=3
params:
- name: country_id
value: "3"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,33 @@
info:
name: modify-address
type: http
seq: 2
http:
method: POST
url: http://localhost:3000/api/v1/restricted/addresses/modify-address?country_id=1&address_id=1
params:
- name: country_id
value: "1"
type: query
- name: address_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "śląskie",
"street": "Długa",
"building_no": "5",
"recipient": "Adam Adamowicz"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: retrieve-addresses
type: http
seq: 3
http:
method: GET
url: http://localhost:3000/api/v1/restricted/addresses/retrieve-addresses
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: auth
type: folder
seq: 1
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: update-choice name: update-choice
type: http type: http
seq: 3 seq: 1
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: add-new-cart name: add-new-cart
type: http type: http
seq: 11 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: add-product-to-cart (1) name: add-product-to-cart (1)
type: http type: http
seq: 16 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: add-product-to-cart name: add-product-to-cart
type: http type: http
seq: 15 seq: 14
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: change-cart-name name: change-cart-name
type: http type: http
seq: 12 seq: 1
http: http:
method: GET method: GET

View File

@@ -0,0 +1,7 @@
info:
name: carts
type: folder
seq: 7
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: retrieve-cart name: retrieve-cart
type: http type: http
seq: 14 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: retrieve-carts-info name: retrieve-carts-info
type: http type: http
seq: 13 seq: 1
http: http:
method: GET method: GET

View File

@@ -0,0 +1,7 @@
info:
name: langs-and-countries
type: folder
seq: 4
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: get_countries name: get_countries
type: http type: http
seq: 4 seq: 1
http: http:
method: GET method: GET

View File

@@ -0,0 +1,7 @@
info:
name: list
type: folder
seq: 3
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: list-users name: list-users
type: http type: http
seq: 2 seq: 1
http: http:
method: GET method: GET

View File

@@ -0,0 +1,7 @@
info:
name: menu
type: folder
seq: 5
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: get-breadcrumb name: get-breadcrumb
type: http type: http
seq: 18 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: get-category-tree name: get-category-tree
type: http type: http
seq: 5 seq: 1
http: http:
method: GET method: GET

View File

@@ -0,0 +1,7 @@
info:
name: product-translation
type: folder
seq: 2
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: get-product-description name: get-product-description
type: http type: http
seq: 17 seq: 1
http: http:
method: GET method: GET

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
info:
name: translate-product-description
type: http
seq: 24
http:
method: GET
url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=2&productToLangID=3&model=Google
params:
- name: productID
value: "51"
type: query
- name: productFromLangID
value: "2"
type: query
- name: productToLangID
value: "3"
type: query
- name: model
value: Google
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,11 +1,11 @@
info: info:
name: create-index name: create-index
type: http type: http
seq: 7 seq: 1
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/meili-search/create-index url: http://localhost:3000/api/v1/restricted/search/create-index
auth: inherit auth: inherit
settings: settings:

View File

@@ -0,0 +1,7 @@
info:
name: search
type: folder
seq: 6
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: get-indexes name: get-indexes
type: http type: http
seq: 9 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: remove-index name: remove-index
type: http type: http
seq: 8 seq: 1
http: http:
method: DELETE method: DELETE

View File

@@ -1,11 +1,11 @@
info: info:
name: search name: search
type: http type: http
seq: 10 seq: 1
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/meili-search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0 url: http://localhost:3000/api/v1/restricted/search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0
params: params:
- name: query - name: query
value: w value: w

View File

@@ -1,11 +1,11 @@
info: info:
name: test name: test
type: http type: http
seq: 6 seq: 1
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/meili-search/test url: http://localhost:3000/api/v1/restricted/search/test
auth: inherit auth: inherit
settings: settings:

View File

@@ -0,0 +1,19 @@
info:
name: copy
type: http
seq: 7
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/copy/folder1/test.txt?dest_path=/folder/a.txt
params:
- name: dest_path
value: /folder/a.txt
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,19 @@
info:
name: create-folder
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/create-folder?name=folder
params:
- name: name
value: folder
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: delete-file
type: http
seq: 1
http:
method: DELETE
url: http://localhost:3000/api/v1/restricted/storage/delete-file/folder1/TODO.txt
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: delete-folder
type: http
seq: 1
http:
method: DELETE
url: http://localhost:3000/api/v1/restricted/storage/delete-folder/folder/
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: download-file
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/download-file/folder1/test.xlsx
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: storage-old
type: folder
seq: 1
request:
auth: inherit

View File

@@ -0,0 +1,15 @@
info:
name: list-content
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/list-content/folder1
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,19 @@
info:
name: move
type: http
seq: 8
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/move/folder?dest_path=/folder1/test.txt
params:
- name: dest_path
value: /folder1/test.txt
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: upload-file
type: http
seq: 1
http:
method: POST
url: http://localhost:3000/api/v1/restricted/storage/upload-file/folder1/
body:
type: multipart-form
data:
- name: document
type: file
value:
- /home/daniel/TODO.txt
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -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

View File

@@ -0,0 +1,15 @@
info:
name: download-file
type: http
seq: 3
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/download-file/dest/src/cccc.txt
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: storage-restricted
type: folder
seq: 9
request:
auth: inherit

View File

@@ -0,0 +1,15 @@
info:
name: list-content
type: http
seq: 2
http:
method: GET
url: http://localhost:3000/api/v1/restricted/storage/list-content/dest/src
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

2
go.mod
View File

@@ -101,7 +101,7 @@ require (
golang.org/x/net v0.52.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.35.0
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
) )

View File

@@ -104,6 +104,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
email_verification_expires DATETIME(6) NULL, email_verification_expires DATETIME(6) NULL,
password_reset_token VARCHAR(255) NULL, password_reset_token VARCHAR(255) NULL,
password_reset_expires DATETIME(6) 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_password_reset_request DATETIME(6) NULL,
last_login_at DATETIME(6) NULL, last_login_at DATETIME(6) NULL,
lang_id INT NULL DEFAULT 2, lang_id INT NULL DEFAULT 2,
@@ -119,6 +121,9 @@ ON b2b_customers (email);
CREATE INDEX IF NOT EXISTS idx_customers_deleted_at CREATE INDEX IF NOT EXISTS idx_customers_deleted_at
ON b2b_customers (deleted_at); ON b2b_customers (deleted_at);
CREATE INDEX IF NOT EXISTS idx_customers_webdav_token
ON b2b_customers (webdav_token);
ALTER TABLE b2b_customers ALTER TABLE b2b_customers
ADD CONSTRAINT fk_customer_role ADD CONSTRAINT fk_customer_role
FOREIGN KEY (role_id) REFERENCES b2b_roles(id); FOREIGN KEY (role_id) REFERENCES b2b_roles(id);
@@ -146,6 +151,16 @@ CREATE TABLE IF NOT EXISTS b2b_carts_products (
CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id); CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id);
-- favorites
CREATE TABLE IF NOT EXISTS b2b_favorites (
user_id BIGINT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
PRIMARY KEY (user_id, product_id),
CONSTRAINT fk_favorites_customer FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_favorites_product FOREIGN KEY (product_id) REFERENCES ps_product(id_product) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- refresh_tokens -- refresh_tokens
CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
@@ -205,6 +220,18 @@ ON `b2b_countries` (
`ps_id_country` ASC `ps_id_country` ASC
); );
-- addresses
CREATE TABLE IF NOT EXISTS b2b_addresses (
id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
b2b_customer_id BIGINT UNSIGNED NOT NULL,
address_info TEXT NOT NULL,
b2b_country_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_b2b_addresses_b2b_customers FOREIGN KEY (b2b_customer_id) REFERENCES b2b_customers (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_b2b_addresses_b2b_countries FOREIGN KEY (b2b_country_id) REFERENCES b2b_countries (id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB;
CREATE TABLE b2b_specific_price ( CREATE TABLE b2b_specific_price (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,

View File

@@ -378,6 +378,15 @@ BEGIN
m.name AS manufacturer, m.name AS manufacturer,
cl.name AS category cl.name AS category
-- This doesn't fit to base product, I'll add proper is_favorite to product later
-- EXISTS(
-- SELECT 1 FROM b2b_favorites f
-- WHERE f.user_id = p_id_customer AND f.product_id = p_id_product
-- ) AS is_favorite
FROM ps_product p FROM ps_product p
LEFT JOIN ps_product_shop ps LEFT JOIN ps_product_shop ps
ON ps.id_product = p.id_product ON ps.id_product = p.id_product

0
storage/.gitkeep Normal file
View File

View File

@@ -1 +0,0 @@
This is a test.