33 Commits

Author SHA1 Message Date
Daniel Goc
7eee0bd032 rebuilt storage 2026-04-08 13:09:19 +02:00
Daniel Goc
f6b321b602 a few fixes for user teleportation 2026-04-03 13:55:57 +02:00
Daniel Goc
af91842b14 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into storage 2026-04-03 13:29:06 +02:00
04e238fd66 Merge pull request 'user_teleport' (#50) from user_teleport into main
Reviewed-on: #50
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-03 11:27:11 +00:00
Daniel Goc
e0c53c97ba Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into user_teleport 2026-04-03 13:01:37 +02:00
09a77c14c9 Merge pull request 'add image link (large_default) to product description' (#49) from add_image_link into main
Reviewed-on: #49
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-03 10:58:21 +00:00
Daniel Goc
c7533a8deb add image link (large_default) to product description 2026-04-03 12:24:05 +02:00
Daniel Goc
1bab7f642f typo 2026-04-03 11:44:15 +02:00
Daniel Goc
a988bbbc33 added copying and moving 2026-04-03 11:25:16 +02:00
Daniel Goc
395d670298 add storage to .gitignore 2026-04-02 14:00:58 +02:00
Daniel Goc
7d4242abb1 move path to params 2026-04-02 13:52:50 +02:00
Daniel Goc
9c7eb5ee4e Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into storage 2026-04-02 11:31:39 +02:00
Daniel Goc
833f4a5a07 deleting and uploading files 2026-04-02 11:26:58 +02:00
Daniel Goc
b9bc121d43 getting to upload 2026-04-02 10:27:14 +02:00
Daniel Goc
b2acb8c922 storage 2026-04-01 13:30:54 +02:00
cf4d14a3cb Merge pull request 'front-styles' (#44) from front-styles into main
Reviewed-on: #44
2026-04-01 07:32:26 +00:00
30eb82ba53 fix: categories 2026-04-01 09:10:38 +02:00
a2a2c35ab3 Merge remote-tracking branch 'origin' into front-styles 2026-04-01 09:10:18 +02:00
Daniel Goc
03f04b2f53 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into user_teleport 2026-03-31 16:57:44 +02:00
Daniel Goc
55da953f32 add teleporting 2026-03-31 16:56:05 +02:00
684f910090 Merge pull request 'expand_get_menu' (#42) from expand_get_menu into main
Reviewed-on: #42
Reviewed-by: Marek Goc <goc_marek@ma-al.com>
2026-03-31 14:55:35 +00:00
5feaa9e15c Merge remote-tracking branch 'origin/expand_get_menu' into front-styles 2026-03-31 14:34:14 +02:00
Daniel Goc
04e2549a66 missing / in ImageLink 2026-03-31 14:30:47 +02:00
fb4f7048ab fix: requests 2026-03-31 12:44:02 +02:00
Daniel Goc
a3f01eca7c misspell fix 2026-03-31 12:27:31 +02:00
Daniel Goc
1fa6206b75 update openapi and add the exists_in_database flag to get-product 2026-03-31 12:00:30 +02:00
Daniel Goc
fa89723eb6 add get-breadcrumb endpoint 2026-03-31 11:40:57 +02:00
Daniel Goc
8665c566ee added new category error, and some fixes 2026-03-31 10:52:36 +02:00
ec5ff123ac Merge pull request 'front-styles' (#38) from front-styles into main
Reviewed-on: #38
2026-03-31 07:30:33 +00:00
17317e778c Merge remote-tracking branch 'origin' into front-styles 2026-03-31 09:19:06 +02:00
94291ccc03 Merge pull request 'improved JWTToken update, added list-users endpoint, debug of getCountries' (#37) from list_users into main
Reviewed-on: #37
Reviewed-by: Marek Goc <goc_marek@ma-al.com>
2026-03-31 07:08:15 +00:00
91c5de1f67 fix: menu and routing 2026-03-30 16:39:14 +02:00
Daniel Goc
d0ce65c287 improved JWTToken update, added list-users endpoint, debug of getCountries 2026-03-30 16:19:26 +02:00
120 changed files with 3415 additions and 983 deletions

6
.env
View File

@@ -48,6 +48,10 @@ EMAIL_FROM=test@ma-al.com
EMAIL_FROM_NAME=Gitea Manager EMAIL_FROM_NAME=Gitea Manager
EMAIL_ADMIN=goc_marek@ma-al.pl EMAIL_ADMIN=goc_marek@ma-al.pl
# STORAGE
STORAGE_ROOT=./storage
I18N_LANGS=en,pl,cs I18N_LANGS=en,pl,cs
PDF_SERVER_URL=http://localhost:8000 PDF_SERVER_URL=http://localhost:8000
@@ -58,3 +62,5 @@ FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
CORS_ORGIN=https://www.naluconcept.com CORS_ORGIN=https://www.naluconcept.com
DSN=root:Maal12345678@tcp(localhost:3306)/nalu

4
.gitignore vendored
View File

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

View File

@@ -1127,21 +1127,32 @@
} }
} }
}, },
"/api/v1/restricted/menu/get-menu": { "/api/v1/restricted/menu/get-category-tree": {
"get": { "get": {
"tags": ["Menu"], "tags": ["Menu"],
"summary": "Get menu structure", "summary": "Get category tree",
"description": "Returns the menu structure for the current language. Requires authentication.", "description": "Returns the category tree rooted at the given category ID for the current language. Requires authentication.",
"operationId": "getMenu", "operationId": "getCategoryTree",
"security": [ "security": [
{ {
"CookieAuth": [], "CookieAuth": [],
"BearerAuth": [] "BearerAuth": []
} }
], ],
"parameters": [
{
"name": "root_category_id",
"in": "query",
"description": "Root category ID to build the tree from",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Menu retrieved successfully", "description": "Category tree retrieved successfully",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1151,7 +1162,73 @@
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Invalid request or root category not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/menu/get-breadcrumb": {
"get": {
"tags": ["Menu"],
"summary": "Get breadcrumb",
"description": "Returns the breadcrumb path from the root category to the specified category for the current language. Requires authentication.",
"operationId": "getBreadcrumb",
"security": [
{
"CookieAuth": [],
"BearerAuth": []
}
],
"parameters": [
{
"name": "root_category_id",
"in": "query",
"description": "Root category ID (breadcrumb starting point)",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "category_id",
"in": "query",
"description": "Target category ID (breadcrumb destination)",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Breadcrumb retrieved successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponse"
}
}
}
},
"400": {
"description": "Invalid request, category not found, or root never reached",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1221,7 +1298,23 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ApiResponse" "type": "object",
"properties": {
"message": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/B2BTopMenu"
},
"description": "Root menu items with nested children"
},
"count": {
"type": "integer",
"description": "Number of root menu items"
}
}
} }
} }
} }
@@ -1995,46 +2088,6 @@
} }
} }
}, },
"MenuItem": {
"type": "object",
"description": "Menu item structure",
"properties": {
"category_id": {
"type": "integer",
"format": "uint",
"description": "Category ID"
},
"label": {
"type": "string",
"description": "Menu item label"
},
"params": {
"$ref": "#/components/schemas/MenuItemParams"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MenuItem"
},
"description": "Child menu items"
}
}
},
"MenuItemParams": {
"type": "object",
"properties": {
"category_id": {
"type": "integer",
"format": "uint"
},
"link_rewrite": {
"type": "string"
},
"locale": {
"type": "string"
}
}
},
"Route": { "Route": {
"type": "object", "type": "object",
"description": "Application route", "description": "Application route",
@@ -2338,6 +2391,58 @@
"description": "Build date in RFC3339 format" "description": "Build date in RFC3339 format"
} }
} }
},
"CategoryInBreadcrumb": {
"type": "object",
"description": "A single item in a category breadcrumb path",
"properties": {
"category_id": {
"type": "integer",
"format": "uint",
"description": "Category ID"
},
"name": {
"type": "string",
"description": "Category name"
}
}
},
"B2BTopMenu": {
"type": "object",
"description": "Top-level menu item for B2B back-office",
"properties": {
"menu_id": {
"type": "integer",
"description": "Menu item ID"
},
"label": {
"type": "object",
"description": "Menu label as JSON (multilingual, e.g. {\"en\": \"Dashboard\", \"pl\": \"Panel\"})"
},
"parent_id": {
"type": "integer",
"description": "Parent menu ID (null for root items)"
},
"params": {
"type": "object",
"description": "Menu item parameters as JSON"
},
"active": {
"type": "integer",
"description": "Active status (1 = active, 0 = inactive)"
},
"position": {
"type": "integer",
"description": "Sort position"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/B2BTopMenu"
},
"description": "Child menu items"
}
}
} }
}, },
"securitySchemes": { "securitySchemes": {

View File

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

View File

@@ -1,12 +1,16 @@
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"
) )
@@ -60,9 +64,52 @@ func AuthMiddleware() fiber.Handler {
}) })
} }
// Set user in context // Create locale. LangID is overwritten by auth Token
c.Locals(constdata.USER_LOCALES_NAME, user.ToSession()) var userLocale model.UserLocale
c.Locals(constdata.USER_LOCALES_ID, user.ID) userLocale.OriginalUser = user
// 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()
} }
@@ -71,21 +118,14 @@ func AuthMiddleware() fiber.Handler {
// RequireAdmin creates admin-only middleware // RequireAdmin creates admin-only middleware
func RequireAdmin() fiber.Handler { func RequireAdmin() fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
user := c.Locals("user") originalUserRole, ok := localeExtractor.GetOriginalUserRole(c)
if user == nil { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated", "error": "not authenticated",
}) })
} }
userSession, ok := user.(*model.UserSession) if originalUserRole != model.RoleAdmin {
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",
}) })
@@ -95,22 +135,70 @@ func RequireAdmin() fiber.Handler {
} }
} }
// GetUserID extracts user ID from context // Webdav
func GetUserID(c fiber.Ctx) uint { func Webdav() fiber.Handler {
userID, ok := c.Locals("userID").(uint) authService := authService.NewAuthService()
if !ok {
return 0
}
return userID
}
// GetUser extracts user from context return func(c fiber.Ctx) error {
func GetUser(c fiber.Ctx) *model.UserSession { authHeader := c.Get("Authorization")
user, ok := c.Locals("user").(*model.UserSession) if authHeader == "" {
if !ok { c.Set("WWW-Authenticate", `Basic realm="webdav"`)
return nil 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 user
} }
// GetConfig returns the app config // GetConfig returns the app config

View File

@@ -4,7 +4,9 @@ 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"
) )
@@ -22,12 +24,8 @@ 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 {
lang, err := langService.GetLanguageById(langID) c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
if err == nil { return c.Next()
c.Locals("langID", langID)
c.Locals("lang", lang)
return c.Next()
}
} }
} }
} }
@@ -38,12 +36,8 @@ 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 {
lang, err := langService.GetLanguageById(langID) c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
if err == nil { return c.Next()
c.Locals("langID", langID)
c.Locals("lang", lang)
return c.Next()
}
} }
} }
} }
@@ -57,8 +51,7 @@ 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("langID", langID) c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
c.Locals("lang", lang)
return c.Next() return c.Next()
} }
} }
@@ -68,8 +61,7 @@ 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("langID", langID) c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
c.Locals("lang", defaultLang)
} }
return c.Next() return c.Next()
@@ -104,11 +96,9 @@ func parseAcceptLanguage(header string) string {
return strings.ToLower(first) return strings.ToLower(first)
} }
// GetLanguageID extracts language ID from context func returnNewLocale(lang_id uint) *model.UserLocale {
func GetLanguageID(c fiber.Ctx) uint { newLocale := model.UserLocale{}
langID, ok := c.Locals("langID").(uint) newLocale.OriginalUser = &model.Customer{}
if !ok { newLocale.OriginalUser.LangID = lang_id
return 0 return &newLocale
}
return langID
} }

View File

@@ -2,13 +2,17 @@ package public
import ( import (
"log" "log"
"strconv"
"time" "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/delivery/middleware" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"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"
"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/nullable"
"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"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@@ -264,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 {
user := c.Locals("user") userLocale := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if user == nil { if userLocale.OriginalUser == 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": user, "user": *userLocale.OriginalUser,
}) })
} }
@@ -345,9 +349,49 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(response) return c.Status(fiber.StatusCreated).JSON(response)
} }
// Updates JWT Tokens // 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 {
return h.authService.UpdateJWTToken(c) userLocale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok {
return c.Status(fiber.StatusUnauthorized).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
}
// Parse language and country_id from query params
langIDStr := c.Query("lang_id")
if langIDStr != "" {
parsedID, err := strconv.ParseUint(langIDStr, 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
}
userLocale.OriginalUser.LangID = uint(parsedID)
}
countryIDStr := c.Query("country_id")
if countryIDStr != "" {
parsedID, err := strconv.ParseUint(countryIDStr, 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
}
userLocale.OriginalUser.CountryID = uint(parsedID)
}
newAccessToken, err := h.authService.UpdateJWTToken(userLocale.OriginalUser)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
}
// does not reset refresh token
h.setAuthCookies(c, newAccessToken, "")
return c.JSON(response.Make(&fiber.Map{"token": newAccessToken}, 0, i18n.T_(c, response.Message_OK)))
} }
// GoogleLogin redirects the user to Google's OAuth2 consent page // GoogleLogin redirects the user to Google's OAuth2 consent page
@@ -414,12 +458,12 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
// Redirect to the locale-prefixed charts page after successful Google login. // Redirect to the locale-prefixed charts page after successful Google login.
// The user's preferred language is stored in the auth response; fall back to "en". // The user's preferred language is stored in the auth response; fall back to "en".
lang, err := h.authService.GetLangISOCode(response.User.LangID) lang_iso_code, err := h.authService.GetLangISOCode(response.User.LangID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadLangID)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
return c.Redirect().To(h.config.App.BaseURL + "/" + lang) return c.Redirect().To(h.config.App.BaseURL + "/" + lang_iso_code)
} }

View File

@@ -3,6 +3,7 @@ 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"
@@ -30,7 +31,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 := c.Locals("langID").(uint) lang_id, ok := localeExtractor.GetLangID(c)
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,6 +5,7 @@ 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"
@@ -37,7 +38,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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)))
@@ -53,7 +54,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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)))
@@ -78,7 +79,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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)))
@@ -94,7 +95,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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)))
@@ -117,7 +118,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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

@@ -0,0 +1,99 @@
package restricted
import (
"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/listService"
"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/query/query_params"
"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"
)
// ListHandler handles endpoints that list various things (e.g. products or users)
type ListHandler struct {
listService *listService.ListService
config *config.Config
}
// NewListHandler creates a new ListHandler instance
func NewListHandler() *ListHandler {
listService := listService.New()
return &ListHandler{
listService: listService,
config: config.Get(),
}
}
func ListHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewListHandler()
r.Get("/list-products", handler.ListProducts)
r.Get("/list-users", handler.ListUsers)
return r
}
func (h *ListHandler) ListProducts(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
id_lang, ok := localeExtractor.GetLangID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.listService.ListProducts(id_lang, paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(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)))
}
var columnMappingListProducts map[string]string = map[string]string{
"product_id": "ps.id_product",
"name": "pl.name",
"reference": "p.reference",
"category_name": "cl.name",
"category_id": "cp.id_category",
"quantity": "sa.quantity",
}
func (h *ListHandler) ListUsers(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Customer](c, columnMappingListUsers)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
id_lang, ok := localeExtractor.GetLangID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.listService.ListUsers(id_lang, paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(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)))
}
var columnMappingListUsers map[string]string = map[string]string{
"user_id": "users.id",
"email": "users.email",
"first_name": "users.first_name",
"second_name": "users.second_name",
"role": "users.role",
}

View File

@@ -1,67 +0,0 @@
package restricted
import (
"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/listProductsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"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"
)
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
type ListProductsHandler struct {
listProductsService *listProductsService.ListProductsService
config *config.Config
}
// NewListProductsHandler creates a new ListProductsHandler instance
func NewListProductsHandler() *ListProductsHandler {
listProductsService := listProductsService.New()
return &ListProductsHandler{
listProductsService: listProductsService,
config: config.Get(),
}
}
func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewListProductsHandler()
r.Get("/get-listing", handler.GetListing)
return r
}
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingGetListing)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
id_lang, ok := c.Locals("langID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
listing, err := h.listProductsService.GetListing(id_lang, paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
}
var columnMappingGetListing map[string]string = map[string]string{
"product_id": "ps.id_product",
"name": "pl.name",
"reference": "p.reference",
"category_name": "cl.name",
"category_id": "cp.id_category",
"quantity": "sa.quantity",
}

View File

@@ -1,8 +1,11 @@
package restricted package restricted
import ( import (
"strconv"
"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"
@@ -23,29 +26,68 @@ func NewMenuHandler() *MenuHandler {
func MenuHandlerRoutes(r fiber.Router) fiber.Router { func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler() handler := NewMenuHandler()
r.Get("/get-menu", handler.GetMenu) r.Get("/get-category-tree", handler.GetCategoryTree)
r.Get("/get-breadcrumb", handler.GetBreadcrumb)
r.Get("/get-top-menu", handler.GetTopMenu) r.Get("/get-top-menu", handler.GetTopMenu)
return r return r
} }
func (h *MenuHandler) GetMenu(c fiber.Ctx) error { func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint) lang_id, ok := localeExtractor.GetLangID(c)
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)))
} }
menu, err := h.menuService.GetMenu(lang_id)
root_category_id_attribute := c.Query("root_category_id")
root_category_id, err := strconv.Atoi(root_category_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK)))
}
func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
root_category_id_attribute := c.Query("root_category_id")
root_category_id, err := strconv.Atoi(root_category_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
category_id_attribute := c.Query("category_id")
category_id, err := strconv.Atoi(category_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&breadcrumb, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint) lang_id, ok := localeExtractor.GetLangID(c)
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,8 +4,10 @@ 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"
@@ -41,7 +43,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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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)))
@@ -72,12 +74,18 @@ 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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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 {
@@ -109,12 +117,18 @@ 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 := c.Locals("userID").(uint) userID, ok := localeExtractor.GetUserID(c)
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,9 +4,11 @@ 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"
@@ -36,12 +38,18 @@ 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 := c.Locals("langID").(uint) id_lang, ok := localeExtractor.GetLangID(c)
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)
@@ -49,12 +57,11 @@ 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)))
} }
nothing := "" return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
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 := c.Locals("langID").(uint) id_lang, ok := localeExtractor.GetLangID(c)
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)))
@@ -88,7 +95,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 := c.Locals("langID").(uint) id_lang, ok := localeExtractor.GetLangID(c)
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

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

@@ -21,10 +21,12 @@ type SettingsResponse struct {
// AppSettings represents app configuration // AppSettings represents app configuration
type AppSettings struct { type AppSettings struct {
Name string `json:"name"` Name string `json:"name"`
Environment string `json:"environment"` Environment string `json:"environment"`
BaseURL string `json:"base_url"` BaseURL string `json:"base_url"`
PasswordRegex string `json:"password_regex"` PasswordRegex string `json:"password_regex"`
CategoryTreeRootID uint `json:"category_tree_root_id"`
ShopDefaultLanguage uint `json:"shop_default_language"`
// Config config.Config `json:"config"` // Config config.Config `json:"config"`
} }
@@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
settings := SettingsResponse{ settings := SettingsResponse{
App: AppSettings{ App: AppSettings{
Name: cfg.App.Name, Name: cfg.App.Name,
Environment: cfg.App.Environment, Environment: cfg.App.Environment,
BaseURL: cfg.App.BaseURL, BaseURL: cfg.App.BaseURL,
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID,
ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE,
// Config: *config.Get(), // Config: *config.Get(),
}, },
Server: ServerSettings{ Server: ServerSettings{

View File

@@ -0,0 +1,198 @@
package webdav
import (
"bytes"
"io"
"net/http"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type StorageHandler struct {
storageService *storageService.StorageService
config *config.Config
}
func NewStorageHandler() *StorageHandler {
return &StorageHandler{
storageService: storageService.New(),
config: config.Get(),
}
}
func StorageHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewStorageHandler()
// for webdav use only
r.Get("/*", handler.Get)
r.Head("/*", handler.Get)
r.Put("/*", handler.Put)
r.Delete("/*", handler.Delete)
r.Add([]string{"MKCOL"}, "/*", handler.Mkcol)
r.Add([]string{"PROPFIND"}, "/*", handler.Propfind)
r.Add([]string{"PROPPATCH"}, "/*", handler.Proppatch)
r.Add([]string{"MOVE"}, "/*", handler.Move)
r.Add([]string{"COPY"}, "/*", handler.Copy)
return r
}
func (h *StorageHandler) Get(c fiber.Ctx) error {
// fmt.Println("GET")
absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
info, err := h.storageService.EntryInfo(absPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
if info.IsDir() {
xml, err := h.storageService.Propfind(h.config.Storage.RootFolder, absPath, "1")
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
c.Set("Content-Type", `application/xml; charset="utf-8"`)
return c.Status(http.StatusMultiStatus).SendString(xml)
} else {
f, filename, filesize, err := h.storageService.DownloadFilePrep(absPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
c.Attachment(filename)
c.Set("Content-Length", strconv.FormatInt(filesize, 10))
c.Set("Content-Type", "application/octet-stream")
return c.SendStream(f, int(filesize))
}
}
func (h *StorageHandler) Put(c fiber.Ctx) error {
// fmt.Println("PUT")
absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
var src io.Reader
if bodyStream := c.Request().BodyStream(); bodyStream != nil {
defer c.Request().CloseBodyStream()
src = bodyStream
} else {
src = bytes.NewReader(c.Body())
}
err = h.storageService.Put(absPath, src)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
return c.SendStatus(http.StatusCreated)
}
func (h *StorageHandler) Delete(c fiber.Ctx) error {
// fmt.Println("DELETE")
absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
if absPath == h.config.Storage.RootFolder {
return c.SendStatus(responseErrors.GetErrorStatus(responseErrors.ErrAccessDenied))
}
err = h.storageService.Delete(absPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
return c.SendStatus(http.StatusNoContent)
}
func (h *StorageHandler) Mkcol(c fiber.Ctx) error {
// fmt.Println("Mkcol")
absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
err = h.storageService.Mkcol(absPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
return c.SendStatus(http.StatusCreated)
}
func (h *StorageHandler) Propfind(c fiber.Ctx) error {
// fmt.Println("PROPFIND")
absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
xml, err := h.storageService.Propfind(h.config.Storage.RootFolder, absPath, "1")
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
c.Set("Content-Type", `application/xml; charset="utf-8"`)
return c.Status(http.StatusMultiStatus).SendString(xml)
}
func (h *StorageHandler) Proppatch(c fiber.Ctx) error {
return c.SendStatus(http.StatusNotImplemented) // 501
}
func (h *StorageHandler) Move(c fiber.Ctx) error {
// fmt.Println("MOVE")
srcAbsPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
dest := c.Get("Destination")
if dest == "" {
return c.SendStatus(http.StatusBadRequest)
}
destAbsPath, err := h.storageService.ObtainDestPath(h.config.Storage.RootFolder, dest)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
err = h.storageService.Move(srcAbsPath, destAbsPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
return c.SendStatus(http.StatusCreated)
}
func (h *StorageHandler) Copy(c fiber.Ctx) error {
// fmt.Println("COPY")
srcAbsPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
dest := c.Get("Destination")
if dest == "" {
return c.SendStatus(http.StatusBadRequest)
}
destAbsPath, err := h.storageService.ObtainDestPath(h.config.Storage.RootFolder, dest)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
err = h.storageService.Copy(srcAbsPath, destAbsPath)
if err != nil {
return c.SendStatus(responseErrors.GetErrorStatus(err))
}
return c.SendStatus(http.StatusCreated)
}

View File

@@ -14,6 +14,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/public" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/public"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/restricted" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/restricted"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/webdav"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/general" "git.ma-al.com/goc_daniel/b2b/app/delivery/web/general"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@@ -25,6 +26,7 @@ import (
type Server struct { type Server struct {
app *fiber.App app *fiber.App
cfg *config.Config cfg *config.Config
webdav fiber.Router
api fiber.Router api fiber.Router
public fiber.Router public fiber.Router
restricted fiber.Router restricted fiber.Router
@@ -42,12 +44,23 @@ func (s *Server) Cfg() *config.Config {
// New creates a new server instance // New creates a new server instance
func New() *Server { func New() *Server {
return &Server{ var s Server
app: fiber.New(fiber.Config{
ErrorHandler: customErrorHandler, app :=
}), fiber.New(fiber.Config{
cfg: config.Get(), ErrorHandler: customErrorHandler,
} BodyLimit: 50 * 1024 * 1024, // 50 MB
StreamRequestBody: true,
RequestMethods: []string{
fiber.MethodGet, fiber.MethodHead, fiber.MethodPost, fiber.MethodPut,
fiber.MethodDelete, fiber.MethodConnect, fiber.MethodOptions,
fiber.MethodTrace, fiber.MethodPatch, "MKCOL", "PROPFIND", "PROPPATCH", "MOVE", "COPY",
},
})
s.app = app
s.cfg = config.Get()
return &s
} }
// Setup configures the server with routes and middleware // Setup configures the server with routes and middleware
@@ -76,6 +89,8 @@ func (s *Server) Setup() error {
s.public = s.api.Group("/public") s.public = s.api.Group("/public")
s.restricted = s.api.Group("/restricted") s.restricted = s.api.Group("/restricted")
s.restricted.Use(middleware.AuthMiddleware()) s.restricted.Use(middleware.AuthMiddleware())
s.webdav = s.api.Group("/webdav")
s.webdav.Use(middleware.Webdav())
// initialize language endpoints (general) // initialize language endpoints (general)
api.NewLangHandler().InitLanguage(s.api, s.cfg) api.NewLangHandler().InitLanguage(s.api, s.cfg)
@@ -94,9 +109,9 @@ func (s *Server) Setup() error {
productTranslation := s.restricted.Group("/product-translation") productTranslation := s.restricted.Group("/product-translation")
restricted.ProductTranslationHandlerRoutes(productTranslation) restricted.ProductTranslationHandlerRoutes(productTranslation)
// listing products routes (restricted) // lists of things routes (restricted)
listProducts := s.restricted.Group("/list-products") list := s.restricted.Group("/list")
restricted.ListProductsHandlerRoutes(listProducts) restricted.ListHandlerRoutes(list)
// locale selector (restricted) // locale selector (restricted)
// this is basically for changing user's selected language and country // this is basically for changing user's selected language and country
@@ -115,6 +130,12 @@ 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)
}) })

33
app/model/category.go Normal file
View File

@@ -0,0 +1,33 @@
package model
type ScannedCategory struct {
CategoryID uint `gorm:"column:category_id;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
Visited bool //this is for internal backend use only
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategoryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategoryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
}
type CategoryInBreadcrumb struct {
CategoryID uint `json:"category_id" form:"category_id"`
Name string `json:"name" form:"name"`
}

View File

@@ -1,31 +1,17 @@
package model package model
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
// Represents a country together with its associated currency // Represents a country together with its associated currency
type Country struct { type Country struct {
ID uint `gorm:"primaryKey;column:id" json:"id"` ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"` Name string `gorm:"column:name" json:"name"`
Flag string `gorm:"size:16;not null;column:flag" json:"flag"` Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"` PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"`
CurrencyName string `gorm:"column:name" json:"currency_name"` PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"`
// PSCountryID int `gorm:"column:id_country" json:"ps_country_id"`
// PSCountry *PSCountry `gorm:"foreignKey:PSCountryID;references:ID" json:"ps_country"`
PSCurrencyID uint `gorm:"column:currency" json:"currency"`
PSCurrency *PSCurrency `gorm:"foreignKey:PSCurrencyID;references:currency_id" json:"ps_currency"`
} }
func (Country) TableName() string { func (Country) TableName() string {
return "b2b_countries" return "b2b_countries"
} }
type PSCountry struct {
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
}
func (PSCountry) TableName() string {
return "ps_country"
}
type PSCurrency struct {
Currency int `gorm:"column:currency" json:"currency"`
}

View File

@@ -23,6 +23,8 @@ type Customer struct {
EmailVerificationExpires *time.Time `json:"-"` EmailVerificationExpires *time.Time `json:"-"`
PasswordResetToken string `gorm:"size:255" json:"-"` PasswordResetToken string `gorm:"size:255" json:"-"`
PasswordResetExpires *time.Time `json:"-"` PasswordResetExpires *time.Time `json:"-"`
WebdavToken string `gorm:"size:255" json:"-"`
WebdavExpires *time.Time `json:"-"`
LastPasswordResetRequest *time.Time `json:"-"` LastPasswordResetRequest *time.Time `json:"-"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"` LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
@@ -82,6 +84,15 @@ 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{
@@ -98,6 +109,7 @@ 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
@@ -144,3 +156,11 @@ type RefreshToken struct {
func (RefreshToken) TableName() string { func (RefreshToken) TableName() string {
return "b2b_refresh_tokens" return "b2b_refresh_tokens"
} }
type UserInList struct {
UserID uint `gorm:"primaryKey;column:id" json:"user_id"`
Email string `gorm:"column:email" json:"email"`
FirstName string `gorm:"column:first_name" json:"first_name"`
LastName string `gorm:"column:last_name" json:"last_name"`
Role string `gorm:"column:role" json:"role"`
}

View File

@@ -21,12 +21,12 @@ func (*PsGroupReduction) TableName() string {
var PsGroupReductionCols = struct { var PsGroupReductionCols = struct {
IDGroupReduction gormcol.Field IDGroupReduction gormcol.Field
IDGroup gormcol.Field IDGroup gormcol.Field
IDCategory gormcol.Field IDCategory gormcol.Field
Reduction gormcol.Field Reduction gormcol.Field
}{ }{
IDGroupReduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group_reduction"), IDGroupReduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group_reduction"),
IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"), IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"),
IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"), IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"),
Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"), Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"),
} }

View File

@@ -18,9 +18,9 @@ func (*PsProductAttributeCombination) TableName() string {
} }
var PsProductAttributeCombinationCols = struct { var PsProductAttributeCombinationCols = struct {
IDAttribute gormcol.Field IDAttribute gormcol.Field
IDProductAttribute gormcol.Field IDProductAttribute gormcol.Field
}{ }{
IDAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_attribute"), IDAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_attribute"),
IDProductAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_product_attribute"), IDProductAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_product_attribute"),
} }

View File

@@ -20,10 +20,10 @@ func (*PsProductGroupReductionCache) TableName() string {
var PsProductGroupReductionCacheCols = struct { var PsProductGroupReductionCacheCols = struct {
IDProduct gormcol.Field IDProduct gormcol.Field
IDGroup gormcol.Field IDGroup gormcol.Field
Reduction gormcol.Field Reduction gormcol.Field
}{ }{
IDProduct: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_product"), IDProduct: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_product"),
IDGroup: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_group"), IDGroup: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_group"),
Reduction: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "reduction"), Reduction: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "reduction"),
} }

View File

@@ -19,11 +19,11 @@ func (*PsPshowPshowproducttabsHook) TableName() string {
} }
var PsPshowPshowproducttabsHookCols = struct { var PsPshowPshowproducttabsHookCols = struct {
IDHook gormcol.Field IDHook gormcol.Field
HookName gormcol.Field HookName gormcol.Field
PrestaIDHook gormcol.Field PrestaIDHook gormcol.Field
}{ }{
IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"), IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"),
HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"), HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"),
PrestaIDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "presta_id_hook"), PrestaIDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "presta_id_hook"),
} }

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

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

View File

@@ -84,28 +84,4 @@ type ProductFilters struct {
InStock uint `query:"stock,omitempty"` InStock uint `query:"stock,omitempty"`
} }
type ScannedCategory struct {
CategoryID uint `gorm:"column:category_id;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategpryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategpryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
}
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint

View File

@@ -18,7 +18,10 @@ 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"`
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
} }
type ProductRow struct { type ProductRow struct {

View File

@@ -37,12 +37,11 @@ func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory,
ps_category_lang.link_rewrite AS link_rewrite, ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code ps_lang.iso_code AS iso_code
`). `).
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ? AND ??.id_lang = ?`, Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
categoryLangTbl, categoryLangTbl, categoryTbl, categoryLangTbl, constdata.SHOP_ID, categoryLangTbl, idLang). constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ?`, Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
categoryShopTbl, categoryShopTbl, categoryTbl, categoryShopTbl, constdata.SHOP_ID). constdata.SHOP_ID).
Joins(`JOIN ? ON ??.id_lang = ??.id_lang`, Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
langTbl, langTbl, categoryLangTbl).
Scan(&allCategories).Error Scan(&allCategories).Error
return allCategories, err return allCategories, err

View File

@@ -1,4 +1,4 @@
package listProductsRepo package listRepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
@@ -7,30 +7,32 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_marek/gormcol"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
) )
type UIListProductsRepo interface { type UIListRepo interface {
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error)
} }
type ListProductsRepo struct{} type ListRepo struct{}
func New() UIListProductsRepo { func New() UIListRepo {
return &ListProductsRepo{} return &ListRepo{}
} }
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { func (repo *ListRepo) ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
var listing []model.ProductInList var list []model.ProductInList
var total int64 var total int64
query := db.Get(). query := db.Get().
Table("ps_product_shop ps"). Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(` Select(`
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,
@@ -67,13 +69,53 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
Order("ps.id_product DESC"). Order("ps.id_product DESC").
Limit(p.Limit()). Limit(p.Limit()).
Offset(p.Offset()). Offset(p.Offset()).
Find(&listing).Error Find(&list).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return find.Found[model.ProductInList]{}, err
} }
return find.Found[model.ProductInList]{ return find.Found[model.ProductInList]{
Items: listing, Items: list,
Count: uint(total),
}, nil
}
func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) {
var list []model.UserInList
var total int64
query := db.Get().
Table("b2b_customers AS users").
Select(`
users.id AS id,
users.email AS email,
users.first_name AS first_name,
users.last_name AS last_name,
users.role AS role
`)
// Apply all filters
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset
err := query.Count(&total).Error
if err != nil {
return find.Found[model.UserInList]{}, err
}
err = query.
Order("users.id DESC").
Limit(p.Limit()).
Offset(p.Offset()).
Find(&list).Error
if err != nil {
return find.Found[model.UserInList]{}, err
}
return find.Found[model.UserInList]{
Items: list,
Count: uint(total), Count: uint(total),
}, nil }, nil
} }

View File

@@ -1,13 +1,16 @@
package productDescriptionRepo package productDescriptionRepo
import ( import (
"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"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
"gorm.io/gorm"
) )
type UIProductDescriptionRepo interface { type UIProductDescriptionRepo interface {
@@ -28,14 +31,42 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
var ProductDescription model.ProductDescription var ProductDescription model.ProductDescription
err := db.Get(). err := db.Get().
Model(dbmodel.PsProductLang{}).
Where(&dbmodel.PsProductLang{ Where(&dbmodel.PsProductLang{
IDProduct: int32(productID), IDProduct: int32(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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// handle "not found" case only
ProductDescription.ExistsInDatabase = false
} else if err != nil {
return nil, fmt.Errorf("database error: %w", err) return nil, fmt.Errorf("database error: %w", err)
} else {
ProductDescription.ExistsInDatabase = true
} }
return &ProductDescription, nil return &ProductDescription, nil
@@ -50,6 +81,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_
} }
err := db.Get(). err := db.Get().
Model(dbmodel.PsProductLang{}).
Where(&dbmodel.PsProductLang{ Where(&dbmodel.PsProductLang{
IDProduct: int32(productID), IDProduct: int32(productID),
IDShop: int32(constdata.SHOP_ID), IDShop: int32(constdata.SHOP_ID),

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strconv"
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
@@ -14,13 +13,9 @@ 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/emailService" "git.ma-al.com/goc_daniel/b2b/app/service/emailService"
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/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"github.com/gofiber/fiber/v3"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -88,6 +83,15 @@ 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)
@@ -167,7 +171,7 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
baseURL := config.Get().App.BaseURL baseURL := config.Get().App.BaseURL
lang, err := s.GetLangISOCode(req.LangID) lang, err := s.GetLangISOCode(req.LangID)
if err != nil { if err != nil {
return responseErrors.ErrBadLangID return err
} }
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil { if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
@@ -276,7 +280,7 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
baseURL := config.Get().App.BaseURL baseURL := config.Get().App.BaseURL
lang, err := s.GetLangISOCode(user.LangID) lang, err := s.GetLangISOCode(user.LangID)
if err != nil { if err != nil {
return responseErrors.ErrBadLangID return err
} }
if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil { if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
@@ -448,6 +452,19 @@ func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) {
return &user, nil return &user, nil
} }
func (s *AuthService) GetUserByWebdavToken(rawToken string) (*model.Customer, error) {
tokenHash := hashToken(rawToken)
var user model.Customer
if err := s.db.Where("webdav_token = ?", tokenHash).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, responseErrors.ErrUserNotFound
}
return nil, fmt.Errorf("database error: %w", err)
}
return &user, nil
}
// createRefreshToken generates a random opaque token, stores its hash in the DB, and returns the raw token. // createRefreshToken generates a random opaque token, stores its hash in the DB, and returns the raw token.
func (s *AuthService) createRefreshToken(userID uint) (string, error) { func (s *AuthService) createRefreshToken(userID uint) (string, error) {
// Generate 32 random bytes → 64-char hex string // Generate 32 random bytes → 64-char hex string
@@ -482,7 +499,7 @@ func hashToken(raw string) string {
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) { func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
_, err := s.GetLangISOCode(user.LangID) _, err := s.GetLangISOCode(user.LangID)
if err != nil { if err != nil {
return "", responseErrors.ErrBadLangID return "", err
} }
err = s.CheckIfCountryExists(user.CountryID) err = s.CheckIfCountryExists(user.CountryID)
@@ -508,97 +525,19 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
return token.SignedString([]byte(s.config.JWTSecret)) return token.SignedString([]byte(s.config.JWTSecret))
} }
func (s *AuthService) UpdateJWTToken(c fiber.Ctx) error { func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
// Get user ID from JWT claims in context (set by auth middleware) // Update choice and get new access token using AuthService
// claims, ok := c.Locals("jwt_claims").(*JWTClaims) new_access_token, err := s.generateAccessToken(user)
// if !ok || claims == nil {
// return c.Status(fiber.StatusUnauthorized).
// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
// }
// fmt.Printf("claims: %v\n", claims)
// var user model.Customer
// // Find user by ID
// if err := s.db.First(&user, claims.UserID).Error; err != nil {
// return err
// }
userLocals, ok := c.Locals(constdata.USER_LOCALES_NAME).(*model.UserSession)
if !ok {
return c.Status(fiber.StatusUnauthorized).
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
langIDStr := c.Query("lang_id")
var langID uint
if langIDStr != "" {
parsedID, err := strconv.ParseUint(langIDStr, 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
}
langID = uint(parsedID)
_, err = s.GetLangISOCode(langID)
if err != nil {
return responseErrors.ErrBadLangID
} else {
user.LangID = langID
}
}
countryIDStr := c.Query("country_id")
var countryID uint
if countryIDStr != "" {
parsedID, err := strconv.ParseUint(countryIDStr, 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
}
countryID = uint(parsedID)
err = s.CheckIfCountryExists(countryID)
if err != nil {
return responseErrors.ErrBadCountryID
} else {
user.CountryID = countryID
}
}
// Update choice and get new token using AuthService
newToken, err := s.generateAccessToken(&user)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return "", err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
// Save the updated user // Save the updated user
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(user).Error; err != nil {
return fmt.Errorf("database error: %w", err) return "", fmt.Errorf("database error: %w", err)
} }
// Set the new JWT cookie return new_access_token, nil
cookie := new(fiber.Cookie)
cookie.Name = "jwt_token"
cookie.Value = newToken
cookie.HTTPOnly = true
cookie.Secure = true
cookie.SameSite = fiber.CookieSameSiteLaxMode
c.Cookie(cookie)
return c.JSON(response.Make(&fiber.Map{"token": newToken}, 0, i18n.T_(c, response.Message_OK)))
} }
// generateVerificationToken generates a random verification token // generateVerificationToken generates a random verification token
@@ -623,14 +562,20 @@ func validatePassword(password string) error {
func (s *AuthService) GetLangISOCode(langID uint) (string, error) { func (s *AuthService) GetLangISOCode(langID uint) (string, error) {
var lang string var lang string
var err error
if langID == 0 { // retrieve the default lang if langID == 0 { // retrieve the default lang
err := db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error err = db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
return lang, err
} else { } else {
err := db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error err = db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
return lang, err
} }
if err != nil {
return lang, err
} else if lang == "" {
return lang, responseErrors.ErrBadLangID
}
return lang, nil
} }
func (s *AuthService) CheckIfCountryExists(countryID uint) error { func (s *AuthService) CheckIfCountryExists(countryID uint) error {

View File

@@ -153,7 +153,8 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
Role: model.RoleUser, Role: model.RoleUser,
IsActive: true, IsActive: true,
EmailVerified: true, EmailVerified: true,
LangID: 2, LangID: 2, // default is english
CountryID: 2, // default is England
} }
if err := s.db.Create(&newUser).Error; err != nil { if err := s.db.Create(&newUser).Error; err != nil {

View File

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

View File

@@ -1,29 +0,0 @@
package listProductsService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/listProductsRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type ListProductsService struct {
listProductsRepo listProductsRepo.UIListProductsRepo
}
func New() *ListProductsService {
return &ListProductsService{
listProductsRepo: listProductsRepo.New(),
}
}
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
var products find.Found[model.ProductInList]
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
if err != nil {
return products, err
}
return products, nil
}

View File

@@ -0,0 +1,26 @@
package listService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/listRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type ListService struct {
listRepo listRepo.UIListRepo
}
func New() *ListService {
return &ListService{
listRepo: listRepo.New(),
}
}
func (s *ListService) ListProducts(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
return s.listRepo.ListProducts(id_lang, p, filters)
}
func (s *ListService) ListUsers(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.UserInList], error) {
return s.listRepo.ListUsers(id_lang, p, filters)
}

View File

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

View File

@@ -1,6 +1,7 @@
package menuService package menuService
import ( import (
"slices"
"sort" "sort"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
@@ -21,7 +22,7 @@ func New() *MenuService {
} }
} }
func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
if err != nil { if err != nil {
return &model.Category{}, err return &model.Category{}, err
@@ -31,7 +32,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
root_index := 0 root_index := 0
root_found := false root_found := false
for i := 0; i < len(all_categories); i++ { for i := 0; i < len(all_categories); i++ {
if all_categories[i].IsRoot == 1 { if all_categories[i].CategoryID == root_category_id {
root_index = i root_index = i
root_found = true root_found = true
break break
@@ -44,6 +45,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
// now create the children and reorder them according to position // now create the children and reorder them according to position
id_to_index := make(map[uint]int) id_to_index := make(map[uint]int)
for i := 0; i < len(all_categories); i++ { for i := 0; i < len(all_categories); i++ {
all_categories[i].Visited = false
id_to_index[all_categories[i].CategoryID] = i id_to_index[all_categories[i].CategoryID] = i
} }
@@ -58,19 +60,32 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
} }
// finally, create the tree // finally, create the tree
tree := s.createTree(root_index, &all_categories, &children_indices) tree, success := s.createTree(root_index, &all_categories, &children_indices)
if !success {
return &tree, responseErrors.ErrCircularDependency
}
return &tree, nil return &tree, nil
} }
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category { func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) (model.Category, bool) {
node := s.scannedToNormalCategory((*all_categories)[index]) node := s.scannedToNormalCategory((*all_categories)[index])
if (*all_categories)[index].Visited {
return node, false
}
(*all_categories)[index].Visited = true
for i := 0; i < len((*children_indices)[index]); i++ { for i := 0; i < len((*children_indices)[index]); i++ {
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)) next_child, success := s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)
if !success {
return node, false
}
node.Children = append(node.Children, next_child)
} }
return node (*all_categories)[index].Visited = false // just in case we have a "diamond" diagram
return node, true
} }
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
@@ -83,7 +98,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
normal.CategoryID = scanned.CategoryID normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name normal.Label = scanned.Name
// normal.Active = scanned.Active == 1 // normal.Active = scanned.Active == 1
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
normal.Children = []model.Category{} normal.Children = []model.Category{}
return normal return normal
} }
@@ -98,6 +113,69 @@ func (a ByPosition) Len() int { return len(a) }
func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position } func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
if err != nil {
return []model.CategoryInBreadcrumb{}, err
}
breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0
start_found := false
for i := 0; i < len(all_categories); i++ {
if all_categories[i].CategoryID == start_category_id {
start_index = i
start_found = true
break
}
}
if !start_found {
return []model.CategoryInBreadcrumb{}, responseErrors.ErrStartCategoryNotFound
}
// map category ids to indices
id_to_index := make(map[uint]int)
for i := 0; i < len(all_categories); i++ {
all_categories[i].Visited = false
id_to_index[all_categories[i].CategoryID] = i
}
// do a simple graph traversal, always jumping from node to its parent
index := start_index
success := true
for {
if all_categories[index].Visited {
success = false
break
}
all_categories[index].Visited = true
var next_category model.CategoryInBreadcrumb
next_category.CategoryID = all_categories[index].CategoryID
next_category.Name = all_categories[index].Name
breadcrumb = append(breadcrumb, next_category)
if all_categories[index].CategoryID == root_category_id {
break
}
next_index, ok := id_to_index[all_categories[index].ParentID]
if !ok {
success = false
break
}
index = next_index
}
slices.Reverse(breadcrumb)
if !success {
return breadcrumb, responseErrors.ErrRootNeverReached
}
return breadcrumb, nil
}
func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) { func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) {
items, err := s.routesRepo.GetTopMenu(id) items, err := s.routesRepo.GetTopMenu(id)
if err != nil { if err != nil {

View File

@@ -0,0 +1,283 @@
package storageService
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type StorageService struct {
storageRepo storageRepo.UIStorageRepo
}
func New() *StorageService {
return &StorageService{
storageRepo: storageRepo.New(),
}
}
func (s *StorageService) EntryInfo(abs_path string) (os.FileInfo, error) {
return s.storageRepo.EntryInfo(abs_path)
}
func (s *StorageService) NewWebdavToken(user_id uint) (string, error) {
b := make([]byte, constdata.NBYTES_IN_WEBDAV_TOKEN)
_, err := rand.Read(b)
if err != nil {
return "", err
}
raw_token := hex.EncodeToString(b)
hash_token_bytes := sha256.Sum256([]byte(raw_token))
hash_token := hex.EncodeToString(hash_token_bytes[:])
expires_at := time.Now().Add(24 * time.Hour)
return raw_token, s.storageRepo.SaveWebdavToken(user_id, hash_token, &expires_at)
}
func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) {
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil || info.IsDir() {
return nil, "", 0, responseErrors.ErrFileDoesNotExist
}
f, err := s.storageRepo.OpenFile(abs_path)
if err != nil {
return nil, "", 0, err
}
return f, filepath.Base(abs_path), info.Size(), nil
}
func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) {
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil || !info.IsDir() {
return nil, responseErrors.ErrFolderDoesNotExist
}
entries_in_list, err := s.storageRepo.ListContent(abs_path)
return entries_in_list, err
}
func (s *StorageService) Propfind(root string, abs_path string, depth string) (string, error) {
href := href(root, abs_path)
max_depth := 0
switch depth {
case "0":
max_depth = 0
case "1":
max_depth = 1
case "infinity":
max_depth = 32
default:
max_depth = 0
}
info, err := s.storageRepo.EntryInfo(abs_path)
if err != nil {
return "", err
}
xml := `<?xml version="1.0" encoding="utf-8"?>` +
`<D:multistatus xmlns:D="DAV:">`
if info.IsDir() {
href = ensureTrailingSlash(href)
next_xml, err := buildDirPropResponse(abs_path, href, info, max_depth)
if err != nil {
return "", err
}
xml += next_xml
} else {
xml += buildFilePropResponse(href, info)
}
xml += `</D:multistatus>`
return xml, nil
}
func (s *StorageService) Put(abs_path string, src io.Reader) error {
return s.storageRepo.Put(abs_path, src)
}
func (s *StorageService) Delete(abs_path string) error {
return s.storageRepo.Delete(abs_path)
}
func (s *StorageService) Mkcol(abs_path string) error {
_, err := s.storageRepo.EntryInfo(abs_path)
if err == nil {
return responseErrors.ErrNameTaken
} else if os.IsNotExist(err) {
return s.storageRepo.Mkcol(abs_path)
} else {
return err
}
}
func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error {
return s.storageRepo.Move(src_abs_path, dest_abs_path)
}
func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error {
return s.storageRepo.Copy(src_abs_path, dest_abs_path)
}
func buildFilePropResponse(href string, info os.FileInfo) string {
name := info.Name()
return "" +
"<D:response>" +
"<D:href>" + xmlEscape(href) + "</D:href>" +
"<D:propstat>" +
"<D:prop>" +
"<D:displayname>" + xmlEscape(name) + "</D:displayname>" +
"<D:getcontentlength>" + strconv.FormatInt(info.Size(), 10) + "</D:getcontentlength>" +
"<D:getlastmodified>" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "</D:getlastmodified>" +
"<D:resourcetype/>" +
"</D:prop>" +
"<D:status>HTTP/1.1 200 OK</D:status>" +
"</D:propstat>" +
"</D:response>"
}
func buildDirPropResponse(abs_path string, href string, info os.FileInfo, max_depth int) (string, error) {
name := info.Name()
xml := "" +
"<D:response>" +
"<D:href>" + xmlEscape(ensureTrailingSlash(href)) + "</D:href>" +
"<D:propstat>" +
"<D:prop>" +
"<D:displayname>" + xmlEscape(name) + "</D:displayname>" +
"<D:resourcetype><D:collection/></D:resourcetype>" +
"<D:getlastmodified>" + xmlEscape(info.ModTime().UTC().Format(http.TimeFormat)) + "</D:getlastmodified>" +
"</D:prop>" +
"<D:status>HTTP/1.1 200 OK</D:status>" +
"</D:propstat>" +
"</D:response>"
if max_depth <= 0 {
return xml, nil
}
entries, err := os.ReadDir(abs_path)
if err != nil {
return "", err
}
for _, entry := range entries {
child_abs_path := filepath.Join(abs_path, entry.Name())
child_href := path.Join(href, entry.Name())
child_info, err := entry.Info()
if err != nil {
return "", err
}
var xml_next string
if entry.IsDir() {
xml_next, err = buildDirPropResponse(child_abs_path, ensureTrailingSlash(child_href), child_info, max_depth-1)
} else {
xml_next = buildFilePropResponse(child_href, child_info)
}
if err != nil {
return "", err
}
xml += xml_next
}
return xml, nil
}
func ensureTrailingSlash(s string) string {
if s == "/" {
return s
}
if !strings.HasSuffix(s, "/") {
return s + "/"
}
return s
}
func xmlEscape(s string) string {
var b strings.Builder
xml.EscapeText(&b, []byte(s))
return b.String()
}
// Returns href based on file's absolute path. Doesn't validate abs_path
func href(root string, abs_path string) string {
rel, _ := filepath.Rel(root, abs_path)
if rel == "." {
return constdata.WEBDAV_HREF_ROOT + "/"
}
rel = filepath.ToSlash(rel)
parts := strings.Split(rel, "/")
for i, p := range parts {
parts[i] = url.PathEscape(p)
}
return strings.TrimRight(constdata.WEBDAV_HREF_ROOT, "/") + "/" + strings.Join(parts, "/")
}
// AbsPath extracts an absolute path and validates it
func (s *StorageService) AbsPath(root string, relative_path string) (string, error) {
decoded, err := url.PathUnescape(relative_path)
if err != nil {
return "", err
}
clean_name := filepath.Clean(decoded)
full_path := filepath.Join(root, clean_name)
if full_path != root && !strings.HasPrefix(full_path, root+"/") {
return "", responseErrors.ErrAccessDenied
}
return full_path, nil
}
// ObtainDestPath extracts the absolute path based on URL absolute path
func (s *StorageService) ObtainDestPath(root string, dest_path string) (string, error) {
idx := strings.Index(dest_path, constdata.WEBDAV_TRIMMED_ROOT)
if idx == -1 {
return "", responseErrors.ErrAccessDenied
}
prefix_removed := dest_path[idx+len(constdata.WEBDAV_TRIMMED_ROOT):]
decoded, err := url.PathUnescape(prefix_removed)
if err != nil {
return "", err
}
clean_dest_path := filepath.Clean(decoded)
if clean_dest_path == "" {
return root, nil
} else if strings.HasPrefix(clean_dest_path, "/") {
return root + "/" + strings.TrimPrefix(clean_dest_path, "/"), nil
} else {
return "", responseErrors.ErrAccessDenied
}
}

View File

@@ -3,8 +3,18 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
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 ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
const CATEGORY_TREE_ROOT_ID = 2
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_LOCALES_NAME = "user" const USER_LOCALE = "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,6 +8,7 @@ import (
"sync" "sync"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -177,7 +178,7 @@ func (s *TranslationsStore) ReloadTranslations(translations []model.Translation)
// T_ is meant to be used to translate error messages and other system communicates. // T_ is meant to be used to translate error messages and other system communicates.
func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string { func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string {
if langID, ok := c.Locals("langID").(uint); ok { if langID, ok := localeExtractor.GetLangID(c); ok {
parts := strings.Split(string(key), ".") parts := strings.Split(string(key), ".")
if len(parts) >= 2 { if len(parts) >= 2 {

View File

@@ -0,0 +1,31 @@
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,13 +9,14 @@ 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")
@@ -50,12 +51,22 @@ var (
ErrBadPaging = errors.New("bad or missing paging attribute value in header") ErrBadPaging = errors.New("bad or missing paging attribute value in header")
// Typed errors for menu handler // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)")
ErrStartCategoryNotFound = errors.New("the start category has not been found")
ErrRootNeverReached = errors.New("the root category is not an ancestor of start category")
// Typed errors for carts handler // Typed errors for carts handler
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
@@ -108,6 +119,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
return i18n.T_(c, "error.err_token_required") return i18n.T_(c, "error.err_token_required")
case errors.Is(err, ErrRefreshTokenRequired): case errors.Is(err, ErrRefreshTokenRequired):
return i18n.T_(c, "error.err_refresh_token_required") return i18n.T_(c, "error.err_refresh_token_required")
case errors.Is(err, ErrAdminAccessRequired):
return i18n.T_(c, "error.err_admin_access_required")
case errors.Is(err, ErrBadLangID): case errors.Is(err, ErrBadLangID):
return i18n.T_(c, "error.err_bad_lang_id") return i18n.T_(c, "error.err_bad_lang_id")
case errors.Is(err, ErrBadCountryID): case errors.Is(err, ErrBadCountryID):
@@ -145,6 +158,12 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrNoRootFound): case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.no_root_found") return i18n.T_(c, "error.no_root_found")
case errors.Is(err, ErrCircularDependency):
return i18n.T_(c, "error.circular_dependency")
case errors.Is(err, ErrStartCategoryNotFound):
return i18n.T_(c, "error.start_category_not_found")
case errors.Is(err, ErrRootNeverReached):
return i18n.T_(c, "error.root_never_reached")
case errors.Is(err, ErrMaxAmtOfCartsReached): case errors.Is(err, ErrMaxAmtOfCartsReached):
return i18n.T_(c, "error.max_amt_of_carts_reached") return i18n.T_(c, "error.max_amt_of_carts_reached")
@@ -153,6 +172,17 @@ 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")
} }
@@ -175,6 +205,7 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrEmailPasswordRequired), errors.Is(err, ErrEmailPasswordRequired),
errors.Is(err, ErrTokenRequired), errors.Is(err, ErrTokenRequired),
errors.Is(err, ErrRefreshTokenRequired), errors.Is(err, ErrRefreshTokenRequired),
errors.Is(err, ErrAdminAccessRequired),
errors.Is(err, ErrBadLangID), errors.Is(err, ErrBadLangID),
errors.Is(err, ErrBadCountryID), errors.Is(err, ErrBadCountryID),
errors.Is(err, ErrPasswordsDoNotMatch), errors.Is(err, ErrPasswordsDoNotMatch),
@@ -189,9 +220,17 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrNoRootFound), errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrCircularDependency),
errors.Is(err, ErrStartCategoryNotFound),
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

1
bo/.gitignore vendored
View File

@@ -13,6 +13,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
/bo/components.d.ts
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

13
bo/components.d.ts vendored
View File

@@ -11,33 +11,26 @@ 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']
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']
Page: typeof import('./src/components/customer/Page.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']
PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default'] PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default']
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default'] PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default'] PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
PageProductsList: typeof import('./src/components/admin/PageProductsList.vue')['default']
PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default'] PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default']
PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.vue')['default'] PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.vue')['default']
PageStatistic: typeof import('./src/components/customer/PageStatistic.vue')['default']
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default'] Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
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']
@@ -49,13 +42,11 @@ declare module 'vue' {
UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default'] UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default'] UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default'] UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default']
UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default'] UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default'] UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default'] UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
ULink: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/overrides/vue-router/Link.vue')['default']
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default'] UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']

View File

@@ -3,10 +3,11 @@ import { useFetchJson } from '@/composable/useFetchJson'
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/langSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/themeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { computed, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { currentLang } from '@/router/langs' import { currentLang } from '@/router/langs'
import type { LabelTrans, TopMenuItem } from '@/types' import type { LabelTrans, TopMenuItem } from '@/types'
import type { NavigationMenuItem } from '@nuxt/ui' import type { NavigationMenuItem } from '@nuxt/ui'
import { useRoute, useRouter } from 'vue-router'
const authStore = useAuthStore() const authStore = useAuthStore()
let menu = ref() let menu = ref()
@@ -19,30 +20,43 @@ async function getTopMenu() {
} }
} }
const router = useRouter()
const route = useRoute()
const menuItems = computed(() => transformMenu(menu.value[0].children, currentLang.value?.iso_code)) const menuItems = computed(() => transformMenu(menu.value[0].children, currentLang.value?.iso_code))
function transformMenu(items: TopMenuItem[], locale: string | undefined): NavigationMenuItem[] { function transformMenu(items: TopMenuItem[], locale: string | undefined): NavigationMenuItem[] {
return items.map((item) => { return items.map((item) => {
let route = { const route: NavigationMenuItem = {
icon: 'i-lucide-house', icon: item.label.icon ? item.label.icon : 'i-lucide-house',
label: item.label.trans[locale as keyof LabelTrans].label, label: item.label.trans[locale as keyof LabelTrans].label,
children: item.children ? transformMenu(item.children, locale) : undefined, children: item.children
? transformMenu(item.children, locale)
: undefined,
}
route.onSelect = () => {
const query = {
name: item.params.route.name,
params: {
...(item.params.route.params || {}),
locale: currentLang.value?.iso_code
}
}
router.push(query)
} }
if (item.params?.route) {
route = { ...route, ...{ to: { name: item.params.route.name, params: { locale: locale } } } }
}
return route return route
}) })
} }
await getTopMenu() await getTopMenu()
</script> </script>
<template> <template>
{{ menuItems }} <header
<!-- fixed top-0 left-0 right-0 z-50 --> class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<header class="bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)"> <!-- px-4 sm:px-6 lg:px-8 -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <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 --> <!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2"> <RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
@@ -52,49 +66,18 @@ await getTopMenu()
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink> </RouterLink>
<UNavigationMenu :items="menuItems" class="w-full" /> <UNavigationMenu :type="'trigger'" :ui="{
root: 'justify-center',
<!-- {{ router }} --> list: 'gap-4'
<!-- <RouterLink :to="{ name: 'admin-products' }"> }" :items="menuItems" class="w-full"></UNavigationMenu>
products list
</RouterLink>
<RouterLink :to="{ name: 'admin-product-detail' }">
product detail
</RouterLink>
<RouterLink :to="{
name: 'customer-product', params: {
product_id: '51'
}
}">
Product (51)
</RouterLink>
<RouterLink :to="{ name: 'addresses' }">
Addresses
</RouterLink>
<RouterLink :to="{ name: 'profile-details' }">
Customer Data
</RouterLink>
<RouterLink :to="{ name: 'cart' }">
Cart
</RouterLink>
<RouterLink :to="{
name: 'cart-details', params: {
cart_id: '1'
}
}">
Cart details (1)
</RouterLink> -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher --> <!-- Language Switcher -->
<LangSwitch /> <LangSwitch />
<!-- Theme Switcher --> <!-- Theme Switcher -->
<ThemeSwitch /> <ThemeSwitch />
<!-- Logout Button (only when authenticated) --> <!-- Logout Button (only when authenticated) -->
<button <button v-if="authStore.isAuthenticated" @click="authStore.logout()"
v-if="authStore.isAuthenticated" class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)">
@click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)"
>
{{ $t('general.logout') }} {{ $t('general.logout') }}
</button> </button>
</div> </div>

View File

@@ -1,39 +1,11 @@
<template> <template>
<suspense> <suspense>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20"> <div class="flex gap-10">
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48"> <CategoryMenu />
<template #item="{ item, active }"> <div class="w-full flex flex-col items-center gap-4">
<div class="flex items-center gap-2 px-3 py-2"> <UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
<UIcon name="i-heroicons-book-open" /> <UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
<span>{{ item.name }}</span>
</div>
</template>
</UNavigationMenu> -->
<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">
<div class="flex gap-2">
<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>
<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>
@@ -46,26 +18,20 @@ 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 CategoryMenuListing from '../inner/categoryMenuListing.vue' import CategoryMenu from '../inner/categoryMenu.vue'
import type { Product } from '@/types/product'
interface Product {
reference: number
product_id: number
name: string
image_link: string
link_rewrite: string
}
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.page) || 1, get: () => Number(route.query.p) || 1,
set: (val: number) => { set: (val: number) => {
router.push({ router.push({
query: { query: {
...route.query, ...route.query,
page: val p: val
} }
}) })
} }
@@ -101,7 +67,6 @@ const sortField = computed({
} }
}) })
const perPage = ref(15)
const total = ref(0) const total = ref(0)
interface ApiResponse { interface ApiResponse {
@@ -162,17 +127,25 @@ 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) params.append(key, String(value)) if (value === undefined || value === null) return
if (Array.isArray(value)) {
value.forEach(v => params.append(key, String(v)))
} else {
params.append(key, String(value))
}
}) })
const url = `/api/v1/restricted/list-products/get-listing?${params}` if (route.params.category_id)
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 = response.count || 0 total.value = Number(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 {
@@ -182,16 +155,11 @@ async function fetchProductList() {
function goToProduct(productId: number) { function goToProduct(productId: number) {
router.push({ router.push({
name: 'product-detail', name: 'customer-product-details',
params: { id: productId } params: { product_id: productId }
}) })
} }
const selectedCount = ref({
product_id: 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'
@@ -205,25 +173,7 @@ const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
const columns: TableColumn<Payment>[] = [ 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 }) => {
@@ -314,108 +264,17 @@ const columns: TableColumn<Payment>[] = [
}, },
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: () => {
console.log('Clicked', row.original) goToProduct(row.original.product_id)
}, },
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary', color: 'primary',
disabled: selectedCount.value.product_id !== row.original.product_id,
variant: 'solid' variant: 'solid'
}, 'Add to cart') }, () => 'Show product')
},
}
]
const columnsChild: TableColumn<Payment>[] = [
{
accessorKey: 'product_id',
header: '',
cell: ({ row }) => `#${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 }) => 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')
}, },
} }
] ]
@@ -427,4 +286,4 @@ watch(
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -1,126 +1,130 @@
<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">
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!" valueKey="iso_code"> <USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!"
<template #default="{ modelValue }"> valueKey="iso_code">
<div class="flex items-center gap-2"> <template #default="{ modelValue }">
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span> <div class="flex items-center gap-2">
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code == <span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
modelValue)?.name}}</span> <span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
</div> modelValue)?.name}}</span>
</template> </div>
<template #item-leading="{ item }"> </template>
<div class="flex items-center rounded-md cursor-pointer transition-colors"> <template #item-leading="{ item }">
<span class="text-md">{{ item.flag }}</span> <div class="flex items-center rounded-md cursor-pointer transition-colors">
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <span class="text-md">{{ item.flag }}</span>
</div> <span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</template> </div>
</USelect> </template>
</USelect>
</div>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Translate
</UButton>
</div> </div>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Translate
</UButton>
</div>
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl"> <div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
</div>
</div>
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" /> <UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
</div> </div>
</div> <div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p>
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
</div>
<div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p>
</div>
<div v-else-if="productStore.productDescription" class="flex items-start gap-30">
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
</div> </div>
<div class="flex flex-col gap-2"> <div v-else-if="productStore.productDescription" class="flex items-start gap-30">
<p class="text-[25px] font-bold text-black dark:text-white"> <div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
{{ productStore.productDescription.name || 'Product Name' }} <span class="text-gray-500 dark:text-gray-400">Product Image</span>
</p> </div>
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p> <div class="flex flex-col gap-2">
<div class="space-y-[10px]"> <p class="text-[25px] font-bold text-black dark:text-white">
<div class="flex items-center gap-1"> {{ productStore.productDescription.name || 'Product Name' }}
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" /> </p>
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> <p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
{{ productStore.productDescription.available_now }} <div class="space-y-[10px]">
</p> <div class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" />
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
{{ productStore.productDescription.available_now }}
</p>
</div>
<div class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" />
<p class="text-[18px] font-bold text-black dark:text-white">
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
</p>
</div>
</div> </div>
<div class="flex items-center gap-1"> </div>
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" /> </div>
<p class="text-[18px] font-bold text-black dark:text-white">
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }} <div v-if="productStore.productDescription" class="mt-16">
</p> <div class="flex gap-4 my-6">
<UButton @click="activeTab = 'description'"
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Description</p>
</UButton>
<UButton @click="activeTab = 'usage'"
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Usage</p>
</UButton>
</div>
<div v-if="activeTab === 'usage'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex justify-end items-center gap-3 mb-4">
<UButton v-if="!isEditing" @click="enableEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black">Save the edited text</p>
</UButton>
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
Cancel
</UButton>
</div>
<p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div>
<div v-if="activeTab === 'description'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex items-center justify-end gap-3 mb-4">
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline"
class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black ">Save the edited text</p>
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral"
variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
</div>
<div ref="descriptionRef" v-html="productStore.productDescription.description"
class="flex flex-col justify-center dark:text-white text-black">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="productStore.productDescription" class="mt-16">
<div class="flex gap-4 my-6">
<UButton @click="activeTab = 'description'"
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Description</p>
</UButton>
<UButton @click="activeTab = 'usage'"
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Usage</p>
</UButton>
</div>
<div v-if="activeTab === 'usage'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex justify-end items-center gap-3 mb-4">
<UButton v-if="!isEditing" @click="enableEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black">Save the edited text</p>
</UButton>
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
Cancel
</UButton>
</div>
<p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div>
<div v-if="activeTab === 'description'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex items-center justify-end gap-3 mb-4">
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
<p class="text-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black ">Save the edited text</p>
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral" variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
</div>
<div ref="descriptionRef" v-html="productStore.productDescription.description"
class="flex flex-col justify-center dark:text-white text-black">
</div>
</div>
</div>
</div>
</component> </component>
</template> </template>
@@ -142,7 +146,7 @@ const isEditing = ref(false)
const availableLangs = computed(() => langs) const availableLangs = computed(() => langs)
const selectedLanguage = ref('pl') const selectedLanguage = ref('en')
const currentLangId = ref(2) const currentLangId = ref(2)
const productID = ref<number>(0) const productID = ref<number>(0)
@@ -176,7 +180,7 @@ const translateToSelectedLanguage = async () => {
} }
onMounted(async () => { onMounted(async () => {
const id = route.params.id const id = route.params.product_id
if (id) { if (id) {
productID.value = Number(id) productID.value = Number(id)
await fetchForLanguage(selectedLanguage.value) await fetchForLanguage(selectedLanguage.value)

View File

@@ -1,6 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20"> <div class="container mx-auto">
<h2 <h2
class="font-semibold text-black dark:text-white pb-6 text-2xl"> class="font-semibold text-black dark:text-white pb-6 text-2xl">
{{ t('Cart Items') }} {{ t('Cart Items') }}

View File

@@ -1,6 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20"> <div class="container mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0"> <div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">

View File

@@ -1,6 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10"> <div class="container mx-auto flex flex-col gap-5 md:gap-10">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1> <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 flex-col lg:flex-row gap-5 md:gap-10">
<div class="flex-1"> <div class="flex-1">

View File

@@ -0,0 +1,9 @@
<template>
<component :is="Default || 'div'">
Orders page
</component>
</template>
<script lang="ts" setup>
import Default from '@/layouts/default.vue'
</script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mt-20 mx-auto"> <div class="container mx-auto">
<div class="flex md:flex-row flex-col justify-between gap-8 my-6"> <div class="flex md:flex-row flex-col justify-between gap-8 my-6">
<div class="flex-1"> <div class="flex-1">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] "> <div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">

View File

@@ -0,0 +1,430 @@
<template>
<suspense>
<component :is="Default || 'div'">
<div class="container mx-auto">
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
<template #item="{ item, active }">
<div class="flex items-center gap-2 px-3 py-2">
<UIcon name="i-heroicons-book-open" />
<span>{{ item.name }}</span>
</div>
</template>
</UNavigationMenu> -->
<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">
<div class="flex gap-2">
<CategoryMenu />
<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>
<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>
</component>
</suspense>
</template>
<script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/categoryMenu.vue'
interface Product {
reference: number
product_id: number
name: string
image_link: string
link_rewrite: string
}
const router = useRouter()
const route = useRoute()
const page = computed({
get: () => Number(route.query.page) || 1,
set: (val: number) => {
router.push({
query: {
...route.query,
page: val
}
})
}
})
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const currentSort = route.query.sort as string | undefined
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
let query = { ...route.query }
if (currentSort === sort) {
if (currentDirection === 'asc') {
query.direction = 'desc'
} else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
query.direction = 'asc'
query.sort = sort
}
} else {
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
const perPage = ref(15)
const total = ref(0)
interface ApiResponse {
message: string
items: Product[]
count: number
}
const productsList = ref<Product[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(key => {
if (!['page', 'sort', 'direction'].includes(key)) {
delete baseQuery[key]
}
})
router.push({
query: {
...baseQuery,
...val,
page: 1
}
})
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
async function fetchProductList() {
loading.value = true
error.value = null
const params = new URLSearchParams()
Object.entries(route.query).forEach(([key, value]) => {
if (value) params.append(key, String(value))
})
const url = `/api/v1/restricted/list/list-products?${params}`
try {
const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || []
total.value = response.count || 0
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load products'
} finally {
loading.value = false
}
}
function goToProduct(productId: number) {
router.push({
name: 'product-detail',
params: { id: productId }
})
}
const selectedCount = ref({
product_id: null,
count: 0
})
function getIcon(name: string) {
if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const UInputNumber = resolveComponent('UInputNumber')
const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon')
const columns: TableColumn<Payment>[] = [
{
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',
header: ({ column }) => {
return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['product_id', 'asc']
}
}, [
h('span', 'ID'),
h(UIcon, {
name: getIcon('product_id')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
// header: '#',
cell: ({ row }) => `#${row.getValue('product_id') as number}`
},
{
accessorKey: 'image_link',
header: 'Image',
cell: ({ row }) => {
return h('img', {
src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;'
})
}
},
{
accessorKey: 'name',
header: ({ column }) => {
return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['name', 'asc']
}
}, [
h('span', 'Name'),
h(UIcon, {
name: getIcon('name')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => row.getValue('name') as string,
filterFn: (row, columnId, value) => {
const name = row.getValue(columnId) as string
return name.toLowerCase().includes(value.toLowerCase())
}
},
{
accessorKey: 'quantity',
header: ({ column }) => {
return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['quantity', 'asc']
}
}, [
h('span', 'In stock'),
h(UIcon, {
name: getIcon('quantity')
})
]),
])
},
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',
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')
},
}
]
const columnsChild: TableColumn<Payment>[] = [
{
accessorKey: 'product_id',
header: '',
cell: ({ row }) => `#${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 }) => 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')
},
}
]
watch(
() => route.query,
() => {
fetchProductList()
},
{ immediate: true }
)
</script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20"> <div class="container mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1>

View File

@@ -1,106 +1,114 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container mx-auto mt-20"> <div class="container mx-auto">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1>
<div <div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4"> class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
<UForm @submit.prevent="saveAccount" :validate="validate" class="space-y-6"> <UForm @submit.prevent="saveAccount" :validate="validate" class="space-y-6">
<div> <div>
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2"> <h2
<UIcon name="mdi:domain" class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> <UIcon name="mdi:domain"
{{ t('Company Information') }} class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
</h2> {{ t('Company Information') }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> </h2>
<div class="md:col-span-2"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ <div class="md:col-span-2">
t('Company Name') }} *</label> <label class="block text-sm font-medium text-black dark:text-white mb-1">{{
<UInput v-model="formData.companyName" :placeholder="t('Enter company name')" t('Company Name') }} *</label>
name="companyName" class="w-full" /> <UInput v-model="formData.companyName" :placeholder="t('Enter company name')"
</div> name="companyName" class="w-full" />
<div class="md:col-span-2"> </div>
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ <div class="md:col-span-2">
t('Company Email') }} *</label> <label class="block text-sm font-medium text-black dark:text-white mb-1">{{
<UInput v-model="formData.companyEmail" type="email" t('Company Email') }} *</label>
:placeholder="t('Enter company email')" name="companyEmail" class="w-full" /> <UInput v-model="formData.companyEmail" type="email"
</div> :placeholder="t('Enter company email')" name="companyEmail"
<div> class="w-full" />
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ </div>
t('REGON') }}</label> <div>
<UInput v-model="formData.regon" :placeholder="t('Enter REGON')" name="regon" <label class="block text-sm font-medium text-black dark:text-white mb-1">{{
class="w-full" /> t('REGON') }}</label>
</div> <UInput v-model="formData.regon" :placeholder="t('Enter REGON')" name="regon"
<div> class="w-full" />
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('NIP') </div>
}}</label> <div>
<UInput v-model="formData.nip" :placeholder="t('Enter NIP')" name="nip" <label class="block text-sm font-medium text-black dark:text-white mb-1">{{
class="w-full" /> t('NIP')
</div> }}</label>
<div> <UInput v-model="formData.nip" :placeholder="t('Enter NIP')" name="nip"
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('VAT') class="w-full" />
}}</label> </div>
<UInput v-model="formData.vat" :placeholder="t('Enter VAT')" name="vat" <div>
class="w-full" /> <label class="block text-sm font-medium text-black dark:text-white mb-1">{{
t('VAT')
}}</label>
<UInput v-model="formData.vat" :placeholder="t('Enter VAT')" name="vat"
class="w-full" />
</div>
</div> </div>
</div> </div>
</div> <div>
<div> <h2
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2"> class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
<UIcon name="mdi:map-marker" <UIcon name="mdi:map-marker"
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" /> class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
{{ t('Select Addresses') }} {{ t('Select Addresses') }}
</h2> </h2>
<div <div class="bg-(--second-light) dark:bg-(--main-dark)">
class="bg-(--second-light) dark:bg-(--main-dark)"> <div class="mb-4">
<div class="mb-4"> <UInput v-model="addressSearchQuery" type="text"
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')" :placeholder="t('Search address')"
class="w-full bg-white dark:bg-(--black) text-black dark:text-white" /> class="w-full bg-white dark:bg-(--black) text-black dark:text-white" />
</div> </div>
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3"> <div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
<label v-for="address in addressStore.filteredAddresses" :key="address.id" <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="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
:class="cartStore.selectedAddressId === address.id :class="cartStore.selectedAddressId === address.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20' ? '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'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress" <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)" /> class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1"> <div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }}</p> <p class="text-black dark:text-white font-medium">{{ address.street }}
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, </p>
{{ address.city }}</p> <p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }} }},
</p> {{ address.city }}</p>
</div> <p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country
</label> }}
</div> </p>
<div v-else class="text-center py-6"> </div>
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" /> </label>
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p> </div>
<RouterLink :to="{ name: 'addresses' }" <div v-else class="text-center py-6">
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline"> <UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
{{ t('Add Address') }} <p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
</RouterLink> <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> </div>
</div> <div class="flex justify-end gap-3 pt-4">
<div class="flex justify-end gap-3 pt-4"> <UButton variant="outline" color="neutral" @click="goBack"
<UButton variant="outline" color="neutral" @click="goBack" class="text-black dark:text-white">
class="text-black dark:text-white"> {{ t('Cancel') }}
{{ t('Cancel') }} </UButton>
</UButton> <UButton type="submit" color="primary"
<UButton type="submit" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)"> <UIcon name="mdi:content-save" />
<UIcon name="mdi:content-save" /> {{ t('Save') }}
{{ t('Save') }} </UButton>
</UButton> </div>
</div> </UForm>
</UForm> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</component> </component>
</template> </template>
@@ -170,7 +178,7 @@ function saveAccount() {
vat: formData.value.vat, vat: formData.value.vat,
companyAddressId: formData.value.companyAddressId, companyAddressId: formData.value.companyAddressId,
billingAddressId: formData.value.billingAddressId, billingAddressId: formData.value.billingAddressId,
companyAddress : '' companyAddress: ''
}) })
router.push({ name: 'profile-details' }) router.push({ name: 'profile-details' })

View File

@@ -0,0 +1,9 @@
<template>
<component :is="Default || 'div'">
Statistic page
</component>
</template>
<script lang="ts" setup>
import Default from '@/layouts/default.vue'
</script>

View File

@@ -1,20 +1,57 @@
<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) {
console.log(item); item.open = path && path.includes(item.category_id) ? true : openAll.value
adaptMenu(item.children); adaptMenu(item.children);
item.open = openAll.value item.children.unshift({
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } }) label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: {
name: 'customer-products-category', params: {
category_id: item.params.category_id,
link_rewrite: item.params.link_rewrite
}
}
})
} else { } else {
item.to = { name: 'category', params: item.params }; item.to = {
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'
} }
} }
@@ -22,15 +59,10 @@ function adaptMenu(menu: NavigationMenuItem[]) {
} }
menu = adaptMenu(menu) menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([ const items = ref<NavigationMenuItem[][]>([
[ [
...menu as NavigationMenuItem[] ...menu as NavigationMenuItem[]
], ],
]) ])
</script> </script>
<template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
</template>

View File

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

@@ -1,16 +1,16 @@
<template> <template>
{{ locale }} <USelectMenu v-model="locale" :items="langs"
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code"
valueKey="iso_code" :searchInput="false"> :searchInput="false">
<template #default="{ modelValue }"> <template #default="{ modelValue }">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> <!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> -->
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.name}}</span> <span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.name}}</span>
</div> </div>
</template> </template>
<template #item-leading="{ item }"> <template #item-leading="{ item }">
<div class="flex items-center rounded-md cursor-pointer transition-colors"> <div class="flex items-center rounded-md cursor-pointer transition-colors">
<span class="text-md ">{{ item.flag }}</span> <!-- <span class="text-md ">{{ item.flag }}</span> -->
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</div> </div>
</template> </template>
@@ -23,6 +23,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useCookie } from '@/composable/useCookie' import { useCookie } from '@/composable/useCookie'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import { useFetchJson } from '@/composable/useFetchJson'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -50,9 +51,22 @@ const locale = computed({
router.replace({ path: '/' + value + currentPath, query: route.query }) router.replace({ path: '/' + value + currentPath, query: route.query })
} }
} }
changeLang()
}, },
}) })
async function changeLang() {
try {
const { items } = await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}
}
watch( watch(
() => route.params.locale, () => route.params.locale,
(newLocale) => { (newLocale) => {

View File

@@ -4,9 +4,11 @@ import TopBar from '@/components/TopBar.vue';
<template> <template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]"> <div class="h-screen grid grid-rows-[auto_1fr_auto]">
<main class="p-10"> <main class="pt-20 pb-10">
<TopBar/> <TopBar />
<slot></slot> <div class="container mx-auto px-4">
<slot />
</div>
</main> </main>
</div> </div>
</template> </template>

View File

@@ -49,6 +49,41 @@ const router = createRouter({
], ],
}) })
const viewModules = import.meta.glob('/src/views/**/*.vue')
const componentModules = import.meta.glob('/src/components/**/*.vue')
async function setRoutes() {
const routes = await getRoutes()
for (const item of routes) {
const componentName = item.component
const [, folder] = componentName.split('/')
const componentPath = `/src${componentName}`
let modules =
folder === 'views' ? viewModules : componentModules
const importer = modules[componentPath]
if (!importer) {
console.error('Component not found:', componentPath)
continue
}
const importedComponent = (await importer()).default
router.addRoute('locale', {
path: item.path,
component: importedComponent,
name: item.name,
meta: item.meta ? JSON.parse(item.meta) : {}
})
}
}
await setRoutes()
router.beforeEach((to, from) => { router.beforeEach((to, from) => {
const locale = to.params.locale as string const locale = to.params.locale as string
const localeLang = langs.find((x) => x.iso_code === locale) const localeLang = langs.find((x) => x.iso_code === locale)

View File

@@ -1,17 +1,20 @@
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 () => {
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu'); if(!categoryId.value){
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
} }
export const getRoutes = async () => { export const getRoutes = async () => {
const resp = await useFetchJson<Route[]>('/api/v1/public/menu/get-routes'); const resp = await useFetchJson<Route[]>('/api/v1/public/menu/get-routes');
return resp.items return resp.items
} }

View File

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

View File

@@ -34,9 +34,8 @@ export const useProductStore = defineStore('product', () => {
try { try {
const response = await useFetchJson<ProductDescription>( const response = await useFetchJson<ProductDescription>(
`/api/v1/restricted/product-description/get-product-description?productID=${productID}&productLangID=${langId}` `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
) )
console.log(response, 'dfsfsdf');
productDescription.value = response.items productDescription.value = response.items
} catch (e: unknown) { } catch (e: unknown) {

View File

@@ -31,14 +31,15 @@ export interface TopMenuItem {
export interface Label { export interface Label {
label: string label: string
trans:LabelTrans trans: LabelTrans
icon?: string
} }
export interface LabelTrans{ export interface LabelTrans {
pl:LabelItem pl: LabelItem
en:LabelItem en: LabelItem
de: LabelItem de: LabelItem
} }
export interface LabelItem { export interface LabelItem {
label: string label: string
} }

View File

@@ -1,4 +1,4 @@
export interface ProductDescription { export interface ProductDescription {
id?: number id?: number
name?: string name?: string
description: string description: string
@@ -6,4 +6,12 @@
meta_description: string meta_description: string
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,6 +7,7 @@ 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

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{bas_url}}/restricted/list-products/get-listing?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62" url: "{{bas_url}}/restricted/list/list-products?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62"
params: params:
- name: p - name: p
value: "1" value: "1"

View File

@@ -10,12 +10,12 @@ http:
type: json type: json
data: |- data: |-
{ {
"q": "kinder", "q": "mat",
"limit": 50, "limit": 50,
"offset": 0, "offset": 0,
// "filter": "'attr.10'= 71", "filter": "'category_ids'= 10",
"facets":["category_ids", "price"] // "facets":["category_ids", "price"]
// "facets": ["category_ids", "attr", "feat", "price"] "facets": ["category_ids", "attr", "feat", "price"]
} }
auth: auth:
type: bearer type: bearer

View File

@@ -0,0 +1,28 @@
info:
name: save-product-description
type: http
seq: 19
http:
method: POST
url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=3
params:
- name: productID
value: "1"
type: query
- name: productLangID
value: "3"
type: query
body:
type: json
data: |-
{
"description": "<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

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

9
bruno/b2b_daniel/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Secrets
.env*
# Dependencies
node_modules
# OS files
.DS_Store
Thumbs.db

View File

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

View File

@@ -0,0 +1,22 @@
info:
name: update-choice
type: http
seq: 1
http:
method: POST
url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=0&country_id=1
params:
- name: lang_id
value: "0"
type: query
- name: country_id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,25 @@
info:
name: add-product-to-cart (1)
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&amount=1
params:
- name: cart_id
value: "1"
type: query
- name: product_id
value: "51"
type: query
- name: amount
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,28 @@
info:
name: add-product-to-cart
type: http
seq: 14
http:
method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=1
params:
- name: cart_id
value: "1"
type: query
- name: product_id
value: "51"
type: query
- name: product_attribute_id
value: "1115"
type: query
- name: amount
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: change-cart-name
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/carts/change-cart-name?cart_id=1&new_name=test
params:
- name: cart_id
value: "1"
type: query
- name: new_name
value: test
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
info:
name: get_countries
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/langs-and-countries/get-countries
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,24 @@
info:
name: list-products
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10&target_user_id=2
params:
- name: p
value: "1"
type: query
- name: elems
value: "10"
type: query
- name: target_user_id
value: "2"
type: query
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,21 @@
info:
name: list-users
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/list/list-users?p=1&elems=10
params:
- name: p
value: "1"
type: query
- name: elems
value: "10"
type: query
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,22 @@
info:
name: get-breadcrumb
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13
params:
- name: root_category_id
value: "10"
type: query
- name: category_id
value: "13"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,21 @@
opencollection: 1.0.0
info:
name: b2b-daniel
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git

View File

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

View File

@@ -0,0 +1,22 @@
info:
name: get-product-description
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=1
params:
- name: productID
value: "51"
type: query
- name: productLangID
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
info:
name: get-indexes
type: http
seq: 1
http:
method: GET
url: http://localhost:7700/indexes
auth:
type: bearer
token: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

Some files were not shown because too many files have changed in this diff Show More