10 Commits

95 changed files with 1358 additions and 1685 deletions

4
.env
View File

@@ -48,10 +48,6 @@ 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,5 +6,3 @@ i18n/*.json
*_templ.go *_templ.go
tmp/main tmp/main
test.go test.go
storage/*
!storage/.gitkeep

View File

@@ -2,10 +2,8 @@ package config
import ( import (
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -26,8 +24,7 @@ type Config struct {
GoogleTranslate GoogleTranslateConfig GoogleTranslate GoogleTranslateConfig
Image ImageConfig Image ImageConfig
Cors CorsConfig Cors CorsConfig
MeiliSearch MeiliSearchConfig MailiSearch MeiliSearchConfig
Storage StorageConfig
} }
type I18n struct { type I18n struct {
@@ -98,10 +95,6 @@ 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"`
} }
@@ -162,7 +155,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 oauth google : ", err.Error(), "") slog.Error("not possible to load env variables for outh google : ", err.Error(), "")
} }
err = loadEnv(&cfg.App) err = loadEnv(&cfg.App)
@@ -177,12 +170,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 i18n : ", err.Error(), "") slog.Error("not possible to load env variables for email : ", 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 pdf : ", err.Error(), "") slog.Error("not possible to load env variables for email : ", err.Error(), "")
} }
err = loadEnv(&cfg.GoogleTranslate) err = loadEnv(&cfg.GoogleTranslate)
@@ -192,25 +185,19 @@ 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 image : ", err.Error(), "") slog.Error("not possible to load env variables for google translate : ", 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 cors : ", err.Error(), "") slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
} }
err = loadEnv(&cfg.MeiliSearch) err = loadEnv(&cfg.MailiSearch)
if err != nil { if err != nil {
slog.Error("not possible to load env variables for meili search : ", err.Error(), "") slog.Error("not possible to load env variables for google translate : ", 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
} }
@@ -321,22 +308,6 @@ 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,16 +1,12 @@
package middleware package middleware
import ( import (
"encoding/base64"
"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"
) )
@@ -64,52 +60,9 @@ func AuthMiddleware() fiber.Handler {
}) })
} }
// Create locale. LangID is overwritten by auth Token // Set user in context
var userLocale model.UserLocale c.Locals(constdata.USER_LOCALES_NAME, user.ToSession())
userLocale.OriginalUser = user c.Locals(constdata.USER_LOCALES_ID, user.ID)
// Check if target user is present
targetUserIDAttribute := c.Query("target_user_id")
if targetUserIDAttribute == "" {
userLocale.User = user
c.Locals(constdata.USER_LOCALE, &userLocale)
return c.Next()
}
// We now populate the target user
if user.Role != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required",
})
}
targetUserID, err := strconv.Atoi(targetUserIDAttribute)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "invalid target user id attribute",
})
}
// to verify target user, we use the same functionality as for verifying original user
// Get target user from database
user, err = authService.GetUserByID(uint(targetUserID))
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "target user not found",
})
}
// Check if target user is active
if !user.IsActive {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "target user account is inactive",
})
}
userLocale.User = user
c.Locals(constdata.USER_LOCALE, &userLocale)
return c.Next() return c.Next()
} }
@@ -118,14 +71,21 @@ 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 {
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c) user := c.Locals("user")
if !ok { if user == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated", "error": "not authenticated",
}) })
} }
if originalUserRole != model.RoleAdmin { userSession, ok := user.(*model.UserSession)
if !ok {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "invalid user session",
})
}
if userSession.Role != 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",
}) })
@@ -135,70 +95,22 @@ func RequireAdmin() fiber.Handler {
} }
} }
// Webdav // GetUserID extracts user ID from context
func Webdav() fiber.Handler { func GetUserID(c fiber.Ctx) uint {
authService := authService.NewAuthService() userID, ok := c.Locals("userID").(uint)
if !ok {
return func(c fiber.Ctx) error { return 0
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()
} }
return userID
}
// GetUser extracts user from context
func GetUser(c fiber.Ctx) *model.UserSession {
user, ok := c.Locals("user").(*model.UserSession)
if !ok {
return nil
}
return user
} }
// GetConfig returns the app config // GetConfig returns the app config

View File

@@ -4,9 +4,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/service/langsService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -24,8 +22,12 @@ func LanguageMiddleware() fiber.Handler {
if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil { if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil {
langID = uint(id) langID = uint(id)
if langID > 0 { if langID > 0 {
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) lang, err := langService.GetLanguageById(langID)
return c.Next() if err == nil {
c.Locals("langID", langID)
c.Locals("lang", lang)
return c.Next()
}
} }
} }
} }
@@ -36,8 +38,12 @@ func LanguageMiddleware() fiber.Handler {
if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil { if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil {
langID = uint(id) langID = uint(id)
if langID > 0 { if langID > 0 {
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) lang, err := langService.GetLanguageById(langID)
return c.Next() if err == nil {
c.Locals("langID", langID)
c.Locals("lang", lang)
return c.Next()
}
} }
} }
} }
@@ -51,7 +57,8 @@ func LanguageMiddleware() fiber.Handler {
lang, err := langService.GetLanguageByISOCode(isoCode) lang, err := langService.GetLanguageByISOCode(isoCode)
if err == nil && lang != nil { if err == nil && lang != nil {
langID = uint(lang.ID) langID = uint(lang.ID)
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) c.Locals("langID", langID)
c.Locals("lang", lang)
return c.Next() return c.Next()
} }
} }
@@ -61,7 +68,8 @@ func LanguageMiddleware() fiber.Handler {
defaultLang, err := langService.GetDefaultLanguage() defaultLang, err := langService.GetDefaultLanguage()
if err == nil && defaultLang != nil { if err == nil && defaultLang != nil {
langID = uint(defaultLang.ID) langID = uint(defaultLang.ID)
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID)) c.Locals("langID", langID)
c.Locals("lang", defaultLang)
} }
return c.Next() return c.Next()
@@ -96,9 +104,11 @@ func parseAcceptLanguage(header string) string {
return strings.ToLower(first) return strings.ToLower(first)
} }
func returnNewLocale(lang_id uint) *model.UserLocale { // GetLanguageID extracts language ID from context
newLocale := model.UserLocale{} func GetLanguageID(c fiber.Ctx) uint {
newLocale.OriginalUser = &model.Customer{} langID, ok := c.Locals("langID").(uint)
newLocale.OriginalUser.LangID = lang_id if !ok {
return &newLocale return 0
}
return langID
} }

View File

@@ -268,15 +268,15 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
// Me returns the current user info // Me returns the current user info
func (h *AuthHandler) Me(c fiber.Ctx) error { func (h *AuthHandler) Me(c fiber.Ctx) error {
userLocale := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) user := c.Locals("user")
if userLocale.OriginalUser == nil { if user == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated), "error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated),
}) })
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"user": *userLocale.OriginalUser, "user": user,
}) })
} }
@@ -351,12 +351,21 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
// Updates JWT Tokens. Requires authentication and updates access token only // Updates JWT Tokens. Requires authentication and updates access token only
func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error { func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
userLocale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) userLocals, ok := c.Locals(constdata.USER_LOCALES_NAME).(*model.UserSession)
if !ok { if !ok {
return c.Status(fiber.StatusUnauthorized). return c.Status(fiber.StatusUnauthorized).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
} }
user := model.Customer{
ID: userLocals.UserID,
Email: userLocals.Email,
Role: userLocals.Role,
LangID: userLocals.LangID,
CountryID: userLocals.CountryID,
IsActive: userLocals.IsActive,
}
// Parse language and country_id from query params // Parse language and country_id from query params
langIDStr := c.Query("lang_id") langIDStr := c.Query("lang_id")
@@ -366,7 +375,7 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest). return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
} }
userLocale.OriginalUser.LangID = uint(parsedID) user.LangID = uint(parsedID)
} }
countryIDStr := c.Query("country_id") countryIDStr := c.Query("country_id")
@@ -377,10 +386,10 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest). return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
} }
userLocale.OriginalUser.CountryID = uint(parsedID) user.CountryID = uint(parsedID)
} }
newAccessToken, err := h.authService.UpdateJWTToken(userLocale.OriginalUser) newAccessToken, err := h.authService.UpdateJWTToken(&user)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{

View File

@@ -3,7 +3,6 @@ package public
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"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/nullable" "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/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -31,7 +30,7 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))

View File

@@ -5,7 +5,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/cartsService" "git.ma-al.com/goc_daniel/b2b/app/service/cartsService"
"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/nullable" "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/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -38,7 +37,7 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *CartsHandler) AddNewCart(c fiber.Ctx) error { func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
@@ -54,7 +53,7 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
} }
func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
@@ -79,7 +78,7 @@ func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
} }
func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error { func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
@@ -95,7 +94,7 @@ func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
} }
func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error { func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
@@ -118,7 +117,7 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
} }
func (h *CartsHandler) AddProduct(c fiber.Ctx) error { func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))

View File

@@ -5,7 +5,6 @@ import (
"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/listService" "git.ma-al.com/goc_daniel/b2b/app/service/listService"
"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/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -44,19 +43,19 @@ func (h *ListHandler) ListProducts(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
id_lang, ok := localeExtractor.GetLangID(c) id_lang, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
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.listService.ListProducts(id_lang, paging, filters) listing, err := h.listService.ListProducts(id_lang, paging, filters)
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)))
} }
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
} }
var columnMappingListProducts map[string]string = map[string]string{ var columnMappingListProducts map[string]string = map[string]string{
@@ -75,19 +74,19 @@ func (h *ListHandler) ListUsers(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
id_lang, ok := localeExtractor.GetLangID(c) id_lang, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
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.listService.ListUsers(id_lang, paging, filters) listing, err := h.listService.ListUsers(id_lang, paging, filters)
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)))
} }
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
} }
var columnMappingListUsers map[string]string = map[string]string{ var columnMappingListUsers map[string]string = map[string]string{

View File

@@ -5,7 +5,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"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/nullable" "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/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -34,7 +33,7 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error { func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
@@ -57,7 +56,7 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
} }
func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error { func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
@@ -87,7 +86,7 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
} }
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))

View File

@@ -4,10 +4,8 @@ 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/nullable" "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/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -43,7 +41,7 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
// GetProductDescription returns the product description for a given product ID // GetProductDescription returns the product description for a given product ID
func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error { func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
@@ -74,18 +72,12 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
// SaveProductDescription saves the description for a given product ID, in given language // SaveProductDescription saves the description for a given product ID, in given language
func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error { func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
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 || userRole != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {
@@ -117,18 +109,12 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
// TranslateProductDescription returns translated product description // TranslateProductDescription returns translated product description
func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) error { func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
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 || userRole != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_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,11 +4,9 @@ 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"
"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/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -38,18 +36,12 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
id_lang, ok := localeExtractor.GetLangID(c) id_lang, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
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 || userRole != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
err := h.meiliService.CreateIndex(id_lang) 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)
@@ -57,11 +49,12 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, 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))) nothing := ""
return c.JSON(response.Make(&nothing, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *MeiliSearchHandler) Search(c fiber.Ctx) error { func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
id_lang, ok := localeExtractor.GetLangID(c) id_lang, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
@@ -95,7 +88,7 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
} }
func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error { func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error {
id_lang, ok := localeExtractor.GetLangID(c) id_lang, ok := c.Locals("langID").(uint)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))

View File

@@ -1,100 +0,0 @@
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 || userRole != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&new_token, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -1,198 +0,0 @@
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,7 +14,6 @@ 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"
@@ -26,7 +25,6 @@ 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
@@ -44,23 +42,12 @@ func (s *Server) Cfg() *config.Config {
// New creates a new server instance // New creates a new server instance
func New() *Server { func New() *Server {
var s Server return &Server{
app: fiber.New(fiber.Config{
app := ErrorHandler: customErrorHandler,
fiber.New(fiber.Config{ }),
ErrorHandler: customErrorHandler, cfg: config.Get(),
BodyLimit: 50 * 1024 * 1024, // 50 MB }
StreamRequestBody: true,
RequestMethods: []string{
fiber.MethodGet, fiber.MethodHead, fiber.MethodPost, fiber.MethodPut,
fiber.MethodDelete, fiber.MethodConnect, fiber.MethodOptions,
fiber.MethodTrace, fiber.MethodPatch, "MKCOL", "PROPFIND", "PROPPATCH", "MOVE", "COPY",
},
})
s.app = app
s.cfg = config.Get()
return &s
} }
// Setup configures the server with routes and middleware // Setup configures the server with routes and middleware
@@ -89,8 +76,6 @@ 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)
@@ -130,12 +115,6 @@ func (s *Server) Setup() error {
carts := s.restricted.Group("/carts") carts := s.restricted.Group("/carts")
restricted.CartsHandlerRoutes(carts) restricted.CartsHandlerRoutes(carts)
// storage (uses various authorization means)
restrictedStorage := s.restricted.Group("/storage")
webdavStorage := s.webdav.Group("/storage")
restricted.StorageHandlerRoutes(restrictedStorage)
webdav.StorageHandlerRoutes(webdavStorage)
s.api.All("*", func(c fiber.Ctx) error { s.api.All("*", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound) return c.SendStatus(fiber.StatusNotFound)
}) })

View File

@@ -23,8 +23,6 @@ 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
@@ -84,15 +82,6 @@ type UserSession struct {
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
} }
type UserLocale struct {
// User is the Target user if present, otherwise same as Original.
// User ought to be used in applications
User *Customer
// Original user is the one associated with auth token
OriginalUser *Customer
// Importantly, lang_id used in application is stored as OriginalUser.LangID
}
// ToSession converts User to UserSession // ToSession converts User to UserSession
func (u *Customer) ToSession() *UserSession { func (u *Customer) ToSession() *UserSession {
return &UserSession{ return &UserSession{
@@ -109,7 +98,6 @@ func (u *Customer) ToSession() *UserSession {
type LoginRequest struct { type LoginRequest struct {
Email string `json:"email" form:"email"` Email string `json:"email" form:"email"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
LangID *uint `json:"lang_id" form:"lang_id"`
} }
// RegisterRequest represents the initial registration form data // RegisterRequest represents the initial registration form data

View File

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

View File

@@ -18,10 +18,9 @@ type ProductDescription struct {
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"` AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"`
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"` DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"`
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"` DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
Usage string `gorm:"column:_usage_;type:text" json:"usage" form:"usage"` Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
ImageLink string `gorm:"column:image_link" json:"image_link"` ExistsInDatabse bool `gorm:"-" json:"exists_in_database"`
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
} }
type ProductRow struct { type ProductRow struct {

View File

@@ -32,7 +32,7 @@ func (repo *ListRepo) ListProducts(id_lang uint, p find.Paging, filt *filters.Fi
ps.id_product AS product_id, ps.id_product AS product_id,
pl.name AS name, pl.name AS name,
pl.link_rewrite AS link_rewrite, pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, CONCAT(?, ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
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,

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"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/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
@@ -37,36 +36,15 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
IDShop: int32(constdata.SHOP_ID), IDShop: int32(constdata.SHOP_ID),
IDLang: int32(productid_lang), IDLang: int32(productid_lang),
}). }).
Select(`
`+dbmodel.PsProductLangCols.IDProduct.TabCol()+` AS id_product,
`+dbmodel.PsProductLangCols.IDShop.TabCol()+` AS id_shop,
`+dbmodel.PsProductLangCols.IDLang.TabCol()+` AS id_lang,
`+dbmodel.PsProductLangCols.Description.TabCol()+` AS description,
`+dbmodel.PsProductLangCols.DescriptionShort.TabCol()+` AS description_short,
`+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+` AS link_rewrite,
`+dbmodel.PsProductLangCols.MetaDescription.TabCol()+` AS meta_description,
`+dbmodel.PsProductLangCols.MetaKeywords.TabCol()+` AS meta_keywords,
`+dbmodel.PsProductLangCols.MetaTitle.TabCol()+` AS meta_title,
`+dbmodel.PsProductLangCols.Name.TabCol()+` AS name,
`+dbmodel.PsProductLangCols.AvailableNow.TabCol()+` AS available_now,
`+dbmodel.PsProductLangCols.AvailableLater.TabCol()+` AS available_later,
`+dbmodel.PsProductLangCols.DeliveryInStock.TabCol()+` AS delivery_in_stock,
`+dbmodel.PsProductLangCols.DeliveryOutStock.TabCol()+` AS delivery_out_stock,
`+dbmodel.PsProductLangCols.Usage.TabCol()+` AS _usage_,
CONCAT(?, '/', `+dbmodel.PsImageShopCols.IDImage.TabCol()+`, '-large_default/', `+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+`, '.webp') AS image_link
`, config.Get().Image.ImagePrefix).
Joins("JOIN " + dbmodel.TableNamePsImageShop +
" ON " + dbmodel.PsImageShopCols.IDProduct.TabCol() + "=" + dbmodel.PsProductLangCols.IDProduct.TabCol() +
" AND " + dbmodel.PsImageShopCols.Cover.TabCol() + " = 1").
First(&ProductDescription).Error First(&ProductDescription).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
// handle "not found" case only // handle "not found" case only
ProductDescription.ExistsInDatabase = false ProductDescription.ExistsInDatabse = false
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("database error: %w", err) return nil, fmt.Errorf("database error: %w", err)
} else { } else {
ProductDescription.ExistsInDatabase = true ProductDescription.ExistsInDatabse = true
} }
return &ProductDescription, nil return &ProductDescription, nil

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.MeiliSearch.ServerURL, index) url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MailiSearch.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.MeiliSearch.ServerURL, index) url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MailiSearch.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.MeiliSearch.ApiKey != "" { if r.cfg.MailiSearch.ApiKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey))
} }
client := &http.Client{} client := &http.Client{}

View File

@@ -1,178 +0,0 @@
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

@@ -83,15 +83,6 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
// Update last login time // Update last login time
now := time.Now() now := time.Now()
user.LastLoginAt = &now user.LastLoginAt = &now
if req.LangID != nil {
_, err := s.GetLangISOCode(*req.LangID)
if err != nil {
return nil, "", responseErrors.ErrBadLangID
}
user.LangID = *req.LangID
}
s.db.Save(&user) s.db.Save(&user)
// Generate access token (JWT) // Generate access token (JWT)
@@ -452,19 +443,6 @@ 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,7 +10,6 @@ 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"
) )
@@ -134,6 +133,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: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: 2, 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().MeiliSearch.ServerURL, config.Get().MailiSearch.ServerURL,
meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey), meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey),
) )
return &MeiliService{ return &MeiliService{

View File

@@ -1,283 +0,0 @@
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,7 +4,6 @@ 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
@@ -12,9 +11,5 @@ 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 USER_LOCALE = "user" const USER_LOCALES_NAME = "user"
const USER_LOCALES_ID = "userID"
// 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"

View File

@@ -8,7 +8,6 @@ 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"
) )
@@ -178,7 +177,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 := localeExtractor.GetLangID(c); ok { if langID, ok := c.Locals("langID").(uint); ok {
parts := strings.Split(string(key), ".") parts := strings.Split(string(key), ".")
if len(parts) >= 2 { if len(parts) >= 2 {

View File

@@ -1,31 +0,0 @@
package localeExtractor
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/gofiber/fiber/v3"
)
func GetLangID(c fiber.Ctx) (uint, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil {
return 0, false
}
return user_locale.OriginalUser.LangID, true
}
func GetUserID(c fiber.Ctx) (uint, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil {
return 0, false
}
return user_locale.User.ID, true
}
func GetOriginalUserRole(c fiber.Ctx) (model.CustomerRole, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil {
return "", false
}
return user_locale.OriginalUser.Role, true
}

View File

@@ -9,14 +9,13 @@ import (
var ( var (
// Typed errors for request validation and authentication // Typed errors for request validation and authentication
ErrInvalidBody = errors.New("invalid request body") ErrInvalidBody = errors.New("invalid request body")
ErrNotAuthenticated = errors.New("not authenticated") ErrNotAuthenticated = errors.New("not authenticated")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
ErrUserInactive = errors.New("user account is inactive") ErrUserInactive = errors.New("user account is inactive")
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 is 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")
@@ -60,13 +59,6 @@ var (
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
// Typed errors for storage
ErrAccessDenied = errors.New("access denied!")
ErrFolderDoesNotExist = errors.New("folder does not exist")
ErrFileDoesNotExist = errors.New("file does not exist")
ErrNameTaken = errors.New("name taken")
ErrMissingFileFieldDocument = errors.New("missing file field 'document'")
) )
// Error represents an error with HTTP status code // Error represents an error with HTTP status code
@@ -119,8 +111,6 @@ 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):
@@ -172,17 +162,6 @@ func GetErrorCode(c fiber.Ctx, err error) string {
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.product_or_its_variation_does_not_exist")
case errors.Is(err, ErrAccessDenied):
return i18n.T_(c, "error.access_denied")
case errors.Is(err, ErrFolderDoesNotExist):
return i18n.T_(c, "error.folder_does_not_exist")
case errors.Is(err, ErrFileDoesNotExist):
return i18n.T_(c, "error.file_does_not_exist")
case errors.Is(err, ErrNameTaken):
return i18n.T_(c, "error.name_taken")
case errors.Is(err, ErrMissingFileFieldDocument):
return i18n.T_(c, "error.missing_file_field_document")
default: default:
return i18n.T_(c, "error.err_internal_server_error") return i18n.T_(c, "error.err_internal_server_error")
} }
@@ -205,7 +184,6 @@ 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),
@@ -225,12 +203,7 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrRootNeverReached), errors.Is(err, ErrRootNeverReached),
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrProductOrItsVariationDoesNotExist):
errors.Is(err, ErrAccessDenied),
errors.Is(err, ErrFolderDoesNotExist),
errors.Is(err, ErrFileDoesNotExist),
errors.Is(err, ErrNameTaken),
errors.Is(err, ErrMissingFileFieldDocument):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict

6
bo/components.d.ts vendored
View File

@@ -11,16 +11,21 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CartSelector: typeof import('./src/components/customer/CartSelector.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'] CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
copy: typeof import('./src/components/inner/categoryMenu copy.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']
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default'] En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default'] LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default'] PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
PageCheckout: typeof import('./src/components/customer/PageCheckout.vue')['default']
PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default'] PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default'] PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default'] PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
@@ -31,6 +36,7 @@ declare module 'vue' {
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default'] Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default'] ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default'] ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default'] ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@@ -58,12 +58,11 @@ await getTopMenu()
<!-- px-4 sm:px-6 lg:px-8 --> <!-- px-4 sm:px-6 lg:px-8 -->
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex items-center justify-between h-14"> <div class="flex items-center justify-between h-14">
<!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2"> <RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center"> <div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="i-heroicons-clock" class="w-5 h-5" /> <UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" />
</div> </div>
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">B2B</span>
</RouterLink> </RouterLink>
<UNavigationMenu :type="'trigger'" :ui="{ <UNavigationMenu :type="'trigger'" :ui="{
@@ -71,9 +70,7 @@ await getTopMenu()
list: 'gap-4' list: 'gap-4'
}" :items="menuItems" class="w-full"></UNavigationMenu> }" :items="menuItems" class="w-full"></UNavigationMenu>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher -->
<LangSwitch /> <LangSwitch />
<!-- Theme Switcher -->
<ThemeSwitch /> <ThemeSwitch />
<!-- Logout Button (only when authenticated) --> <!-- Logout Button (only when authenticated) -->
<button v-if="authStore.isAuthenticated" @click="authStore.logout()" <button v-if="authStore.isAuthenticated" @click="authStore.logout()"

View File

@@ -11,18 +11,14 @@ const authStore = useAuthStore()
class="fixed top-0 left-0 right-0 z-50 bg-(--main-light)/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)"> class="fixed top-0 left-0 right-0 z-50 bg-(--main-light)/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<div class="container px-4 sm:px-6 lg:px-8"> <div class="container px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14"> <div class="flex items-center justify-between h-14">
<!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2"> <RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center"> <div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" /> <UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" />
</div> </div>
<span class="font-semibold text-gray-900 dark:text-white">B2B</span> <span class="font-semibold text-gray-900 dark:text-white">B2B</span>
</RouterLink> </RouterLink>
<!-- Right Side Actions -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher -->
<LangSwitch /> <LangSwitch />
<!-- Theme Switcher -->
<ThemeSwitch /> <ThemeSwitch />
</div> </div>
</div> </div>

View File

@@ -1,11 +1,70 @@
<template> <template>
<suspense> <suspense>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="flex gap-10"> <!-- <div class="w-64 h-128">
<CategoryMenu /> <CategoryMenu />
<div class="w-full flex flex-col items-center gap-4"> </div> -->
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" /> <CategoryMenuListing />
<UTable :data="productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }">
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
thead: 'hidden'
}" />
</template>
</UTable>
<div class="container mx-auto mt-20">
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
<div v-if="loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
</div>
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ error }}
</div>
<div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Image</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Product Code</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Name</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Link</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="product in productsList" :key="product.product_id"
class="hover:bg-gray-50 dark:hover:bg-gray-800"
@click="goToProduct(product.product_id)">
<td class="px-6 py-4 whitespace-nowrap">
<img :src="product.image_link" alt="product image"
class="w-16 h-16 object-cover rounded" />
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
product.reference }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
product.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600 dark:text-blue-400">
{{ product.link_rewrite }}
</td>
</tr>
</tbody>
</table>
<div class="flex justify-center items-center py-8">
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
</div>
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
No products found
</div>
</div> </div>
</div> </div>
</component> </component>
@@ -18,20 +77,27 @@ import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/categoryMenu.vue' import CategoryMenuListing from '../inner/categoryMenuListing.vue'
import type { Product } from '@/types/product'
interface Product {
reference: number
product_id: number
name: string
image_link: string
link_rewrite: string
quantity: number
}
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const perPage = ref(15)
const page = computed({ const page = computed({
get: () => Number(route.query.p) || 1, get: () => Number(route.query.page) || 1,
set: (val: number) => { set: (val: number) => {
router.push({ router.push({
query: { query: {
...route.query, ...route.query,
p: val page: val
} }
}) })
} }
@@ -67,6 +133,7 @@ const sortField = computed({
} }
}) })
const perPage = ref(15)
const total = ref(0) const total = ref(0)
interface ApiResponse { interface ApiResponse {
@@ -127,25 +194,17 @@ async function fetchProductList() {
error.value = null error.value = null
const params = new URLSearchParams() const params = new URLSearchParams()
Object.entries(route.query).forEach(([key, value]) => { Object.entries(route.query).forEach(([key, value]) => {
if (value === undefined || value === null) return if (value) params.append(key, String(value))
if (Array.isArray(value)) {
value.forEach(v => params.append(key, String(v)))
} else {
params.append(key, String(value))
}
}) })
if (route.params.category_id) const url = `/api/v1/restricted/list/list-products?${params}`
params.append('category_id', String(route.params.category_id))
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}`
try { try {
const response = await useFetchJson<ApiResponse>(url) const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || [] productsList.value = response.items || []
total.value = Number(response.count) || 0 total.value = response.count || 0
} catch (e: unknown) { } catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load products' error.value = e instanceof Error ? e.message : 'Failed to load products'
} finally { } finally {
@@ -153,13 +212,19 @@ async function fetchProductList() {
} }
} }
function goToProduct(productId: number) { function goToProduct(productId: number, imageLink: string) {
router.push({ router.push({
name: 'customer-product-details', name: 'product-detail',
params: { product_id: productId } params: { id: productId },
query: { image: imageLink }
}) })
} }
const selectedCount = ref({
product_id: null as number | null,
count: 0
})
function getIcon(name: string) { function getIcon(name: string) {
if (sortField.value[0] === name) { if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide' if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
@@ -174,6 +239,24 @@ const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
const columns: TableColumn<Product>[] = [ const columns: TableColumn<Product>[] = [
{
id: 'expand',
cell: ({ row }) =>
h(UButton, {
color: 'neutral',
variant: 'ghost',
icon: 'i-lucide-chevron-down',
square: true,
'aria-label': 'Expand',
ui: {
leadingIcon: [
'transition-transform',
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
]
},
onClick: () => row.toggleExpanded()
})
},
{ {
accessorKey: 'product_id', accessorKey: 'product_id',
header: ({ column }) => { header: ({ column }) => {
@@ -200,8 +283,13 @@ const columns: TableColumn<Product>[] = [
}) })
]) ])
}, },
// header: '#', cell: ({ row }) => h('span', {
cell: ({ row }) => `#${row.getValue('product_id') as number}` class: 'cursor-pointer text-blue-500 hover:underline',
onClick: (e: Event) => {
e.stopPropagation()
goToProduct(row.original.product_id, row.original.image_link)
}
}, `#${row.getValue('product_id') as number}`)
}, },
{ {
accessorKey: 'image_link', accessorKey: 'image_link',
@@ -239,7 +327,13 @@ const columns: TableColumn<Product>[] = [
}) })
]) ])
}, },
cell: ({ row }) => row.getValue('name') as string, cell: ({ row }) => h('span', {
class: 'cursor-pointer text-blue-500 hover:underline',
onClick: (e: Event) => {
e.stopPropagation()
goToProduct(row.original.product_id, row.original.image_link)
}
}, row.getValue('name') as string),
filterFn: (row, columnId, value) => { filterFn: (row, columnId, value) => {
const name = row.getValue(columnId) as string const name = row.getValue(columnId) as string
return name.toLowerCase().includes(value.toLowerCase()) return name.toLowerCase().includes(value.toLowerCase())
@@ -264,17 +358,120 @@ const columns: TableColumn<Product>[] = [
}, },
cell: ({ row }) => row.getValue('quantity') as number cell: ({ row }) => row.getValue('quantity') as number
}, },
{
accessorKey: 'count',
header: 'Count',
cell: ({ row }) => {
return h(UInputNumber, {
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
'onUpdate:modelValue': (val: number) => {
if (val)
selectedCount.value = {
product_id: row.original.product_id,
count: val
}
else {
selectedCount.value = {
product_id: null,
count: 0
}
}
},
min: 0,
max: row.original.quantity
})
}
},
{ {
accessorKey: 'count', accessorKey: 'count',
header: '', header: '',
cell: ({ row }) => { cell: ({ row }) => {
return h(UButton, { return h(UButton, {
onClick: () => { onClick: () => {
goToProduct(row.original.product_id) console.log('Clicked', row.original)
}, },
color: 'primary', color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
disabled: selectedCount.value.product_id !== row.original.product_id,
variant: 'solid' variant: 'solid'
}, () => 'Show product') }, 'Add to cart')
},
}
]
const columnsChild: TableColumn<Product>[] = [
{
accessorKey: 'product_id',
header: '',
cell: ({ row }) => h('span', {
class: 'cursor-pointer text-blue-500 hover:underline',
onClick: (e: Event) => {
e.stopPropagation()
goToProduct(row.original.product_id, row.original.image_link)
}
}, `#${row.getValue('product_id') as number}`)
},
{
accessorKey: 'image_link',
header: '',
cell: ({ row }) => {
return h('img', {
src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;'
})
}
},
{
accessorKey: 'name',
header: '',
cell: ({ row }) => h('span', {
class: 'cursor-pointer text-blue-500 hover:underline',
onClick: (e: Event) => {
e.stopPropagation()
goToProduct(row.original.product_id, row.original.image_link)
}
}, row.getValue('name') as string)
},
{
accessorKey: 'quantity',
header: '',
cell: ({ row }) => row.getValue('quantity') as number
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UInputNumber, {
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
'onUpdate:modelValue': (val: number) => {
if (val)
selectedCount.value = {
product_id: row.original.product_id,
count: val
}
else {
selectedCount.value = {
product_id: null,
count: 0
}
}
},
min: 0,
max: row.original.quantity
})
}
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
console.log('Clicked', row.original)
},
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
disabled: selectedCount.value.product_id !== row.original.product_id,
variant: 'solid'
}, 'Add to cart')
}, },
} }
] ]

View File

@@ -1,7 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container my-10 mx-auto "> <div class="container my-10 mx-auto ">
<div <div
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md"> class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
<div class="flex items-end gap-3"> <div class="flex items-end gap-3">
@@ -101,7 +100,6 @@
</div> </div>
<p ref="usageRef" v-html="productStore.productDescription.usage" <p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p> class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div> </div>
<div v-if="activeTab === 'description'" <div v-if="activeTab === 'description'"
@@ -151,7 +149,10 @@ const selectedLanguage = ref('en')
const currentLangId = ref(2) const currentLangId = ref(2)
const productID = ref<number>(0) const productID = ref<number>(0)
// Watch for language changes and refetch product description const imageUrl = computed(() => {
return route.query.image ? String(route.query.image) : ''
})
watch(selectedLanguage, async (newLang: string) => { watch(selectedLanguage, async (newLang: string) => {
if (productID.value) { if (productID.value) {
await fetchForLanguage(newLang) await fetchForLanguage(newLang)
@@ -197,10 +198,14 @@ const originalDescription = ref('')
const originalUsage = ref('') const originalUsage = ref('')
const saveDescription = async () => { const saveDescription = async () => {
descriptionEdit.disableEdit() if (descriptionRef.value) {
await productStore.saveProductDescription(productID.value) productStore.productDescription.description = descriptionRef.value.innerHTML
} }
descriptionEdit.disableEdit()
await productStore.saveProductDescription(productID.value, currentLangId.value)
}
const cancelDescriptionEdit = () => { const cancelDescriptionEdit = () => {
if (descriptionRef.value) { if (descriptionRef.value) {
descriptionRef.value.innerHTML = originalDescription.value descriptionRef.value.innerHTML = originalDescription.value
@@ -224,9 +229,14 @@ const enableEdit = () => {
} }
const saveText = () => { const saveText = () => {
if (usageRef.value) {
productStore.productDescription.usage = usageRef.value.innerHTML
}
usageEdit.disableEdit() usageEdit.disableEdit()
isEditing.value = false isEditing.value = false
productStore.saveProductDescription(productID.value)
productStore.saveProductDescription(productID.value, currentLangId.value)
} }
const cancelEdit = () => { const cancelEdit = () => {
@@ -236,14 +246,5 @@ const cancelEdit = () => {
usageEdit.disableEdit() usageEdit.disableEdit()
isEditing.value = false isEditing.value = false
} }
</script> </script>
<style>
.images {
display: flex;
align-items: center;
gap: 70px;
margin: 20px 0 20px 0;
}
</style>

View File

@@ -0,0 +1,237 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue'
import Default from '@/layouts/default.vue'
const router = useRouter()
const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n()
const searchName = ref('')
const searchCode = ref('')
const priceFromFilter = ref<number | null>(null)
const priceToFilter = ref<number | null>(null)
// Pagination
const page = ref(1)
const pageSize = 5
// Fetch products on mount
// onMounted(() => {
// productStore.getProductDescription(langID: , productID.value)
// })
// Filtered products
// const filteredProducts = computed(() => {
// console.log(productStore.products);
// return productStore.products.filter(product => {
// const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
// const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
// const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
// const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
// return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
// })
// })
// const totalItems = computed(() => filteredProducts.value.length)
// const paginatedProducts = computed(() => {
// const start = (page.value - 1) * pageSize
// const end = start + pageSize
// return filteredProducts.value.slice(start, end)
// })
// Reset page when filters change
function resetPage() {
page.value = 1
}
// Navigate to product detail
function goToProduct(product: Product) {
router.push({ name: 'product-detail', params: { id: product.id } })
}
// Table columns
const columns = computed<TableColumn<Product>[]>(() => [
{
accessorKey: 'image',
header: () => h('div', { class: 'text-center' }, t('products.image')),
cell: ({ row }) => h('img', {
src: row.getValue('image'),
alt: 'Product',
class: 'w-12 h-12 object-cover rounded'
})
},
{
accessorKey: 'name',
header: t('products.product_name'),
cell: ({ row }) => {
const product = row.original
return h('button', {
class: 'text-primary hover:underline font-medium text-left',
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
}, product.name)
}
},
{
accessorKey: 'code',
header: t('products.product_code'),
},
{
accessorKey: 'description',
header: t('products.description'),
cell: ({ row }) => {
const desc = row.getValue('description') as string
return h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, desc?.substring(0, 50) + (desc && desc.length > 50 ? '...' : ''))
}
},
{
accessorKey: 'inStock',
header: t('products.in_stock'),
cell: ({ row }) => {
const inStock = row.getValue('inStock')
return h('span', {
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
}, inStock ? t('products.yes') : t('products.no'))
}
},
{
accessorKey: 'price',
header: t('products.price'),
cell: ({ row }) => {
const priceFromVal = row.original.priceFrom
const priceToVal = row.original.priceTo
return `${priceFromVal} - ${priceToVal}`
}
},
{
accessorKey: 'count',
header: t('products.count'),
},
{
id: 'actions',
header: '',
cell: ({ row }) => {
const product = row.original
return h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
}, t('products.add_to_cart')),
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
}, '+')
])
}
}
])
// Actions
function addToCart(product: Product) {
console.log('Add to cart:', product)
}
function incrementCount(product: Product) {
product.count++
}
function clearFilters() {
searchName.value = ''
searchCode.value = ''
priceFromFilter.value = null
priceToFilter.value = null
resetPage()
}
</script>
<template>
<component :is="Default || 'div'">
<div class="container">
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<div>
</div>
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
{{ t('products.login_to_view') }}
</div>
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
<div
class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name')
}}</label>
<UInput v-model="searchName" :placeholder="t('products.search_name_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code')
}}</label>
<UInput v-model="searchCode" :placeholder="t('products.search_code_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
<UInput v-model="priceFromFilter" type="number" :placeholder="t('products.price_from')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
<UInput v-model="priceToFilter" type="number" :placeholder="t('products.price_to')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex items-end">
<button @click="clearFilters"
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
{{ t('products.clear_filters') }}
</button>
</div>
</div>
<!-- Products Table -->
<!-- <div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
<UTable
:data="paginatedProducts"
:columns="columns"
class="dark:text-white! text-dark"
/>
</div> -->
<!-- Empty State -->
<!-- <div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
{{ t('products.no_products') }}
</div> -->
<!-- Pagination -->
<!-- <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination
v-model:page="page"
:page-count="pageSize"
:total="totalItems"
/>
</div> -->
<!-- Results count -->
<!-- <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
{{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
</div> -->
</div>
</div>
</div>
</component>
</template>

View File

@@ -0,0 +1,233 @@
<template>
<div class="relative">
<button
@click="toggleDropdown"
class="flex items-center gap-2 px-4 py-2 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<UIcon name="mdi:cart" class="text-lg" />
<span class="text-black dark:text-white font-medium">{{ activeCartName }}</span>
<UIcon :name="isOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'" class="text-lg" />
</button>
<div
v-if="isOpen"
class="absolute top-full right-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-(--border-light) dark:border-(--border-dark) rounded-lg shadow-lg z-50"
>
<div class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
<div
v-for="cart in carts"
:key="cart.id"
class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': cart.id === activeCartId }"
>
<button
@click="selectCart(cart.id)"
class="flex-1 text-left"
>
<span class="text-black dark:text-white">{{ cart.name }}</span>
<span class="text-gray-500 dark:text-gray-400 text-sm ml-2">({{ cart.items.length }})</span>
</button>
<div class="flex items-center gap-1">
<button
@click.stop="startEditing(cart)"
class="p-1.5 text-gray-500 hover:text-(--accent-blue-light) dark:hover:text-(--accent-blue-dark) hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
:title="t('Edit')"
>
<UIcon name="mdi:pencil" class="text-base" />
</button>
<button
@click.stop="confirmDelete(cart)"
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
:title="t('Delete')"
>
<UIcon name="mdi:delete" class="text-base" />
</button>
</div>
</div>
</div>
<div class="p-3 border-t border-(--border-light) dark:border-(--border-dark)">
<button
@click="showCreateModal = true"
class="w-full flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:bg-gray-50 dark:hover:bg-gray-700 p-2 rounded"
>
<UIcon name="mdi:plus" class="text-lg" />
<span>{{ t('Create New Cart') }}</span>
</button>
</div>
</div>
<div
v-if="isOpen"
@click="closeDropdown"
class="fixed inset-0 z-40"
/>
</div>
<div
v-if="showCreateModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="showCreateModal = false"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Create New Cart') }}</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Cart Name') }}
</label>
<UInput
v-model="newCartName"
:placeholder="t('Enter cart name')"
class="w-full"
/>
</div>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="showCreateModal = false"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="primary"
@click="createNewCart"
:disabled="!newCartName.trim()"
>
{{ t('Create and Continue') }}
</UButton>
</div>
</div>
</div>
<div
v-if="editingCart"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="editingCart = null"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Edit Cart Name') }}</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Cart Name') }}
</label>
<UInput
v-model="editCartName"
:placeholder="t('Enter cart name')"
class="w-full"
/>
</div>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="editingCart = null"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="primary"
@click="saveEditedCart"
:disabled="!editCartName.trim()"
>
{{ t('Save') }}
</UButton>
</div>
</div>
</div>
<div
v-if="deletingCart"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="deletingCart = null"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delete Cart') }}</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
{{ t('Are you sure you want to delete') }} "{{ deletingCart.name }}"?
</p>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="deletingCart = null"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="error"
@click="confirmDeleteCart"
>
{{ t('Delete') }}
</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
const cartStore = useCartStore()
const { t } = useI18n()
const isOpen = ref(false)
const showCreateModal = ref(false)
const newCartName = ref('')
const editingCart = ref<{ id: string; name: string } | null>(null)
const editCartName = ref('')
const deletingCart = ref<{ id: string; name: string } | null>(null)
const carts = computed(() => cartStore.carts)
const activeCartId = computed(() => cartStore.activeCartId)
const activeCartName = computed(() => {
const cart = cartStore.activeCart
return cart ? cart.name : t('Select Cart')
})
function toggleDropdown() {
isOpen.value = !isOpen.value
}
function closeDropdown() {
isOpen.value = false
}
function selectCart(cartId: string) {
cartStore.setActiveCart(cartId)
closeDropdown()
}
function startEditing(cart: { id: string; name: string }) {
editingCart.value = { id: cart.id, name: cart.name }
editCartName.value = cart.name
}
function saveEditedCart() {
if (editingCart.value && editCartName.value.trim()) {
cartStore.renameCart(editingCart.value.id, editCartName.value.trim())
editingCart.value = null
}
}
function confirmDelete(cart: { id: string; name: string }) {
deletingCart.value = cart
}
function confirmDeleteCart() {
if (deletingCart.value) {
cartStore.deleteCart(deletingCart.value.id)
deletingCart.value = null
}
}
function createNewCart() {
if (newCartName.value.trim()) {
cartStore.createCart(newCartName.value.trim())
newCartName.value = ''
showCreateModal.value = false
}
}
</script>

View File

@@ -0,0 +1,198 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<h2
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
{{ t('Selected Products') }}
</h2>
<div v-if="cartStore.items.length > 0">
<div v-for="item in cartStore.items" :key="item.id"
class="grid grid-cols-5 items-center p-4 border-b border-(--border-light) dark:border-(--border-dark) w-[100%]">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
</div>
<p class="text-black dark:text-white text-sm font-medium">{{ item.name }}</p>
<p class="text-black dark:text-white">${{ item.price.toFixed(2) }}</p>
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
}}</p>
<div class="flex items-center justify-end gap-10">
<UInputNumber v-model="item.quantity" :min="1"
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
<div class="flex justify-center">
<button @click="removeItem(item.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[20px]" />
</button>
</div>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
<RouterLink :to="{ name: 'product-card-full' }"
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Continue Shopping') }}
</RouterLink>
</div>
</div>
</div>
<div class="lg:w-80">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Shipping') }}</span>
<span class="text-black dark:text-white">
{{ cartStore.shippingCost > 0 ? `$${cartStore.shippingCost.toFixed(2)}` : t('Free') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
}}%)</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div>
</div>
<div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
cartStore.orderTotal.toFixed(2) }}</span>
</div>
<div class="flex flex-col gap-3">
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light) disabled:opacity-50 disabled:cursor-not-allowed">
{{ t('Place Order') }}
</UButton>
<UButton block variant="outline" color="neutral" @click="cancelOrder"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
<div class="mb-4">
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
</div>
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress"
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}</p>
</div>
</label>
</div>
<div v-else class="text-center py-6">
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }"
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Add Address') }}
</RouterLink>
</div>
</div>
</div>
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
<div class="space-y-3">
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-black dark:text-white font-medium">{{ method.name }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium">
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
</span>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ method.description }}</p>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useAddressStore } from '@/stores/address'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const addressStore = useAddressStore()
const { t } = useI18n()
const router = useRouter()
const selectedAddress = ref<number | null>(cartStore.selectedAddressId)
const selectedDeliveryMethod = ref<number | null>(cartStore.selectedDeliveryMethodId)
const addressSearchQuery = ref('')
watch(addressSearchQuery, (val) => {
addressStore.setSearchQuery(val)
})
watch(selectedAddress, (newValue) => {
cartStore.setSelectedAddress(newValue)
})
watch(selectedDeliveryMethod, (newValue) => {
if (newValue) {
cartStore.setDeliveryMethod(newValue)
}
})
const canPlaceOrder = computed(() => {
return cartStore.items.length > 0 &&
cartStore.selectedAddressId !== null &&
cartStore.selectedDeliveryMethodId !== null
})
function removeItem(itemId: number) {
cartStore.removeItem(itemId)
}
function placeOrder() {
if (canPlaceOrder.value) {
router.push({ name: 'checkout' })
}
}
function cancelOrder() {
router.back()
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20">
<div class="max-w-2xl mx-auto">
<div class="grid grid-cols-3 pb-6">
<button variant="outline" color="neutral" @click="goBackToCart"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) flex items-center gap-2">
<UIcon name="mdi:arrow-left" />
{{ t('Back') }}
</button>
<h2 class="font-semibold text-black dark:text-white text-2xl text-center">
{{ t('Checkout') }}
</h2>
</div>
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden mb-6">
<div class="p-6">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
{{ t('Order Summary') }}
</h3>
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products') }}</span>
<span class="text-black dark:text-white">{{ cartStore.items.length }} {{ t('items')
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2)
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }}</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div>
</div>
<div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">
${{ cartStore.orderTotal.toFixed(2) }}
</span>
</div>
<div class="space-y-2 mb-4">
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center gap-3 text-sm">
<div
class="w-10 h-10 bg-white dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name"
class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-lg text-gray-400" />
</div>
<span class="text-black dark:text-white flex-1 truncate">{{ item.name }}</span>
<span class="text-gray-600 dark:text-gray-400">x{{ item.quantity }}</span>
<span class="text-black dark:text-white">${{ (item.price * item.quantity).toFixed(2)
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const { t } = useI18n()
const router = useRouter()
function goBackToCart() {
router.push({ name: 'cart' })
}
</script>

View File

@@ -19,7 +19,7 @@
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<div class="flex gap-2"> <div class="flex gap-2">
<CategoryMenu /> <CategoryMenuListing />
<UTable :data="productsList" :columns="columns" class="flex-1"> <UTable :data="productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }"> <template #expanded="{ row }">
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{ <UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
@@ -46,7 +46,7 @@ import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/categoryMenu.vue' import CategoryMenuListing from '../inner/categoryMenuListing.vue'
interface Product { interface Product {
reference: number reference: number

View File

@@ -1,57 +1,20 @@
<template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" />
</template>
<script setup lang="ts"> <script setup lang="ts">
import { getMenu } from '@/router/menu' import { getMenu } from '@/router/menu'
import type { NavigationMenuItem } from '@nuxt/ui'; import type { NavigationMenuItem } from '@nuxt/ui';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router';
let menu = await getMenu() as NavigationMenuItem[] let menu = await getMenu() as NavigationMenuItem[]
const openAll = ref(false) const openAll = ref(false)
const route = useRoute()
let categoryId = ref(route.params.category_id)
function findPath(tree: NavigationMenuItem[], id: number, path: Array<number> = []): Array<number> | null {
for (let item of tree) {
let newPath: Array<number> = [...path, item.category_id]
if (item.category_id === id) {
return newPath
}
if (item.children) {
const result: Array<number> | null = findPath(item.children, id, newPath)
if (result) {
return result
}
}
}
return null
}
let path = findPath(menu, Number(categoryId.value))
function adaptMenu(menu: NavigationMenuItem[]) { function adaptMenu(menu: NavigationMenuItem[]) {
for (const item of menu) { for (const item of menu) {
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
item.open = path && path.includes(item.category_id) ? true : openAll.value console.log(item);
adaptMenu(item.children); adaptMenu(item.children);
item.children.unshift({ item.open = openAll.value
label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
name: 'customer-products-category', params: {
category_id: item.params.category_id,
link_rewrite: item.params.link_rewrite
}
}
})
} else { } else {
item.to = { item.to = { name: 'category', params: item.params };
name: 'customer-products-category', params: {
category_id: item.params.category_id,
link_rewrite: item.params.link_rewrite
}
};
item.icon = 'i-lucide-file-text' item.icon = 'i-lucide-file-text'
} }
} }
@@ -59,6 +22,7 @@ function adaptMenu(menu: NavigationMenuItem[]) {
} }
menu = adaptMenu(menu) menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([ const items = ref<NavigationMenuItem[][]>([
[ [
...menu as NavigationMenuItem[] ...menu as NavigationMenuItem[]
@@ -66,3 +30,7 @@ const items = ref<NavigationMenuItem[][]>([
]) ])
</script> </script>
<template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import { getMenu } from '@/router/menu'
import type { NavigationMenuItem } from '@nuxt/ui';
import { ref } from 'vue';
let menu = await getMenu() as NavigationMenuItem[]
const openAll = ref(false)
function adaptMenu(menu: NavigationMenuItem[]) {
for (const item of menu) {
if (item.children && item.children.length > 0) {
adaptMenu(item.children);
item.open = openAll.value
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
} else {
item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([
[
...menu as NavigationMenuItem[]
],
])
</script>
<template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
</template>

View File

@@ -59,6 +59,7 @@ async function setRoutes() {
const componentName = item.component const componentName = item.component
const [, folder] = componentName.split('/') const [, folder] = componentName.split('/')
const componentPath = `/src${componentName}` const componentPath = `/src${componentName}`
console.log(componentPath);
let modules = let modules =
@@ -113,3 +114,4 @@ router.beforeEach((to, from) => {
}) })
export default router export default router

View File

@@ -1,15 +1,8 @@
import { useFetchJson } from "@/composable/useFetchJson"; import { useFetchJson } from "@/composable/useFetchJson";
import type { MenuItem, Route } from "@/types/menu"; import type { MenuItem, Route } from "@/types/menu";
import { ref } from "vue";
import { settings } from "./settings";
const categoryId = ref()
export const getMenu = async () => { export const getMenu = async () => {
if(!categoryId.value){ const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-category-tree');
categoryId.value = settings['app'].category_tree_root_id
}
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${categoryId.value}`);
return resp.items.children return resp.items.children
} }

View File

@@ -18,8 +18,16 @@ export interface DeliveryMethod {
description: string description: string
} }
export interface Cart {
id: string
name: string
items: CartItem[]
}
export const useCartStore = defineStore('cart', () => { export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]) const carts = ref<Cart[]>([])
const activeCartId = ref<string | null>(null)
const selectedAddressId = ref<number | null>(null) const selectedAddressId = ref<number | null>(null)
const selectedDeliveryMethodId = ref<number | null>(null) const selectedDeliveryMethodId = ref<number | null>(null)
const shippingCost = ref(0) const shippingCost = ref(0)
@@ -31,13 +39,15 @@ export const useCartStore = defineStore('cart', () => {
{ id: 3, name: 'Priority Delivery', price: 30, description: 'Next business day' } { id: 3, name: 'Priority Delivery', price: 30, description: 'Next business day' }
]) ])
function initMockData() { const items = computed(() => {
items.value = [ if (!activeCartId.value) return []
{ id: 1, productId: 101, name: 'Premium Widget Pro', product_number: 'NC209/7000', image: '/img/product-1.jpg', price: 129.99, quantity: 2 }, const cart = carts.value.find(c => c.id === activeCartId.value)
{ id: 2, productId: 102, name: 'Ultra Gadget X', product_number: 'NC234/6453', image: '/img/product-2.jpg', price: 89.50, quantity: 1 }, return cart ? cart.items : []
{ id: 3, productId: 103, name: 'Mega Tool Set', product_number: 'NC324/9030', image: '/img/product-3.jpg', price: 249.00, quantity: 3 } })
]
} const activeCart = computed(() => {
return carts.value.find(c => c.id === activeCartId.value) || null
})
const productsTotal = computed(() => { const productsTotal = computed(() => {
return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0) return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
@@ -55,8 +65,63 @@ export const useCartStore = defineStore('cart', () => {
return items.value.reduce((sum, item) => sum + item.quantity, 0) return items.value.reduce((sum, item) => sum + item.quantity, 0)
}) })
function createCart(name: string): Cart {
const newCart: Cart = {
id: `cart-${Date.now()}`,
name,
items: []
}
carts.value.push(newCart)
activeCartId.value = newCart.id
return newCart
}
function deleteCart(cartId: string) {
const index = carts.value.findIndex(c => c.id === cartId)
if (index !== -1) {
carts.value.splice(index, 1)
if (activeCartId.value === cartId) {
const firstCart = carts.value[0]
activeCartId.value = firstCart ? firstCart.id : null
}
}
}
function renameCart(cartId: string, newName: string) {
const cart = carts.value.find(c => c.id === cartId)
if (cart) {
cart.name = newName
}
}
function setActiveCart(cartId: string) {
const cart = carts.value.find(c => c.id === cartId)
if (cart) {
activeCartId.value = cartId
}
}
function addItemToActiveCart(item: CartItem) {
if (!activeCartId.value) {
createCart('Cart 1')
}
const cart = carts.value.find(c => c.id === activeCartId.value)
if (cart) {
const existingItem = cart.items.find(i => i.productId === item.productId)
if (existingItem) {
existingItem.quantity += item.quantity
} else {
cart.items.push(item)
}
}
}
function updateQuantity(itemId: number, quantity: number) { function updateQuantity(itemId: number, quantity: number) {
const item = items.value.find(i => i.id === itemId) const cart = carts.value.find(c => c.id === activeCartId.value)
if (!cart) return
const item = cart.items.find(i => i.id === itemId)
if (item) { if (item) {
if (quantity <= 0) { if (quantity <= 0) {
removeItem(itemId) removeItem(itemId)
@@ -67,12 +132,14 @@ export const useCartStore = defineStore('cart', () => {
} }
function deleteProduct(id: number): boolean { function deleteProduct(id: number): boolean {
const index = items.value.findIndex(a => a.id === id) const cart = carts.value.find(c => c.id === activeCartId.value)
if (!cart) return false
const index = cart.items.findIndex(a => a.id === id)
if (index === -1) return false if (index === -1) return false
items.value.splice(index, 1) cart.items.splice(index, 1)
resetProductPagination() resetProductPagination()
return true return true
} }
@@ -81,14 +148,20 @@ export const useCartStore = defineStore('cart', () => {
} }
function removeItem(itemId: number) { function removeItem(itemId: number) {
const index = items.value.findIndex(i => i.id === itemId) const cart = carts.value.find(c => c.id === activeCartId.value)
if (!cart) return
const index = cart.items.findIndex(i => i.id === itemId)
if (index !== -1) { if (index !== -1) {
items.value.splice(index, 1) cart.items.splice(index, 1)
} }
} }
function clearCart() { function clearCart() {
items.value = [] const cart = carts.value.find(c => c.id === activeCartId.value)
if (cart) {
cart.items = []
}
selectedAddressId.value = null selectedAddressId.value = null
selectedDeliveryMethodId.value = null selectedDeliveryMethodId.value = null
shippingCost.value = 0 shippingCost.value = 0
@@ -106,9 +179,40 @@ export const useCartStore = defineStore('cart', () => {
} }
} }
function initMockData() {
const cart1: Cart = {
id: 'cart-1',
name: 'Cart 1',
items: [
{ id: 1, productId: 101, name: 'Premium Widget Pro', product_number: 'NC209/7000', image: '/img/product-1.jpg', price: 129.99, quantity: 2 },
{ id: 2, productId: 102, name: 'Ultra Gadget X', product_number: 'NC234/6453', image: '/img/product-2.jpg', price: 89.50, quantity: 1 }
]
}
const cart2: Cart = {
id: 'cart-2',
name: 'Cart 2',
items: [
{ id: 3, productId: 103, name: 'Mega Tool Set', product_number: 'NC324/9030', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
]
}
const cart3: Cart = {
id: 'cart-3',
name: 'Cart 3',
items: []
}
carts.value = [cart1, cart2, cart3]
activeCartId.value = 'cart-1'
}
initMockData() initMockData()
return { return {
carts,
activeCartId,
activeCart,
items, items,
selectedAddressId, selectedAddressId,
selectedDeliveryMethodId, selectedDeliveryMethodId,
@@ -119,8 +223,14 @@ export const useCartStore = defineStore('cart', () => {
vatAmount, vatAmount,
orderTotal, orderTotal,
itemCount, itemCount,
deleteProduct,
createCart,
deleteCart,
renameCart,
setActiveCart,
addItemToActiveCart,
updateQuantity, updateQuantity,
deleteProduct,
removeItem, removeItem,
clearCart, clearCart,
setSelectedAddress, setSelectedAddress,

View File

@@ -13,24 +13,24 @@ const products = [
// type CategoryProducts = {} // type CategoryProducts = {}
export const useCategoryStore = defineStore('category', () => { export const useCategoryStore = defineStore('category', () => {
const idCategory = ref(0) const id_category = ref(0)
const categoryProducts = ref(products) const categoryProducts = ref(products)
function setCategoryID(id: number) { function setCategoryID(id: number) {
idCategory.value = id id_category.value = id
} }
async function getCategoryProducts() { async function getCategoryProducts() {
return new Promise<typeof products>((resolve) => { return new Promise<typeof products>((resolve) => {
setTimeout(() => { setTimeout(() => {
// console.log('Fetching products from category id: ', idCategory.value); console.log('Fetching products from category id: ', id_category.value);
resolve(categoryProducts.value) resolve(categoryProducts.value)
}, 2000 * Math.random()) }, 2000 * Math.random())
}) })
} }
return { return {
idCategory, id_category,
getCategoryProducts, getCategoryProducts,
setCategoryID setCategoryID
} }

View File

@@ -37,6 +37,7 @@ export const useProductStore = defineStore('product', () => {
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}` `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
) )
productDescription.value = response.items productDescription.value = response.items
console.log(productDescription, 'dfsfsdf');
} catch (e: unknown) { } catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load product description' error.value = e instanceof Error ? e.message : 'Failed to load product description'
@@ -44,22 +45,32 @@ export const useProductStore = defineStore('product', () => {
loading.value = false loading.value = false
} }
} }
function stripHtml(html: string) {
async function saveProductDescription(productID?: number) { const div = document.createElement('div')
div.innerHTML = html
return div.textContent || div.innerText || ''
}
async function saveProductDescription(productID?: number, langId?: number) {
const id = productID || 1 const id = productID || 1
const lang = langId || 1
try { try {
const data = await useFetchJson( const data = await useFetchJson(
`/api/v1/restricted/product-description/save-product-description?productID=${id}&productShopID=1&productLangID=1`, `/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
{ {
method: 'POST', method: 'POST',
body: JSON.stringify( headers: {
{ 'Content-Type': 'application/json'
description: productDescription.value.description, },
description_short: productDescription.value.description_short, body: JSON.stringify({
meta_description: productDescription.value.meta_description, name: stripHtml(productDescription.value?.name || ''),
available_now: productDescription.value.available_now, description: stripHtml(productDescription.value?.description || ''),
usage: productDescription.value.usage description_short: stripHtml(productDescription.value?.description_short || ''),
}) meta_title: stripHtml(productDescription.value?.meta_title || ''),
meta_description: stripHtml(productDescription.value?.meta_description || ''),
available_now: stripHtml(productDescription.value?.available_now || ''),
available_later: stripHtml(productDescription.value?.available_later || ''),
usage: stripHtml(productDescription.value?.usage || '')
})
} }
) )
return data return data
@@ -68,12 +79,12 @@ export const useProductStore = defineStore('product', () => {
} }
} }
async function translateProductDescription(productID: number, fromLangId: number, toLangId: number) { async function translateProductDescription(productID: number, fromLangId: number, toLangId: number, model: string = 'OpenAI') {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-description/translate-product-description?productID=${productID}&productShopID=1&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=OpenAI`) const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=${model}`)
productDescription.value = response.items productDescription.value = response.items
return response.items return response.items
} catch (e: any) { } catch (e: any) {

View File

@@ -1,4 +1,4 @@
export interface ProductDescription { export interface ProductDescription {
id?: number id?: number
name?: string name?: string
description: string description: string
@@ -7,11 +7,3 @@ export interface ProductDescription {
available_now: string available_now: string
usage: string usage: string
} }
export interface Product {
reference: number
product_id: number
name: string
image_link: string
link_rewrite: string
}

View File

@@ -7,7 +7,6 @@ export interface Settings {
} }
export interface App { export interface App {
category_tree_root_id: number
name: string name: string
environment: string environment: string
base_url: string base_url: string

View File

@@ -1,7 +1,7 @@
info: info:
name: add-new-cart name: add-new-cart
type: http type: http
seq: 1 seq: 11
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: 1 seq: 16
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: 14 seq: 15
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: 1 seq: 12
http: http:
method: GET method: GET

View File

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

View File

@@ -1,7 +1,7 @@
info: info:
name: get-breadcrumb name: get-breadcrumb
type: http type: http
seq: 1 seq: 18
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: 1 seq: 5
http: http:
method: GET method: GET

View File

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

View File

@@ -1,17 +1,17 @@
info: info:
name: get-product-description name: get-product-description
type: http type: http
seq: 1 seq: 17
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=1 url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=4
params: params:
- name: productID - name: productID
value: "51" value: "51"
type: query type: query
- name: productLangID - name: productLangID
value: "1" value: "4"
type: query type: query
auth: inherit auth: inherit

View File

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

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10&target_user_id=2 url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10
params: params:
- name: p - name: p
value: "1" value: "1"
@@ -13,9 +13,6 @@ http:
- name: elems - name: elems
value: "10" value: "10"
type: query type: query
- name: target_user_id
value: "2"
type: query
settings: settings:
encodeUrl: true encodeUrl: true

View File

@@ -1,7 +1,7 @@
info: info:
name: list-users name: list-users
type: http type: http
seq: 1 seq: 2
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: 1 seq: 8
http: http:
method: DELETE method: DELETE

View File

@@ -1,7 +1,7 @@
info: info:
name: retrieve-cart name: retrieve-cart
type: http type: http
seq: 1 seq: 14
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: 1 seq: 13
http: http:
method: GET method: GET

View File

@@ -1,28 +0,0 @@
info:
name: save-product-description
type: http
seq: 19
http:
method: POST
url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=3
params:
- name: productID
value: "1"
type: query
- name: productLangID
value: "3"
type: query
body:
type: json
data: |-
{
"description": "<p>Der Einsatz von Rehabilitationsrollen in verschiedenen Übungen und Behandlungen wirkt sich positiv auf die Reduzierung von Verletzungen und die Genesungschancen aus. Sie werden in der Rehabilitation, bei Korrekturgymnastik sowie in der traditionellen und Sportmassage eingesetzt, da sie ideal zum Anheben und Spreizen von Gliedmaßen geeignet sind. Zudem können sie zur Unterstützung von Knien, Füßen, Armen und Schultern verwendet werden. Auch für Kinder sind Rehabilitationsrollen empfehlenswert; ihre spielerische Anwendung fördert die Entwicklung der Grobmotorik.</p><p> Dank der großen Auswahl an Farben und Größen lässt sich ein Übungsset zusammenstellen, das in jeder Physiotherapiepraxis, jedem Massageraum, jeder Schule oder jedem Kindergarten benötigt wird.</p><p> Die Rehabilitationsrolle ist ein Medizinprodukt, das den grundlegenden Anforderungen an Medizinprodukte und den Bestimmungen des Medizinproduktegesetzes entspricht, im Register für Medizinprodukte des Amtes für die Registrierung von Arzneimitteln, Medizinprodukten und Biozidprodukten eingetragen ist, mit der Konformitätserklärung des Herstellers versehen ist und das CE-Zeichen trägt. </p><p></p><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/images.jpg\" alt=\"Medizinprodukt\" style=\"margin-left:auto;margin-right:auto;\" width=\"253\" height=\"86\" /></p><h4> <strong>Empfohlene Verwendung:</strong></h4><ul style=\"list-style-type:circle;\"><li> in der Rehabilitation</li><li> während Massagen (traditionell, Sport)</li><li> in der Korrekturgymnastik (insbesondere für Kinder)</li><li> zur Linderung von Verletzungen einzelner Körperteile</li><li> Zur Unterstützung von: Knien, Knöcheln, Kopf des Patienten</li><li> bei Übungen zur Entwicklung der motorischen Fähigkeiten von Kindern</li><li> in Schönheitssalons</li><li> in Kinderspielzimmern</li></ul><p></p><h4> <strong>Materialspezifikationen:</strong></h4><p> <strong>Abdeckung:</strong> PVC-beschichtetes Material, das für medizinische Geräte vorgesehen ist und daher sehr leicht zu reinigen und zu desinfizieren ist:</p><ul style=\"list-style-type:circle;\"><li> Material gemäß REACH-Verordnung, zertifiziert mit dem STANDARD 100 Zertifikat von OEKO-TEX®.</li><li> Enthält keine Phthalate</li><li> feuerfest</li><li> resistent gegenüber physiologischen Flüssigkeiten (Blut, Urin, Schweiß) und Alkohol</li><li> UV-beständig, daher auch für den Einsatz im Freien geeignet.</li><li> kratzfest</li><li> ölbeständig </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/reach.jpg\" alt=\"ERREICHEN\" width=\"115\" height=\"115\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"116\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/phthalate-free.jpg\" alt=\"Enthält keine Phthalate\" width=\"112\" height=\"111\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/fireresistant.jpg\" alt=\"Feuerfest\" width=\"114\" height=\"113\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-alkohol.jpg\" alt=\"Alkoholbeständig\" width=\"114\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-uv.jpg\" alt=\"UV-beständig\" width=\"117\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/outdoor.jpg\" alt=\"Für den Einsatz im Freien konzipiert\" width=\"116\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-zadrapania.jpg\" alt=\"Kratzfest\" width=\"97\" height=\"96\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/olejoodporny.jpg\" alt=\"Ölbeständig\" width=\"99\" height=\"98\" /></p><p> <strong>Füllung:</strong> mittelharter Polyurethanschaum mit erhöhter Verformungsbeständigkeit:</p><ul style=\"list-style-type:circle;\"><li> besitzt ein Hygienezertifikat, ausgestellt vom Institut für Maritime und Tropenmedizin in Gdynia</li><li> zertifiziert mit dem STANDARD 100 by OEKO-TEX® Zertifikat Produktklasse I, ausgestellt vom Textilforschungsinstitut in Łódź</li><li> Hergestellt aus hochwertigen Rohstoffen, die die Ozonschicht nicht schädigen. </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"95\" height=\"95\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/Logo_GUMed_kolor-180x180.jpg\" alt=\"Hygienezertifikat\" width=\"94\" height=\"94\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/atest_higieniczny_kolor.jpg\" alt=\"Hygienezertifikat\" width=\"79\" height=\"94\" /></p><p></p><p></p>"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,11 +1,11 @@
info: info:
name: search name: search
type: http type: http
seq: 1 seq: 10
http: http:
method: GET method: GET
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 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
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: 1 seq: 6
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/search/test url: http://localhost:3000/api/v1/restricted/meili-search/test
auth: inherit auth: inherit
settings: settings:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,28 +0,0 @@
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,7 +0,0 @@
info:
name: search
type: folder
seq: 6
request:
auth: inherit

View File

@@ -1,19 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,15 +0,0 @@
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

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

View File

@@ -1,15 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,15 +0,0 @@
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

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

4
go.mod
View File

@@ -36,8 +36,6 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
@@ -100,7 +98,7 @@ require (
github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xyproto/randomstring v1.2.0 // indirect github.com/xyproto/randomstring v1.2.0 // indirect
golang.org/x/net v0.52.0 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 // indirect

6
go.sum
View File

@@ -72,8 +72,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
@@ -136,8 +134,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
@@ -158,8 +154,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View File

@@ -69,8 +69,6 @@ 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 BIGINT NULL DEFAULT 2, lang_id BIGINT NULL DEFAULT 2,
@@ -86,8 +84,6 @@ 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);
-- customer_carts -- customer_carts
CREATE TABLE IF NOT EXISTS b2b_customer_carts ( CREATE TABLE IF NOT EXISTS b2b_customer_carts (

View File