Compare commits
62 Commits
91c5de1f67
...
addresses
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1083ab7a61 | ||
| 75997ab15b | |||
|
|
569a805a13 | ||
|
|
578d8c6cac | ||
|
|
cbd0baaa50 | ||
|
|
7eee0bd032 | ||
| 92ba9c5f07 | |||
| a121ddc246 | |||
| d56650ae5d | |||
| 1a6311dc3d | |||
| 2e645f3368 | |||
| de3f2d1777 | |||
| 9187297367 | |||
| 813d1f4879 | |||
| c5cc4f7a48 | |||
| 76ca2a2eed | |||
| 84388792f0 | |||
|
|
7264a11ba6 | ||
| 61dc240c38 | |||
|
|
f6b321b602 | ||
|
|
af91842b14 | ||
| 04e238fd66 | |||
|
|
e0c53c97ba | ||
| 09a77c14c9 | |||
|
|
c7533a8deb | ||
|
|
1bab7f642f | ||
|
|
a988bbbc33 | ||
| 701004d005 | |||
| c31964c41b | |||
| 0ed9d792b6 | |||
|
|
395d670298 | ||
|
|
7d4242abb1 | ||
|
|
9c7eb5ee4e | ||
|
|
833f4a5a07 | ||
|
|
b9bc121d43 | ||
|
|
b2acb8c922 | ||
| cf4d14a3cb | |||
| 30eb82ba53 | |||
| a2a2c35ab3 | |||
|
|
03f04b2f53 | ||
|
|
55da953f32 | ||
| 684f910090 | |||
| 5feaa9e15c | |||
|
|
04e2549a66 | ||
| 6428ddb527 | |||
| fb4f7048ab | |||
|
|
a3f01eca7c | ||
|
|
1fa6206b75 | ||
|
|
fa89723eb6 | ||
|
|
8665c566ee | ||
| ec5ff123ac | |||
| 17317e778c | |||
| 94291ccc03 | |||
|
|
d0ce65c287 | ||
| 05bfa6e8b8 | |||
| f4ad8e02b4 | |||
| bd97ed1a3b | |||
| df14eb5ae4 | |||
| f5d524d45b | |||
| 78bdac8ff0 | |||
| 2c128a4b36 | |||
| dd806bbb1e |
6
.env
6
.env
@@ -48,6 +48,10 @@ EMAIL_FROM=test@ma-al.com
|
||||
EMAIL_FROM_NAME=Gitea Manager
|
||||
EMAIL_ADMIN=goc_marek@ma-al.pl
|
||||
|
||||
# STORAGE
|
||||
STORAGE_ROOT=./storage
|
||||
|
||||
|
||||
I18N_LANGS=en,pl,cs
|
||||
|
||||
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
|
||||
CORS_ORGIN=https://www.naluconcept.com
|
||||
|
||||
DSN=root:Maal12345678@tcp(localhost:3306)/nalu
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ bin/
|
||||
i18n/*.json
|
||||
*_templ.go
|
||||
tmp/main
|
||||
test.go
|
||||
test.go
|
||||
storage/*
|
||||
!storage/.gitkeep
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "./app/cmd/main.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1127,21 +1127,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/menu/get-menu": {
|
||||
"/api/v1/restricted/menu/get-category-tree": {
|
||||
"get": {
|
||||
"tags": ["Menu"],
|
||||
"summary": "Get menu structure",
|
||||
"description": "Returns the menu structure for the current language. Requires authentication.",
|
||||
"operationId": "getMenu",
|
||||
"summary": "Get category tree",
|
||||
"description": "Returns the category tree rooted at the given category ID for the current language. Requires authentication.",
|
||||
"operationId": "getCategoryTree",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": [],
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "root_category_id",
|
||||
"in": "query",
|
||||
"description": "Root category ID to build the tree from",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Menu retrieved successfully",
|
||||
"description": "Category tree retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -1151,7 +1162,73 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -1221,7 +1298,23 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "Application route",
|
||||
@@ -2338,6 +2391,58 @@
|
||||
"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": {
|
||||
|
||||
@@ -2,8 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,7 +26,8 @@ type Config struct {
|
||||
GoogleTranslate GoogleTranslateConfig
|
||||
Image ImageConfig
|
||||
Cors CorsConfig
|
||||
MailiSearch MeiliSearchConfig
|
||||
MeiliSearch MeiliSearchConfig
|
||||
Storage StorageConfig
|
||||
}
|
||||
|
||||
type I18n struct {
|
||||
@@ -95,6 +98,10 @@ type EmailConfig struct {
|
||||
Enabled bool `env:"EMAIL_ENABLED,false"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
RootFolder string `env:"STORAGE_ROOT"`
|
||||
}
|
||||
|
||||
type PdfPrinter struct {
|
||||
ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"`
|
||||
}
|
||||
@@ -155,7 +162,7 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.OAuth.Google)
|
||||
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)
|
||||
@@ -170,12 +177,12 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.I18n)
|
||||
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)
|
||||
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)
|
||||
@@ -185,19 +192,25 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.Image)
|
||||
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)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -308,6 +321,22 @@ func setValue(field reflect.Value, val string, key string) error {
|
||||
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) {
|
||||
if tag == "" {
|
||||
return "", nil
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"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/localeExtractor"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
@@ -60,9 +64,52 @@ func AuthMiddleware() fiber.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// Set user in context
|
||||
c.Locals(constdata.USER_LOCALES_NAME, user.ToSession())
|
||||
c.Locals(constdata.USER_LOCALES_ID, user.ID)
|
||||
// Create locale. LangID is overwritten by auth Token
|
||||
var userLocale model.UserLocale
|
||||
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 model.CustomerRole(user.Role.Name) != 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()
|
||||
}
|
||||
@@ -71,21 +118,14 @@ func AuthMiddleware() fiber.Handler {
|
||||
// RequireAdmin creates admin-only middleware
|
||||
func RequireAdmin() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "not authenticated",
|
||||
})
|
||||
}
|
||||
|
||||
userSession, ok := user.(*model.UserSession)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "invalid user session",
|
||||
})
|
||||
}
|
||||
|
||||
if userSession.Role != model.RoleAdmin {
|
||||
if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "admin access required",
|
||||
})
|
||||
@@ -95,22 +135,70 @@ func RequireAdmin() fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserID extracts user ID from context
|
||||
func GetUserID(c fiber.Ctx) uint {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return userID
|
||||
}
|
||||
// Webdav
|
||||
func Webdav() fiber.Handler {
|
||||
authService := authService.NewAuthService()
|
||||
|
||||
// GetUser extracts user from context
|
||||
func GetUser(c fiber.Ctx) *model.UserSession {
|
||||
user, ok := c.Locals("user").(*model.UserSession)
|
||||
if !ok {
|
||||
return nil
|
||||
return func(c fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "authorization token required",
|
||||
})
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Basic ") {
|
||||
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization token",
|
||||
})
|
||||
}
|
||||
|
||||
encoded := strings.TrimPrefix(authHeader, "Basic ")
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization token",
|
||||
})
|
||||
}
|
||||
|
||||
credentials := strings.SplitN(string(decoded), ":", 2)
|
||||
rawToken := ""
|
||||
if len(credentials) == 1 {
|
||||
rawToken = credentials[0]
|
||||
} else if len(credentials) == 2 {
|
||||
rawToken = credentials[1]
|
||||
}
|
||||
if len(rawToken) != constdata.NBYTES_IN_WEBDAV_TOKEN*2 {
|
||||
c.Set("WWW-Authenticate", `Basic realm="webdav"`)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization token",
|
||||
})
|
||||
}
|
||||
|
||||
// we identify user based on this token.
|
||||
user, err := authService.GetUserByWebdavToken(rawToken)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "user not found",
|
||||
})
|
||||
}
|
||||
|
||||
if user.WebdavExpires != nil && user.WebdavExpires.Before(time.Now()) {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid or expired token",
|
||||
})
|
||||
}
|
||||
|
||||
var userLocale model.UserLocale
|
||||
userLocale.OriginalUser = user
|
||||
userLocale.User = user
|
||||
c.Locals(constdata.USER_LOCALE, &userLocale)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// GetConfig returns the app config
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -22,12 +24,8 @@ func LanguageMiddleware() fiber.Handler {
|
||||
if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,12 +36,8 @@ func LanguageMiddleware() fiber.Handler {
|
||||
if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,8 +51,7 @@ func LanguageMiddleware() fiber.Handler {
|
||||
lang, err := langService.GetLanguageByISOCode(isoCode)
|
||||
if err == nil && lang != nil {
|
||||
langID = uint(lang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -68,8 +61,7 @@ func LanguageMiddleware() fiber.Handler {
|
||||
defaultLang, err := langService.GetDefaultLanguage()
|
||||
if err == nil && defaultLang != nil {
|
||||
langID = uint(defaultLang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", defaultLang)
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
@@ -104,11 +96,9 @@ func parseAcceptLanguage(header string) string {
|
||||
return strings.ToLower(first)
|
||||
}
|
||||
|
||||
// GetLanguageID extracts language ID from context
|
||||
func GetLanguageID(c fiber.Ctx) uint {
|
||||
langID, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return langID
|
||||
func returnNewLocale(lang_id uint) *model.UserLocale {
|
||||
newLocale := model.UserLocale{}
|
||||
newLocale.OriginalUser = &model.Customer{}
|
||||
newLocale.OriginalUser.LangID = lang_id
|
||||
return &newLocale
|
||||
}
|
||||
|
||||
28
app/delivery/middleware/permissions.go
Normal file
28
app/delivery/middleware/permissions.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Require(p perms.Permission) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
u := c.Locals("user")
|
||||
if u == nil {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
user, ok := u.(*model.UserSession)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, perm := range user.Permissions {
|
||||
if perm == p {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
return c.SendStatus(fiber.StatusForbidden)
|
||||
}
|
||||
}
|
||||
10
app/delivery/middleware/perms/permissions.go
Normal file
10
app/delivery/middleware/perms/permissions.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package perms
|
||||
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
UserReadAny Permission = "user.read.any"
|
||||
UserWriteAny Permission = "user.write.any"
|
||||
UserDeleteAny Permission = "user.delete.any"
|
||||
CurrencyWrite Permission = "currency.write"
|
||||
)
|
||||
@@ -2,13 +2,17 @@ package public
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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/model"
|
||||
"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/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"
|
||||
@@ -264,15 +268,15 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
|
||||
|
||||
// Me returns the current user info
|
||||
func (h *AuthHandler) Me(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
userLocale := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if userLocale.OriginalUser == nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated),
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Updates JWT Tokens
|
||||
// Updates JWT Tokens. Requires authentication and updates access token only
|
||||
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
|
||||
@@ -414,12 +458,12 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
||||
|
||||
// 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".
|
||||
lang, err := h.authService.GetLangISOCode(response.User.LangID)
|
||||
lang_iso_code, err := h.authService.GetLangISOCode(response.User.LangID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadLangID)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package public
|
||||
import (
|
||||
"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/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"
|
||||
@@ -30,7 +31,7 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
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)))
|
||||
|
||||
157
app/delivery/web/api/restricted/addresses.go
Normal file
157
app/delivery/web/api/restricted/addresses.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type AddressesHandler struct {
|
||||
addressesService *addressesService.AddressesService
|
||||
}
|
||||
|
||||
func NewAddressesHandler() *AddressesHandler {
|
||||
addressesService := addressesService.New()
|
||||
return &AddressesHandler{
|
||||
addressesService: addressesService,
|
||||
}
|
||||
}
|
||||
|
||||
func AddressesHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewAddressesHandler()
|
||||
|
||||
r.Get("/get-template", handler.GetTemplate)
|
||||
r.Post("/add-new-address", handler.AddNewAddress)
|
||||
r.Post("/modify-address", handler.ModifyAddress)
|
||||
r.Get("/retrieve-addresses", handler.RetrieveAddressesInfo)
|
||||
r.Delete("/delete-address", handler.DeleteAddress)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *AddressesHandler) GetTemplate(c fiber.Ctx) error {
|
||||
country_id_attribute := c.Query("country_id")
|
||||
country_id, err := strconv.Atoi(country_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
template, err := h.addressesService.GetTemplate(uint(country_id))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&template, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *AddressesHandler) AddNewAddress(c fiber.Ctx) error {
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
address_info := string(c.Body())
|
||||
if address_info == "" {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
country_id_attribute := c.Query("country_id")
|
||||
country_id, err := strconv.Atoi(country_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *AddressesHandler) ModifyAddress(c fiber.Ctx) error {
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
address_id_attribute := c.Query("address_id")
|
||||
address_id, err := strconv.Atoi(address_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
address_info := string(c.Body())
|
||||
if address_info == "" {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
country_id_attribute := c.Query("country_id")
|
||||
country_id, err := strconv.Atoi(country_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
address_id_attribute := c.Query("address_id")
|
||||
address_id, err := strconv.Atoi(address_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
err = h.addressesService.DeleteAddress(userID, uint(address_id))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"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/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"
|
||||
@@ -37,7 +38,7 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
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)))
|
||||
@@ -53,7 +54,7 @@ func (h *CartsHandler) AddNewCart(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 {
|
||||
return c.Status(responseErrors.GetErrorStatus(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 {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
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)))
|
||||
@@ -94,7 +95,7 @@ func (h *CartsHandler) RetrieveCartsInfo(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 {
|
||||
return c.Status(responseErrors.GetErrorStatus(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 {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
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)))
|
||||
|
||||
70
app/delivery/web/api/restricted/currency.go
Normal file
70
app/delivery/web/api/restricted/currency.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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/perms"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
|
||||
"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"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type CurrencyHandler struct {
|
||||
CurrencyService *currencyService.CurrencyService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewCurrencyHandler() *CurrencyHandler {
|
||||
currencyService := currencyService.New()
|
||||
return &CurrencyHandler{
|
||||
CurrencyService: currencyService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func CurrencyHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewCurrencyHandler()
|
||||
|
||||
r.Post("/currency-rate", middleware.Require(perms.CurrencyWrite), handler.PostCurrencyRate)
|
||||
r.Get("/currency-rate/:id", handler.GetCurrencyRate)
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
|
||||
var currencyRate model.CurrencyRate
|
||||
if err := c.Bind().Body(¤cyRate); err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrJSONBody)))
|
||||
}
|
||||
|
||||
err := h.CurrencyService.CreateCurrencyRate(¤cyRate)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 1, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
}
|
||||
|
||||
currency, err := h.CurrencyService.GetCurrency(uint(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(currency, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
111
app/delivery/web/api/restricted/customer.go
Normal file
111
app/delivery/web/api/restricted/customer.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/customerService"
|
||||
"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"
|
||||
)
|
||||
|
||||
type customerHandler struct {
|
||||
service *customerService.CustomerService
|
||||
}
|
||||
|
||||
func NewCustomerHandler() *customerHandler {
|
||||
customerService := customerService.New()
|
||||
return &customerHandler{
|
||||
service: customerService,
|
||||
}
|
||||
}
|
||||
|
||||
func CustomerHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewCustomerHandler()
|
||||
|
||||
r.Get("", handler.customerData)
|
||||
r.Get("/list", handler.listCustomers)
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *customerHandler) customerData(fc fiber.Ctx) error {
|
||||
var customerId uint
|
||||
|
||||
user, ok := localeExtractor.GetCustomer(fc)
|
||||
if !ok || user == nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
customerIdStr := fc.Query("id")
|
||||
if customerIdStr != "" {
|
||||
id, err := strconv.ParseUint(customerIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
if user.ID != uint(id) && !user.HasPermission(perms.UserReadAny) {
|
||||
return fc.Status(fiber.StatusForbidden).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
|
||||
}
|
||||
|
||||
customerId = uint(id)
|
||||
} else {
|
||||
customerId = user.ID
|
||||
}
|
||||
|
||||
customer, err := h.service.GetById(customerId)
|
||||
if err != nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
|
||||
}
|
||||
|
||||
return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
|
||||
user, ok := localeExtractor.GetCustomer(fc)
|
||||
if !ok || user == nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
if !user.HasPermission(perms.UserReadAny) {
|
||||
return fc.Status(fiber.StatusForbidden).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
|
||||
}
|
||||
|
||||
p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers)
|
||||
if err != nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
|
||||
}
|
||||
|
||||
search := fc.Query("search")
|
||||
if search != "" {
|
||||
if !user.HasPermission(perms.UserReadAny) {
|
||||
return fc.Status(fiber.StatusForbidden).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
|
||||
}
|
||||
}
|
||||
|
||||
customer, err := h.service.Find(user.LangID, p, filt, search)
|
||||
if err != nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
|
||||
}
|
||||
|
||||
return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK)))
|
||||
}
|
||||
|
||||
var columnMappingListUsers map[string]string = map[string]string{
|
||||
"user_id": "users.id",
|
||||
"email": "users.email",
|
||||
"first_name": "users.first_name",
|
||||
"last_name": "users.last_name",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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/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"
|
||||
@@ -23,34 +26,73 @@ func NewMenuHandler() *MenuHandler {
|
||||
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
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)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
func (h *MenuHandler) GetCategoryTree(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)))
|
||||
}
|
||||
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 {
|
||||
return c.Status(responseErrors.GetErrorStatus(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) GetTopMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
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)))
|
||||
}
|
||||
menu, err := h.menuService.GetTopMenu(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_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 {
|
||||
customer, ok := localeExtractor.GetCustomer(c)
|
||||
if !ok || customer == nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
menu, err := h.menuService.GetTopMenu(customer.LangID, customer.RoleID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
109
app/delivery/web/api/restricted/product.go
Normal file
109
app/delivery/web/api/restricted/product.go
Normal file
@@ -0,0 +1,109 @@
|
||||
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/productService"
|
||||
"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"
|
||||
)
|
||||
|
||||
type ProductsHandler struct {
|
||||
productService *productService.ProductService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewListProductsHandler creates a new ListProductsHandler instance
|
||||
func NewProductsHandler() *ProductsHandler {
|
||||
productService := productService.New()
|
||||
return &ProductsHandler{
|
||||
productService: productService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewProductsHandler()
|
||||
|
||||
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
|
||||
r.Get("/list", handler.ListProducts)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
|
||||
p_id_product, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
country_idStr := c.Params("country_id")
|
||||
|
||||
b2b_id_country, err := strconv.Atoi(country_idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
quantityStr := c.Params("quantity")
|
||||
|
||||
p_quantity, err := strconv.Atoi(quantityStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
customer, ok := localeExtractor.GetCustomer(c)
|
||||
if !ok || customer == nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
productJson, err := h.productService.GetJSON(p_id_product, int(customer.LangID), int(customer.ID), b2b_id_country, p_quantity)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&productJson, 1, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *ProductsHandler) 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.productService.Find(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",
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
|
||||
"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"
|
||||
@@ -41,7 +43,7 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
|
||||
// GetProductDescription returns the product description for a given product ID
|
||||
func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
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)))
|
||||
@@ -72,12 +74,18 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
|
||||
// SaveProductDescription saves the description for a given product ID, in given language
|
||||
func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
userRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
|
||||
}
|
||||
|
||||
productID_attribute := c.Query("productID")
|
||||
productID, err := strconv.Atoi(productID_attribute)
|
||||
if err != nil {
|
||||
@@ -109,12 +117,18 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
|
||||
// TranslateProductDescription returns translated product description
|
||||
func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
userRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
|
||||
}
|
||||
|
||||
productID_attribute := c.Query("productID")
|
||||
productID, err := strconv.Atoi(productID_attribute)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
|
||||
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"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"
|
||||
@@ -36,12 +38,18 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||
id_lang, ok := c.Locals("langID").(uint)
|
||||
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)))
|
||||
}
|
||||
|
||||
userRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
|
||||
}
|
||||
|
||||
err := h.meiliService.CreateIndex(id_lang)
|
||||
if err != nil {
|
||||
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)))
|
||||
}
|
||||
|
||||
nothing := ""
|
||||
return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK)))
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
|
||||
id_lang, ok := c.Locals("langID").(uint)
|
||||
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)))
|
||||
@@ -88,7 +95,7 @@ func (h *MeiliSearchHandler) Search(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 {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
|
||||
100
app/delivery/web/api/restricted/storage.go
Normal file
100
app/delivery/web/api/restricted/storage.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type StorageHandler struct {
|
||||
storageService *storageService.StorageService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewStorageHandler() *StorageHandler {
|
||||
return &StorageHandler{
|
||||
storageService: storageService.New(),
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func StorageHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewStorageHandler()
|
||||
|
||||
// for all users
|
||||
r.Get("/list-content/*", handler.ListContent)
|
||||
r.Get("/download-file/*", handler.DownloadFile)
|
||||
|
||||
// for admins only
|
||||
r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// accepted path looks like e.g. "/folder1/" or "folder1"
|
||||
func (h *StorageHandler) ListContent(c fiber.Ctx) error {
|
||||
// relative path defaults to root directory
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
entries_in_list, err := h.storageService.ListContent(abs_path)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(entries_in_list, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
c.Attachment(filename)
|
||||
c.Set("Content-Length", strconv.FormatInt(filesize, 10))
|
||||
c.Set("Content-Type", "application/octet-stream")
|
||||
return c.SendStream(f, int(filesize))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
}
|
||||
|
||||
userRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
|
||||
}
|
||||
|
||||
new_token, err := h.storageService.NewWebdavToken(userID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&new_token, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -21,10 +21,12 @@ type SettingsResponse struct {
|
||||
|
||||
// AppSettings represents app configuration
|
||||
type AppSettings struct {
|
||||
Name string `json:"name"`
|
||||
Environment string `json:"environment"`
|
||||
BaseURL string `json:"base_url"`
|
||||
PasswordRegex string `json:"password_regex"`
|
||||
Name string `json:"name"`
|
||||
Environment string `json:"environment"`
|
||||
BaseURL string `json:"base_url"`
|
||||
PasswordRegex string `json:"password_regex"`
|
||||
CategoryTreeRootID uint `json:"category_tree_root_id"`
|
||||
ShopDefaultLanguage uint `json:"shop_default_language"`
|
||||
// Config config.Config `json:"config"`
|
||||
}
|
||||
|
||||
@@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
settings := SettingsResponse{
|
||||
App: AppSettings{
|
||||
Name: cfg.App.Name,
|
||||
Environment: cfg.App.Environment,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
|
||||
Name: cfg.App.Name,
|
||||
Environment: cfg.App.Environment,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
|
||||
CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID,
|
||||
ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE,
|
||||
// Config: *config.Get(),
|
||||
},
|
||||
Server: ServerSettings{
|
||||
|
||||
198
app/delivery/web/api/webdav/storage.go
Normal file
198
app/delivery/web/api/webdav/storage.go
Normal 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)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/public"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/restricted"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/api/webdav"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/web/general"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
type Server struct {
|
||||
app *fiber.App
|
||||
cfg *config.Config
|
||||
webdav fiber.Router
|
||||
api fiber.Router
|
||||
public fiber.Router
|
||||
restricted fiber.Router
|
||||
@@ -42,12 +44,23 @@ func (s *Server) Cfg() *config.Config {
|
||||
|
||||
// New creates a new server instance
|
||||
func New() *Server {
|
||||
return &Server{
|
||||
app: fiber.New(fiber.Config{
|
||||
ErrorHandler: customErrorHandler,
|
||||
}),
|
||||
cfg: config.Get(),
|
||||
}
|
||||
var s Server
|
||||
|
||||
app :=
|
||||
fiber.New(fiber.Config{
|
||||
ErrorHandler: customErrorHandler,
|
||||
BodyLimit: 50 * 1024 * 1024, // 50 MB
|
||||
StreamRequestBody: true,
|
||||
RequestMethods: []string{
|
||||
fiber.MethodGet, fiber.MethodHead, fiber.MethodPost, fiber.MethodPut,
|
||||
fiber.MethodDelete, fiber.MethodConnect, fiber.MethodOptions,
|
||||
fiber.MethodTrace, fiber.MethodPatch, "MKCOL", "PROPFIND", "PROPPATCH", "MOVE", "COPY",
|
||||
},
|
||||
})
|
||||
|
||||
s.app = app
|
||||
s.cfg = config.Get()
|
||||
return &s
|
||||
}
|
||||
|
||||
// Setup configures the server with routes and middleware
|
||||
@@ -76,6 +89,8 @@ func (s *Server) Setup() error {
|
||||
s.public = s.api.Group("/public")
|
||||
s.restricted = s.api.Group("/restricted")
|
||||
s.restricted.Use(middleware.AuthMiddleware())
|
||||
s.webdav = s.api.Group("/webdav")
|
||||
s.webdav.Use(middleware.Webdav())
|
||||
|
||||
// initialize language endpoints (general)
|
||||
api.NewLangHandler().InitLanguage(s.api, s.cfg)
|
||||
@@ -90,13 +105,15 @@ func (s *Server) Setup() error {
|
||||
menuRouting := s.public.Group("/menu")
|
||||
public.RoutingHandlerRoutes(menuRouting)
|
||||
|
||||
pCustomer := s.restricted.Group("/customer")
|
||||
restricted.CustomerHandlerRoutes(pCustomer)
|
||||
|
||||
// product translation routes (restricted)
|
||||
productTranslation := s.restricted.Group("/product-translation")
|
||||
restricted.ProductTranslationHandlerRoutes(productTranslation)
|
||||
|
||||
// listing products routes (restricted)
|
||||
listProducts := s.restricted.Group("/list-products")
|
||||
restricted.ListProductsHandlerRoutes(listProducts)
|
||||
product := s.restricted.Group("/product")
|
||||
restricted.ProductsHandlerRoutes(product)
|
||||
|
||||
// locale selector (restricted)
|
||||
// this is basically for changing user's selected language and country
|
||||
@@ -115,6 +132,18 @@ func (s *Server) Setup() error {
|
||||
carts := s.restricted.Group("/carts")
|
||||
restricted.CartsHandlerRoutes(carts)
|
||||
|
||||
// addresses (restricted)
|
||||
addresses := s.restricted.Group("/addresses")
|
||||
restricted.AddressesHandlerRoutes(addresses)
|
||||
|
||||
// storage (uses various authorization means)
|
||||
restrictedStorage := s.restricted.Group("/storage")
|
||||
webdavStorage := s.webdav.Group("/storage")
|
||||
restricted.StorageHandlerRoutes(restrictedStorage)
|
||||
webdav.StorageHandlerRoutes(webdavStorage)
|
||||
|
||||
restricted.CurrencyHandlerRoutes(s.restricted)
|
||||
|
||||
s.api.All("*", func(c fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
})
|
||||
|
||||
79
app/model/address.go
Normal file
79
app/model/address.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package model
|
||||
|
||||
type Address struct {
|
||||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
|
||||
AddressInfo string `gorm:"column:address_info;not null" json:"address_info"`
|
||||
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
|
||||
}
|
||||
|
||||
func (Address) TableName() string {
|
||||
return "b2b_addresses"
|
||||
}
|
||||
|
||||
type AddressUnparsed struct {
|
||||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
|
||||
AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"`
|
||||
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
|
||||
}
|
||||
|
||||
type AddressField interface {
|
||||
}
|
||||
|
||||
// Address template in Poland
|
||||
type AddressPL struct {
|
||||
PostalCode string `json:"postal_code"` // format: 00-000
|
||||
City string `json:"city"` // e.g. Kraków
|
||||
Voivodeship string `json:"voivodeship"` // e.g. małopolskie (optional but useful)
|
||||
|
||||
Street string `json:"street"` // e.g. Marszałkowska
|
||||
BuildingNo string `json:"building_no"` // e.g. 10, 221B, 12A
|
||||
ApartmentNo string `json:"apartment_no"` // e.g. 5, 12B
|
||||
|
||||
AddressLine2 string `json:"address_line2"` // optional extra info
|
||||
|
||||
Recipient string `json:"recipient"` // name/company
|
||||
}
|
||||
|
||||
// Address template in Great Britain
|
||||
type AddressGB struct {
|
||||
PostalCode string `json:"postal_code"` // e.g. SW1A 1AA
|
||||
PostTown string `json:"post_town"` // e.g. London
|
||||
County string `json:"county"` // optional
|
||||
|
||||
Thoroughfare string `json:"thoroughfare"` // street name, e.g. Baker Street
|
||||
BuildingNo string `json:"building_no"` // e.g. 221B
|
||||
BuildingName string `json:"building_name"` // e.g. Flatiron House
|
||||
SubBuilding string `json:"sub_building"` // e.g. Flat 5, Apt 2
|
||||
|
||||
AddressLine2 string `json:"address_line2"`
|
||||
Recipient string `json:"recipient"`
|
||||
}
|
||||
|
||||
// Address template in Czech Republic
|
||||
type AddressCZ struct {
|
||||
PostalCode string `json:"postal_code"` // usually 110 00 or 11000
|
||||
City string `json:"city"` // e.g. Praha
|
||||
Region string `json:"region"`
|
||||
|
||||
Street string `json:"street"` // may be omitted in some village-style addresses
|
||||
HouseNumber string `json:"house_number"` // descriptive / conscription no.
|
||||
OrientationNumber string `json:"orientation_number"` // optional, often after slash
|
||||
|
||||
AddressLine2 string `json:"address_line2"`
|
||||
Recipient string `json:"recipient"`
|
||||
}
|
||||
|
||||
// Address template in Germany
|
||||
type AddressDE struct {
|
||||
PostalCode string `json:"postal_code"` // e.g. 10115
|
||||
City string `json:"city"` // e.g. Berlin
|
||||
State string `json:"state"` // Bundesland, optional
|
||||
|
||||
Street string `json:"street"` // e.g. Unter den Linden
|
||||
HouseNumber string `json:"house_number"` // e.g. 77, 12a
|
||||
|
||||
AddressLine2 string `json:"address_line2"` // extra details
|
||||
Recipient string `json:"recipient"`
|
||||
}
|
||||
33
app/model/category.go
Normal file
33
app/model/category.go
Normal 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"`
|
||||
}
|
||||
@@ -1,31 +1,17 @@
|
||||
package model
|
||||
|
||||
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||
|
||||
// Represents a country together with its associated currency
|
||||
type Country struct {
|
||||
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
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"`
|
||||
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
||||
// 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"`
|
||||
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
||||
|
||||
PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"`
|
||||
PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"`
|
||||
}
|
||||
|
||||
func (Country) TableName() string {
|
||||
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"`
|
||||
}
|
||||
|
||||
25
app/model/currency.go
Normal file
25
app/model/currency.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Currency struct {
|
||||
ID int `json:"id"`
|
||||
PsIDCurrency uint `json:"ps_id_currency"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ConversionRate *float64 `json:"conversion_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (Currency) TableName() string {
|
||||
return "b2b_currencies"
|
||||
}
|
||||
|
||||
type CurrencyRate struct {
|
||||
B2bIdCurrency uint `json:"b2b_id_currency"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ConversionRate *float64 `json:"conversion_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (CurrencyRate) TableName() string {
|
||||
return "b2b_currency_rates"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,8 @@ type Customer struct {
|
||||
Password string `gorm:"size:255" json:"-"` // Hashed password, not exposed in JSON
|
||||
FirstName string `gorm:"size:100" json:"first_name"`
|
||||
LastName string `gorm:"size:100" json:"last_name"`
|
||||
Role CustomerRole `gorm:"type:varchar(20);default:'user'" json:"role"`
|
||||
RoleID uint `gorm:"column:role_id;not null;default:1" json:"-"`
|
||||
Role *Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
|
||||
Provider AuthProvider `gorm:"type:varchar(20);default:'local'" json:"provider"`
|
||||
ProviderID string `gorm:"size:255" json:"provider_id,omitempty"` // ID from OAuth provider
|
||||
AvatarURL string `gorm:"size:500" json:"avatar_url,omitempty"`
|
||||
@@ -23,6 +25,8 @@ type Customer struct {
|
||||
EmailVerificationExpires *time.Time `json:"-"`
|
||||
PasswordResetToken string `gorm:"size:255" json:"-"`
|
||||
PasswordResetExpires *time.Time `json:"-"`
|
||||
WebdavToken string `gorm:"size:255" json:"-"`
|
||||
WebdavExpires *time.Time `json:"-"`
|
||||
LastPasswordResetRequest *time.Time `json:"-"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
|
||||
@@ -32,13 +36,14 @@ type Customer struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// CustomerRole represents the role of a user
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
RoleUser CustomerRole = "user"
|
||||
RoleAdmin CustomerRole = "admin"
|
||||
)
|
||||
func (u *Customer) HasPermission(permission perms.Permission) bool {
|
||||
for _, p := range u.Role.Permissions {
|
||||
if p.Name == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthProvider represents the authentication provider
|
||||
type AuthProvider string
|
||||
@@ -53,16 +58,6 @@ func (Customer) TableName() string {
|
||||
return "b2b_customers"
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user has admin role
|
||||
func (u *Customer) IsAdmin() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// CanManageUsers checks if the user can manage other users
|
||||
func (u *Customer) CanManageUsers() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// FullName returns the user's full name
|
||||
func (u *Customer) FullName() string {
|
||||
if u.FirstName == "" && u.LastName == "" {
|
||||
@@ -73,31 +68,65 @@ func (u *Customer) FullName() string {
|
||||
|
||||
// UserSession represents a user session for JWT claims
|
||||
type UserSession struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role CustomerRole `json:"role"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
RoleID uint `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Permissions []perms.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
func (us *UserSession) HasPermission(permission perms.Permission) bool {
|
||||
for _, p := range us.Permissions {
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
func (u *Customer) ToSession() *UserSession {
|
||||
|
||||
return &UserSession{
|
||||
UserID: u.ID,
|
||||
Email: u.Email,
|
||||
Role: u.Role,
|
||||
LangID: u.LangID,
|
||||
CountryID: u.CountryID,
|
||||
IsActive: u.IsActive,
|
||||
UserID: u.ID,
|
||||
Email: u.Email,
|
||||
RoleID: u.Role.ID,
|
||||
RoleName: u.Role.Name,
|
||||
Permissions: BuildPermissionSlice(u),
|
||||
LangID: u.LangID,
|
||||
CountryID: u.CountryID,
|
||||
IsActive: u.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildPermissionSlice(user *Customer) []perms.Permission {
|
||||
var perms []perms.Permission
|
||||
|
||||
for _, p := range user.Role.Permissions {
|
||||
perms = append(perms, p.Name)
|
||||
}
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
// LoginRequest represents the login form data
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" form:"email"`
|
||||
Password string `json:"password" form:"password"`
|
||||
LangID *uint `json:"lang_id" form:"lang_id"`
|
||||
}
|
||||
|
||||
// RegisterRequest represents the initial registration form data
|
||||
@@ -144,3 +173,10 @@ type RefreshToken struct {
|
||||
func (RefreshToken) TableName() string {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ func (*PsGroupReduction) TableName() string {
|
||||
|
||||
var PsGroupReductionCols = struct {
|
||||
IDGroupReduction gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDCategory gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDCategory gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
}{
|
||||
IDGroupReduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group_reduction"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"),
|
||||
IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"),
|
||||
Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"),
|
||||
IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"),
|
||||
Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"),
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ func (*PsProductAttributeCombination) TableName() string {
|
||||
}
|
||||
|
||||
var PsProductAttributeCombinationCols = struct {
|
||||
IDAttribute gormcol.Field
|
||||
IDAttribute 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"),
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ func (*PsProductGroupReductionCache) TableName() string {
|
||||
|
||||
var PsProductGroupReductionCacheCols = struct {
|
||||
IDProduct gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
}{
|
||||
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"),
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func (*PsPshowPshowproducttabsHook) TableName() string {
|
||||
}
|
||||
|
||||
var PsPshowPshowproducttabsHookCols = struct {
|
||||
IDHook gormcol.Field
|
||||
HookName gormcol.Field
|
||||
IDHook gormcol.Field
|
||||
HookName gormcol.Field
|
||||
PrestaIDHook gormcol.Field
|
||||
}{
|
||||
IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"),
|
||||
HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"),
|
||||
IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"),
|
||||
HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"),
|
||||
PrestaIDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "presta_id_hook"),
|
||||
}
|
||||
|
||||
6
app/model/entry.go
Normal file
6
app/model/entry.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type EntryInList struct {
|
||||
Name string
|
||||
IsFolder bool
|
||||
}
|
||||
18
app/model/model.go
Normal file
18
app/model/model.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement" swaggerignore:"true" json:"id,omitempty" hidden:"true"`
|
||||
CreatedAt time.Time `gorm:"not null;autoCreateTime" swaggerignore:"true" json:"-"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" swaggerignore:"true" json:"-"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true" json:"-"`
|
||||
}
|
||||
|
||||
// Makes all objects embedding db.Model implementators of ModelWithID interface
|
||||
func (m Model) ModelWithID() {
|
||||
}
|
||||
12
app/model/permission.go
Normal file
12
app/model/permission.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
|
||||
type Permission struct {
|
||||
ID uint
|
||||
Name perms.Permission
|
||||
}
|
||||
|
||||
func (Permission) TableName() string {
|
||||
return "b2b_permissions"
|
||||
}
|
||||
@@ -84,28 +84,4 @@ type ProductFilters struct {
|
||||
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
|
||||
|
||||
@@ -19,6 +19,9 @@ type ProductDescription struct {
|
||||
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"`
|
||||
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 {
|
||||
|
||||
19
app/model/role.go
Normal file
19
app/model/role.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
type Role struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:64" json:"name"`
|
||||
Permissions []Permission `gorm:"many2many:b2b_role_permissions;" json:"permissions"`
|
||||
}
|
||||
|
||||
func (Role) TableName() string {
|
||||
return "b2b_roles"
|
||||
}
|
||||
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
RoleUser CustomerRole = "user"
|
||||
RoleAdmin CustomerRole = "admin"
|
||||
RoleSuperAdmin CustomerRole = "super_admin"
|
||||
)
|
||||
91
app/repos/addressesRepo/addressesRepo.go
Normal file
91
app/repos/addressesRepo/addressesRepo.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package addressesRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
)
|
||||
|
||||
type UIAddressesRepo interface {
|
||||
UserHasAddress(user_id uint, address_id uint) (uint, error)
|
||||
UserAddressesAmt(user_id uint) (uint, error)
|
||||
AddNewAddress(user_id uint, address_info string, country_id uint) error
|
||||
UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error
|
||||
RetrieveAddresses(user_id uint) (*[]model.Address, error)
|
||||
DeleteAddress(user_id uint, address_id uint) error
|
||||
}
|
||||
|
||||
type AddressesRepo struct{}
|
||||
|
||||
func New() UIAddressesRepo {
|
||||
return &AddressesRepo{}
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) UserHasAddress(user_id uint, address_id uint) (uint, error) {
|
||||
var amt uint
|
||||
|
||||
err := db.DB.
|
||||
Table("b2b_addresses").
|
||||
Select("COUNT(*) AS amt").
|
||||
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
|
||||
Scan(&amt).
|
||||
Error
|
||||
|
||||
return amt, err
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) {
|
||||
var amt uint
|
||||
|
||||
err := db.DB.
|
||||
Table("b2b_addresses").
|
||||
Select("COUNT(*) AS amt").
|
||||
Where("b2b_customer_id = ?", user_id).
|
||||
Scan(&amt).
|
||||
Error
|
||||
|
||||
return amt, err
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error {
|
||||
address := model.Address{
|
||||
CustomerID: user_id,
|
||||
AddressInfo: address_info,
|
||||
CountryID: country_id,
|
||||
}
|
||||
|
||||
return db.DB.
|
||||
Create(&address).
|
||||
Error
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error {
|
||||
address := model.Address{
|
||||
ID: address_id,
|
||||
CustomerID: user_id,
|
||||
AddressInfo: address_info,
|
||||
CountryID: country_id,
|
||||
}
|
||||
|
||||
return db.DB.
|
||||
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
|
||||
Updates(&address).
|
||||
Error
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) RetrieveAddresses(user_id uint) (*[]model.Address, error) {
|
||||
var addresses []model.Address
|
||||
|
||||
err := db.DB.
|
||||
Where("b2b_customer_id = ?", user_id).
|
||||
Find(&addresses).
|
||||
Error
|
||||
|
||||
return &addresses, err
|
||||
}
|
||||
|
||||
func (repo *AddressesRepo) DeleteAddress(user_id uint, address_id uint) error {
|
||||
return db.DB.
|
||||
Where("id = ? AND b2b_customer_id = ?", address_id, user_id).
|
||||
Delete(&model.Address{}).
|
||||
Error
|
||||
}
|
||||
@@ -37,12 +37,11 @@ func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory,
|
||||
ps_category_lang.link_rewrite AS link_rewrite,
|
||||
ps_lang.iso_code AS iso_code
|
||||
`).
|
||||
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ? AND ??.id_lang = ?`,
|
||||
categoryLangTbl, categoryLangTbl, categoryTbl, categoryLangTbl, constdata.SHOP_ID, categoryLangTbl, idLang).
|
||||
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ?`,
|
||||
categoryShopTbl, categoryShopTbl, categoryTbl, categoryShopTbl, constdata.SHOP_ID).
|
||||
Joins(`JOIN ? ON ??.id_lang = ??.id_lang`,
|
||||
langTbl, langTbl, categoryLangTbl).
|
||||
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
|
||||
constdata.SHOP_ID, idLang).
|
||||
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
|
||||
constdata.SHOP_ID).
|
||||
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
|
||||
Scan(&allCategories).Error
|
||||
|
||||
return allCategories, err
|
||||
|
||||
53
app/repos/currencyRepo/currencyRepo.go
Normal file
53
app/repos/currencyRepo/currencyRepo.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package currencyRepo
|
||||
|
||||
import (
|
||||
"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/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type UICurrencyRepo interface {
|
||||
CreateConversionRate(currencyRate *model.CurrencyRate) error
|
||||
Get(id uint) (*model.Currency, error)
|
||||
}
|
||||
|
||||
type CurrencyRepo struct{}
|
||||
|
||||
func New() UICurrencyRepo {
|
||||
return &CurrencyRepo{}
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error {
|
||||
return db.DB.Create(currencyRate).Error
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
|
||||
var currency model.Currency
|
||||
|
||||
err := db.DB.Table("b2b_currencies c").
|
||||
Select("c.*, r.conversion_rate").
|
||||
Joins(`
|
||||
LEFT JOIN b2b_currency_rates r
|
||||
ON r.b2b_id_currency = c.id
|
||||
AND r.created_at = (
|
||||
SELECT MAX(created_at)
|
||||
FROM b2b_currency_rates
|
||||
WHERE b2b_id_currency = c.id
|
||||
)
|
||||
`).
|
||||
Where("c.id = ?", id).
|
||||
Scan(¤cy).Error
|
||||
|
||||
return ¤cy, err
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
|
||||
|
||||
found, err := find.Paginate[model.Currency](langId, p, db.DB.
|
||||
Model(&model.Currency{}).
|
||||
Scopes(filt.All()...),
|
||||
)
|
||||
|
||||
return &found, err
|
||||
}
|
||||
197
app/repos/customerRepo/customerRepo.go
Normal file
197
app/repos/customerRepo/customerRepo.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package customerRepo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type UICustomerRepo interface {
|
||||
Get(id uint) (*model.Customer, error)
|
||||
GetByEmail(email string) (*model.Customer, error)
|
||||
GetByExternalProviderId(provider model.AuthProvider, id string) (*model.Customer, error)
|
||||
Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error)
|
||||
Save(customer *model.Customer) error
|
||||
Create(customer *model.Customer) error
|
||||
}
|
||||
|
||||
type CustomerRepo struct{}
|
||||
|
||||
func New() UICustomerRepo {
|
||||
return &CustomerRepo{}
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) {
|
||||
var customer model.Customer
|
||||
|
||||
err := db.DB.
|
||||
Preload("Role.Permissions").
|
||||
First(&customer, id).
|
||||
Error
|
||||
|
||||
return &customer, err
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) GetByEmail(email string) (*model.Customer, error) {
|
||||
var customer model.Customer
|
||||
|
||||
err := db.DB.
|
||||
Preload("Role.Permissions").
|
||||
Where("email = ?", email).
|
||||
First(&customer).
|
||||
Error
|
||||
|
||||
return &customer, err
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) GetByExternalProviderId(provider model.AuthProvider, id string) (*model.Customer, error) {
|
||||
var customer model.Customer
|
||||
|
||||
err := db.DB.
|
||||
Preload("Role.Permissions").
|
||||
Where("provider = ? AND provider_id = ?", provider, id).
|
||||
First(&customer).
|
||||
Error
|
||||
|
||||
return &customer, err
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) {
|
||||
|
||||
query := db.DB.
|
||||
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
|
||||
`)
|
||||
|
||||
if search != "" {
|
||||
words := strings.Fields(search)
|
||||
if len(words) > 5 {
|
||||
words = words[:5]
|
||||
}
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
for _, word := range words {
|
||||
|
||||
conditions = append(conditions, `
|
||||
(LOWER(first_name) LIKE ? OR
|
||||
LOWER(last_name) LIKE ? OR
|
||||
LOWER(email) LIKE ?)
|
||||
`)
|
||||
|
||||
for range 3 {
|
||||
args = append(args, "%"+strings.ToLower(word)+"%")
|
||||
}
|
||||
}
|
||||
|
||||
conditionsQuery := strings.Join(conditions, " AND ")
|
||||
|
||||
query = query.Where(conditionsQuery, args...)
|
||||
|
||||
}
|
||||
|
||||
query = query.Scopes(filt.All()...)
|
||||
|
||||
found, err := find.Paginate[model.UserInList](langId, p, query)
|
||||
|
||||
return &found, err
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) Save(customer *model.Customer) error {
|
||||
return db.DB.Save(customer).Error
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) Create(customer *model.Customer) error {
|
||||
return db.DB.Create(customer).Error
|
||||
}
|
||||
|
||||
// func (repo *CustomerRepo) Search(
|
||||
// customerId uint,
|
||||
// partnerCode string,
|
||||
// p find.Paging,
|
||||
// filt *filters.FiltersList,
|
||||
// search string,
|
||||
// ) (found find.Found[model.UserInList], err error) {
|
||||
// words := strings.Fields(search)
|
||||
// if len(words) > 5 {
|
||||
// words = words[:5]
|
||||
// }
|
||||
|
||||
// query := ctx.DB().
|
||||
// Model(&model.Customer{}).
|
||||
// Select("customer.id AS id, customer.first_name as first_name, customer.last_name as last_name, customer.phone_number AS phone_number, customer.email AS email, count(distinct investment_plan_contract.id) as iiplan_purchases, count(distinct `order`.id) as single_purchases, entity.name as entity_name").
|
||||
// Where("customer.id <> ?", customerId).
|
||||
// Where("(customer.id IN (SELECT id FROM customer WHERE partner_code IN (WITH RECURSIVE partners AS (SELECT code AS dst FROM partner WHERE code = ? UNION SELECT code FROM partner JOIN partners ON partners.dst = partner.superior_code) SELECT dst FROM partners)) OR customer.recommender_code = ?)", partnerCode, partnerCode).
|
||||
// Scopes(view.CustomerListQuery())
|
||||
|
||||
// var conditions []string
|
||||
// var args []interface{}
|
||||
// for _, word := range words {
|
||||
|
||||
// conditions = append(conditions, `
|
||||
// (LOWER(first_name) LIKE ? OR
|
||||
// LOWER(last_name) LIKE ? OR
|
||||
// phone_number LIKE ? OR
|
||||
// LOWER(email) LIKE ?)
|
||||
// `)
|
||||
|
||||
// for i := 0; i < 4; i++ {
|
||||
// args = append(args, "%"+strings.ToLower(word)+"%")
|
||||
// }
|
||||
// }
|
||||
|
||||
// finalQuery := strings.Join(conditions, " AND ")
|
||||
|
||||
// query = query.Where(finalQuery, args...).
|
||||
// Scopes(filt.All()...)
|
||||
|
||||
// found, err = find.Paginate[V](ctx, p, query)
|
||||
|
||||
// return found, errs.Recorded(span, err)
|
||||
// }
|
||||
|
||||
// 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),
|
||||
// }, nil
|
||||
// }
|
||||
@@ -1,13 +1,16 @@
|
||||
package productDescriptionRepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UIProductDescriptionRepo interface {
|
||||
@@ -28,14 +31,42 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
||||
var ProductDescription model.ProductDescription
|
||||
|
||||
err := db.Get().
|
||||
Model(dbmodel.PsProductLang{}).
|
||||
Where(&dbmodel.PsProductLang{
|
||||
IDProduct: int32(productID),
|
||||
IDShop: int32(constdata.SHOP_ID),
|
||||
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
|
||||
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)
|
||||
} else {
|
||||
ProductDescription.ExistsInDatabase = true
|
||||
}
|
||||
|
||||
return &ProductDescription, nil
|
||||
@@ -43,13 +74,14 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
||||
|
||||
// If it doesn't exist, returns an error.
|
||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error {
|
||||
record := model.ProductDescription{
|
||||
ProductID: productID,
|
||||
ShopID: constdata.SHOP_ID,
|
||||
LangID: productid_lang,
|
||||
record := dbmodel.PsProductLang{
|
||||
IDProduct: int32(productID),
|
||||
IDShop: int32(constdata.SHOP_ID),
|
||||
IDLang: int32(productid_lang),
|
||||
}
|
||||
|
||||
err := db.Get().
|
||||
Model(dbmodel.PsProductLang{}).
|
||||
Where(&dbmodel.PsProductLang{
|
||||
IDProduct: int32(productID),
|
||||
IDShop: int32(constdata.SHOP_ID),
|
||||
|
||||
@@ -1,36 +1,62 @@
|
||||
package listProductsRepo
|
||||
package productsRepo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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/model"
|
||||
"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/find"
|
||||
"git.ma-al.com/goc_marek/gormcol"
|
||||
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
||||
)
|
||||
|
||||
type UIListProductsRepo interface {
|
||||
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
type UIProductsRepo interface {
|
||||
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
|
||||
Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
}
|
||||
|
||||
type ListProductsRepo struct{}
|
||||
type ProductsRepo struct{}
|
||||
|
||||
func New() UIListProductsRepo {
|
||||
return &ListProductsRepo{}
|
||||
func New() UIProductsRepo {
|
||||
return &ProductsRepo{}
|
||||
}
|
||||
|
||||
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var listing []model.ProductInList
|
||||
func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
|
||||
var productStr string // ← Scan as string first
|
||||
|
||||
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`,
|
||||
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity).
|
||||
Scan(&productStr).
|
||||
Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optional: validate it's valid JSON
|
||||
if !json.Valid([]byte(productStr)) {
|
||||
return nil, fmt.Errorf("invalid json returned from stored procedure")
|
||||
}
|
||||
|
||||
raw := json.RawMessage(productStr)
|
||||
return &raw, nil
|
||||
}
|
||||
|
||||
func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var list []model.ProductInList
|
||||
var total int64
|
||||
|
||||
query := db.Get().
|
||||
Table("ps_product_shop ps").
|
||||
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
|
||||
Select(`
|
||||
ps.id_product AS product_id,
|
||||
pl.name AS name,
|
||||
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,
|
||||
p.reference AS reference,
|
||||
COALESCE(v.variants_number, 0) AS variants_number,
|
||||
@@ -50,7 +76,8 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
|
||||
Name: "variants",
|
||||
Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")},
|
||||
},
|
||||
}})
|
||||
}}).
|
||||
Order("ps.id_product DESC")
|
||||
|
||||
// Apply all filters
|
||||
if filt != nil {
|
||||
@@ -64,16 +91,15 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("ps.id_product DESC").
|
||||
Limit(p.Limit()).
|
||||
Offset(p.Offset()).
|
||||
Find(&listing).Error
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
return find.Found[model.ProductInList]{}, err
|
||||
}
|
||||
|
||||
return find.Found[model.ProductInList]{
|
||||
Items: listing,
|
||||
Items: list,
|
||||
Count: uint(total),
|
||||
}, nil
|
||||
}
|
||||
22
app/repos/rolesRepo/rolesRepo.go
Normal file
22
app/repos/rolesRepo/rolesRepo.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package roleRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
)
|
||||
|
||||
type UIRolesRepo interface {
|
||||
Get(id uint) (*model.Role, error)
|
||||
}
|
||||
|
||||
type RolesRepo struct{}
|
||||
|
||||
func New() UIRolesRepo {
|
||||
return &RolesRepo{}
|
||||
}
|
||||
|
||||
func (r *RolesRepo) Get(id uint) (*model.Role, error) {
|
||||
var role model.Role
|
||||
err := db.DB.First(&role, id).Error
|
||||
return &role, err
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type UIRoutesRepo interface {
|
||||
GetRoutes(langId uint) ([]model.Route, error)
|
||||
GetTopMenu(id uint) ([]model.B2BTopMenu, error)
|
||||
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
|
||||
}
|
||||
|
||||
type RoutesRepo struct{}
|
||||
@@ -26,12 +26,16 @@ func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func (p *RoutesRepo) GetTopMenu(id uint) ([]model.B2BTopMenu, error) {
|
||||
func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) {
|
||||
var menus []model.B2BTopMenu
|
||||
|
||||
err := db.Get().
|
||||
Where("active = ?", 1).
|
||||
Order("parent_id ASC, position ASC").
|
||||
err := db.
|
||||
Get().
|
||||
Model(model.B2BTopMenu{}).
|
||||
Joins("JOIN b2b_top_menu_roles tmr ON tmr.top_menu_id = b2b_top_menu.menu_id").
|
||||
Where(model.B2BTopMenu{Active: 1}).
|
||||
Where("tmr.role_id = ?", roleId).
|
||||
Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC").
|
||||
Find(&menus).Error
|
||||
|
||||
return menus, err
|
||||
|
||||
@@ -32,12 +32,12 @@ func New() UISearchRepo {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if r.cfg.MailiSearch.ApiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey))
|
||||
if r.cfg.MeiliSearch.ApiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey))
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
178
app/repos/storageRepo/storageRepo.go
Normal file
178
app/repos/storageRepo/storageRepo.go
Normal 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
|
||||
}
|
||||
152
app/service/addressesService/addressesService.go
Normal file
152
app/service/addressesService/addressesService.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package addressesService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/addressesRepo"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
)
|
||||
|
||||
type AddressesService struct {
|
||||
repo addressesRepo.UIAddressesRepo
|
||||
}
|
||||
|
||||
func New() *AddressesService {
|
||||
return &AddressesService{
|
||||
repo: addressesRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) {
|
||||
switch country_id {
|
||||
|
||||
case 1: // Poland
|
||||
return model.AddressPL{}, nil
|
||||
|
||||
case 2: // Great Britain
|
||||
return model.AddressGB{}, nil
|
||||
|
||||
case 3: // Czech Republic
|
||||
return model.AddressCZ{}, nil
|
||||
|
||||
case 4: // Germany
|
||||
return model.AddressDE{}, nil
|
||||
|
||||
default:
|
||||
return nil, responseErrors.ErrInvalidCountryID
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AddressesService) AddNewAddress(user_id uint, address_info string, country_id uint) error {
|
||||
amt, err := s.repo.UserAddressesAmt(user_id)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if amt >= constdata.MAX_AMOUNT_OF_ADDRESSES_PER_USER {
|
||||
return responseErrors.ErrMaxAmtOfAddressesReached
|
||||
}
|
||||
|
||||
_, err = s.validateAddressJson(address_info, country_id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.AddNewAddress(user_id, address_info, country_id)
|
||||
}
|
||||
|
||||
// country_id = 0 means that country_id remains unchanged
|
||||
func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_info string, country_id uint) error {
|
||||
amt, err := s.repo.UserHasAddress(user_id, address_id)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if amt != 1 {
|
||||
return responseErrors.ErrUserHasNoSuchAddress
|
||||
}
|
||||
|
||||
_, err = s.validateAddressJson(address_info, country_id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.UpdateAddress(user_id, address_id, address_info, country_id)
|
||||
}
|
||||
|
||||
func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) {
|
||||
parsed_addresses, err := s.repo.RetrieveAddresses(user_id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unparsed_addresses []model.AddressUnparsed
|
||||
|
||||
for i := 0; i < len(*parsed_addresses); i++ {
|
||||
var next_address model.AddressUnparsed
|
||||
next_address.ID = (*parsed_addresses)[i].ID
|
||||
next_address.CustomerID = (*parsed_addresses)[i].CustomerID
|
||||
next_address.CountryID = (*parsed_addresses)[i].CountryID
|
||||
|
||||
next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID)
|
||||
// log such errors
|
||||
if err != nil {
|
||||
fmt.Printf("err: %v\n", err)
|
||||
}
|
||||
|
||||
unparsed_addresses = append(unparsed_addresses, next_address)
|
||||
}
|
||||
|
||||
return &unparsed_addresses, nil
|
||||
}
|
||||
|
||||
func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
|
||||
amt, err := s.repo.UserHasAddress(user_id, address_id)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if amt != 1 {
|
||||
return responseErrors.ErrUserHasNoSuchAddress
|
||||
}
|
||||
|
||||
return s.repo.DeleteAddress(user_id, address_id)
|
||||
}
|
||||
|
||||
// validateAddressJson makes sure that the info string represents a valid json of address in given country
|
||||
func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(info))
|
||||
dec.DisallowUnknownFields()
|
||||
|
||||
switch country_id {
|
||||
|
||||
case 1: // Poland
|
||||
var address model.AddressPL
|
||||
if err := dec.Decode(&address); err != nil {
|
||||
return address, responseErrors.ErrInvalidAddressJSON
|
||||
}
|
||||
return address, nil
|
||||
|
||||
case 2: // Great Britain
|
||||
var address model.AddressGB
|
||||
if err := dec.Decode(&address); err != nil {
|
||||
return address, responseErrors.ErrInvalidAddressJSON
|
||||
}
|
||||
return address, nil
|
||||
|
||||
case 3: // Czech Republic
|
||||
var address model.AddressCZ
|
||||
if err := dec.Decode(&address); err != nil {
|
||||
return address, responseErrors.ErrInvalidAddressJSON
|
||||
}
|
||||
return address, nil
|
||||
|
||||
case 4: // Germany
|
||||
var address model.AddressDE
|
||||
if err := dec.Decode(&address); err != nil {
|
||||
return address, responseErrors.ErrInvalidAddressJSON
|
||||
}
|
||||
return address, nil
|
||||
|
||||
default:
|
||||
return nil, responseErrors.ErrInvalidCountryID
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,18 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/customerRepo"
|
||||
roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
|
||||
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"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,29 +25,33 @@ import (
|
||||
|
||||
// JWTClaims represents the JWT claims
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role model.CustomerRole `json:"customer_role"`
|
||||
CartsIDs []uint `json:"carts_ids"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"customer_role"`
|
||||
CartsIDs []uint `json:"carts_ids"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AuthService handles authentication operations
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
config *config.AuthConfig
|
||||
email *emailService.EmailService
|
||||
db *gorm.DB
|
||||
config *config.AuthConfig
|
||||
email *emailService.EmailService
|
||||
customerRepo customerRepo.UICustomerRepo
|
||||
roleRepo roleRepo.UIRolesRepo
|
||||
}
|
||||
|
||||
// NewAuthService creates a new AuthService instance
|
||||
func NewAuthService() *AuthService {
|
||||
svc := &AuthService{
|
||||
db: db.Get(),
|
||||
config: &config.Get().Auth,
|
||||
email: emailService.NewEmailService(),
|
||||
db: db.Get(),
|
||||
config: &config.Get().Auth,
|
||||
email: emailService.NewEmailService(),
|
||||
customerRepo: customerRepo.New(),
|
||||
roleRepo: roleRepo.New(),
|
||||
}
|
||||
// Auto-migrate the refresh_tokens table
|
||||
if svc.db != nil {
|
||||
@@ -64,7 +65,7 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
|
||||
var user model.Customer
|
||||
|
||||
// Find user by email
|
||||
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", responseErrors.ErrInvalidCredentials
|
||||
}
|
||||
@@ -88,6 +89,15 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
|
||||
// Update last login time
|
||||
now := time.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)
|
||||
|
||||
// Generate access token (JWT)
|
||||
@@ -149,7 +159,6 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
||||
Password: string(hashedPassword),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: model.RoleUser,
|
||||
Provider: model.ProviderLocal,
|
||||
IsActive: false,
|
||||
EmailVerified: false,
|
||||
@@ -167,7 +176,7 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
||||
baseURL := config.Get().App.BaseURL
|
||||
lang, err := s.GetLangISOCode(req.LangID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadLangID
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
|
||||
@@ -276,7 +285,7 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
|
||||
baseURL := config.Get().App.BaseURL
|
||||
lang, err := s.GetLangISOCode(user.LangID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadLangID
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
|
||||
@@ -427,7 +436,7 @@ func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
|
||||
// GetUserByID retrieves a user by ID
|
||||
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
|
||||
var user model.Customer
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
if err := s.db.Preload("Role.Permissions").First(&user, userID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, responseErrors.ErrUserNotFound
|
||||
}
|
||||
@@ -448,6 +457,19 @@ func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUserByWebdavToken(rawToken string) (*model.Customer, error) {
|
||||
tokenHash := hashToken(rawToken)
|
||||
|
||||
var user model.Customer
|
||||
if err := s.db.Where("webdav_token = ?", tokenHash).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, responseErrors.ErrUserNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// createRefreshToken generates a random opaque token, stores its hash in the DB, and returns the raw token.
|
||||
func (s *AuthService) createRefreshToken(userID uint) (string, error) {
|
||||
// Generate 32 random bytes → 64-char hex string
|
||||
@@ -482,7 +504,7 @@ func hashToken(raw string) string {
|
||||
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
|
||||
_, err := s.GetLangISOCode(user.LangID)
|
||||
if err != nil {
|
||||
return "", responseErrors.ErrBadLangID
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = s.CheckIfCountryExists(user.CountryID)
|
||||
@@ -494,7 +516,7 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Username: user.Email,
|
||||
Role: user.Role,
|
||||
Role: user.Role.Name,
|
||||
CartsIDs: []uint{},
|
||||
LangID: user.LangID,
|
||||
CountryID: user.CountryID,
|
||||
@@ -508,97 +530,19 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
|
||||
return token.SignedString([]byte(s.config.JWTSecret))
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateJWTToken(c fiber.Ctx) error {
|
||||
// Get user ID from JWT claims in context (set by auth middleware)
|
||||
// claims, ok := c.Locals("jwt_claims").(*JWTClaims)
|
||||
// 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)
|
||||
func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
|
||||
// Update choice and get new access token using AuthService
|
||||
new_access_token, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Save the updated user
|
||||
if err := s.db.Save(&user).Error; err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return "", fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
// Set the new JWT cookie
|
||||
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)))
|
||||
return new_access_token, nil
|
||||
}
|
||||
|
||||
// generateVerificationToken generates a random verification token
|
||||
@@ -623,14 +567,20 @@ func validatePassword(password string) error {
|
||||
|
||||
func (s *AuthService) GetLangISOCode(langID uint) (string, error) {
|
||||
var lang string
|
||||
var err error
|
||||
|
||||
if langID == 0 { // retrieve the default lang
|
||||
err := db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
return lang, err
|
||||
err = db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
} else {
|
||||
err := db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
return lang, err
|
||||
err = db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return lang, err
|
||||
} else if lang == "" {
|
||||
return lang, responseErrors.ErrBadLangID
|
||||
}
|
||||
return lang, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) CheckIfCountryExists(countryID uint) error {
|
||||
|
||||
@@ -108,26 +108,32 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
|
||||
// findOrCreateGoogleUser finds an existing user by Google provider ID or email,
|
||||
// or creates a new one.
|
||||
func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.Customer, error) {
|
||||
var user model.Customer
|
||||
var user *model.Customer
|
||||
|
||||
// Try to find by provider + provider_id
|
||||
err := s.db.Where("provider = ? AND provider_id = ?", model.ProviderGoogle, info.ID).First(&user).Error
|
||||
user, err := s.customerRepo.GetByExternalProviderId(model.ProviderGoogle, info.ID)
|
||||
if err == nil {
|
||||
// Update avatar in case it changed
|
||||
user.AvatarURL = info.Picture
|
||||
s.db.Save(&user)
|
||||
return &user, nil
|
||||
err = s.customerRepo.Save(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Try to find by email (user may have registered locally before)
|
||||
err = s.db.Where("email = ?", info.Email).First(&user).Error
|
||||
user, err = s.customerRepo.GetByEmail(info.Email)
|
||||
if err == nil {
|
||||
// Link Google provider to existing account
|
||||
user.Provider = model.ProviderGoogle
|
||||
user.ProviderID = info.ID
|
||||
user.AvatarURL = info.Picture
|
||||
user.IsActive = true
|
||||
s.db.Save(&user)
|
||||
err = s.customerRepo.Save(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If email has not been verified yet, send email to admin.
|
||||
if !user.EmailVerified {
|
||||
@@ -139,7 +145,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
||||
}
|
||||
user.EmailVerified = true
|
||||
|
||||
return &user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Create new user
|
||||
@@ -148,15 +154,16 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
||||
FirstName: info.GivenName,
|
||||
LastName: info.FamilyName,
|
||||
Provider: model.ProviderGoogle,
|
||||
RoleID: 1, // user
|
||||
ProviderID: info.ID,
|
||||
AvatarURL: info.Picture,
|
||||
Role: model.RoleUser,
|
||||
IsActive: 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.customerRepo.Create(&newUser); err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
@@ -169,6 +176,13 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
||||
}
|
||||
}
|
||||
|
||||
var role *model.Role
|
||||
role, err = s.roleRepo.Get(newUser.RoleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser.Role = role
|
||||
|
||||
return &newUser, nil
|
||||
}
|
||||
|
||||
|
||||
25
app/service/currencyService/currencyService.go
Normal file
25
app/service/currencyService/currencyService.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package currencyService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo"
|
||||
)
|
||||
|
||||
type CurrencyService struct {
|
||||
repo currencyRepo.UICurrencyRepo
|
||||
}
|
||||
|
||||
func (s *CurrencyService) GetCurrency(id uint) (*model.Currency, error) {
|
||||
return s.repo.Get(id)
|
||||
}
|
||||
|
||||
func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error {
|
||||
return s.repo.CreateConversionRate(currency)
|
||||
}
|
||||
|
||||
func New() *CurrencyService {
|
||||
repo := currencyRepo.New()
|
||||
return &CurrencyService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
26
app/service/customerService/customerService.go
Normal file
26
app/service/customerService/customerService.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package customerService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/customerRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type CustomerService struct {
|
||||
repo customerRepo.UICustomerRepo
|
||||
}
|
||||
|
||||
func New() *CustomerService {
|
||||
return &CustomerService{
|
||||
repo: customerRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CustomerService) GetById(id uint) (*model.Customer, error) {
|
||||
return s.repo.Get(id)
|
||||
}
|
||||
|
||||
func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) {
|
||||
return s.repo.Find(langId, p, filt, search)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"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/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/view"
|
||||
)
|
||||
@@ -133,6 +134,6 @@ func (s *EmailService) passwordResetEmailTemplate(name, resetURL string, langID
|
||||
// newUserAdminNotificationTemplate returns the HTML template for admin notification
|
||||
func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, baseURL string) string {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -27,8 +27,8 @@ type MeiliService struct {
|
||||
func New() *MeiliService {
|
||||
|
||||
client := meilisearch.New(
|
||||
config.Get().MailiSearch.ServerURL,
|
||||
meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey),
|
||||
config.Get().MeiliSearch.ServerURL,
|
||||
meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey),
|
||||
)
|
||||
|
||||
return &MeiliService{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package menuService
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"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)
|
||||
if err != nil {
|
||||
return &model.Category{}, err
|
||||
@@ -31,7 +32,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
root_index := 0
|
||||
root_found := false
|
||||
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_found = true
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
@@ -58,19 +60,32 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
if (*all_categories)[index].Visited {
|
||||
return node, false
|
||||
}
|
||||
(*all_categories)[index].Visited = true
|
||||
|
||||
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) {
|
||||
@@ -83,7 +98,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
|
||||
normal.CategoryID = scanned.CategoryID
|
||||
normal.Label = scanned.Name
|
||||
// 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{}
|
||||
return normal
|
||||
}
|
||||
@@ -98,8 +113,71 @@ 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) Less(i, j int) bool { return a[i].Position < a[j].Position }
|
||||
|
||||
func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) {
|
||||
items, err := s.routesRepo.GetTopMenu(id)
|
||||
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(languageId uint, roleId uint) ([]*model.B2BTopMenu, error) {
|
||||
items, err := s.routesRepo.GetTopMenu(languageId, roleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
34
app/service/productService/productService.go
Normal file
34
app/service/productService/productService.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package productService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type ProductService struct {
|
||||
productsRepo productsRepo.UIProductsRepo
|
||||
}
|
||||
|
||||
func New() *ProductService {
|
||||
return &ProductService{
|
||||
productsRepo: productsRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
|
||||
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
|
||||
if err != nil {
|
||||
return products, err
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
|
||||
func (s *ProductService) Find(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
return s.productsRepo.Find(id_lang, p, filters)
|
||||
}
|
||||
@@ -89,13 +89,24 @@ func (s *ProductTranslationService) GetProductDescription(userID uint, productID
|
||||
// Updates relevant fields with the "updates" map
|
||||
func (s *ProductTranslationService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error {
|
||||
// only some fields can be affected
|
||||
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
||||
allowedFields := []string{"description", "description_short", "link_rewrite", "meta_description", "meta_keywords", "meta_title", "name",
|
||||
"available_now", "available_later", "delivery_in_stock", "delivery_out_stock", "usage"}
|
||||
for key := range updates {
|
||||
if !slices.Contains(allowedFields, key) {
|
||||
return responseErrors.ErrBadField
|
||||
}
|
||||
}
|
||||
|
||||
if text, exists := updates["link_rewrite"]; exists {
|
||||
// sanitize and check that link_rewrite is a valid url slug
|
||||
sanitized := SanitizeSlug(text)
|
||||
if !IsValidSlug(sanitized) {
|
||||
return responseErrors.ErrInvalidURLSlug
|
||||
}
|
||||
|
||||
updates["link_rewrite"] = sanitized
|
||||
}
|
||||
|
||||
// check that fields description, description_short and usage, if they exist, have a valid html format
|
||||
mustBeHTML := []string{"description", "description_short", "usage"}
|
||||
for i := 0; i < len(mustBeHTML); i++ {
|
||||
@@ -136,20 +147,28 @@ func (s *ProductTranslationService) TranslateProductDescription(userID uint, pro
|
||||
|
||||
fields := []*string{&productDescription.Description,
|
||||
&productDescription.DescriptionShort,
|
||||
&productDescription.LinkRewrite,
|
||||
&productDescription.MetaDescription,
|
||||
&productDescription.MetaKeywords,
|
||||
&productDescription.MetaTitle,
|
||||
&productDescription.Name,
|
||||
&productDescription.AvailableNow,
|
||||
&productDescription.AvailableLater,
|
||||
&productDescription.DeliveryInStock,
|
||||
&productDescription.DeliveryOutStock,
|
||||
&productDescription.Usage,
|
||||
}
|
||||
keys := []string{"translation_of_product_description",
|
||||
"translation_of_product_short_description",
|
||||
"translation_of_product_url_link",
|
||||
"translation_of_product_meta_description",
|
||||
"translation_of_product_meta_keywords",
|
||||
"translation_of_product_meta_title",
|
||||
"translation_of_product_name",
|
||||
"translation_of_product_available_now",
|
||||
"translation_of_product_available_later",
|
||||
"translation_of_product_available_now_message",
|
||||
"translation_of_product_available_later_message",
|
||||
"translation_of_product_delivery_in_stock_message",
|
||||
"translation_of_product_delivery_out_stock_message",
|
||||
"translation_of_product_usage",
|
||||
}
|
||||
|
||||
|
||||
69
app/service/productTranslationService/sanitizeURLSlug.go
Normal file
69
app/service/productTranslationService/sanitizeURLSlug.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package productTranslationService
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"github.com/dlclark/regexp2"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
func IsValidSlug(s string) bool {
|
||||
var slug_regex2 = regexp2.MustCompile(constdata.SLUG_REGEX, regexp2.None)
|
||||
|
||||
ok, _ := slug_regex2.MatchString(s)
|
||||
return ok
|
||||
}
|
||||
|
||||
func SanitizeSlug(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
|
||||
// First apply explicit transliteration for language-specific letters.
|
||||
s = transliterateWithTable(s)
|
||||
|
||||
// Then normalize and strip any remaining combining marks.
|
||||
s = removeDiacritics(s)
|
||||
|
||||
// Replace all non-alphanumeric runs with "-"
|
||||
var non_alphanum_regex2 = regexp2.MustCompile(constdata.NON_ALNUM_REGEX, regexp2.None)
|
||||
s, _ = non_alphanum_regex2.Replace(s, "-", -1, -1)
|
||||
|
||||
// Collapse repeated "-" and trim edges
|
||||
var multi_dash_regex2 = regexp2.MustCompile(constdata.MULTI_DASH_REGEX, regexp2.None)
|
||||
s, _ = multi_dash_regex2.Replace(s, "-", -1, -1)
|
||||
|
||||
s = strings.Trim(s, "-")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func transliterateWithTable(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
|
||||
for _, r := range s {
|
||||
if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok {
|
||||
b.WriteString(repl)
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func removeDiacritics(s string) string {
|
||||
t := transform.Chain(
|
||||
norm.NFD,
|
||||
runes.Remove(runes.In(unicode.Mn)),
|
||||
norm.NFC,
|
||||
)
|
||||
out, _, err := transform.String(t, s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return out
|
||||
}
|
||||
283
app/service/storageService/storageService.go
Normal file
283
app/service/storageService/storageService.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,45 @@ package constdata
|
||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||
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 DEFAULT_NEW_CART_NAME = "new cart"
|
||||
|
||||
const USER_LOCALES_NAME = "user"
|
||||
const USER_LOCALES_ID = "userID"
|
||||
const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10
|
||||
|
||||
const USER_LOCALE = "user"
|
||||
|
||||
// WEBDAV
|
||||
const NBYTES_IN_WEBDAV_TOKEN = 32
|
||||
const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage"
|
||||
const WEBDAV_TRIMMED_ROOT = "localhost:3000/api/v1/webdav/storage"
|
||||
|
||||
// Slug sanitization
|
||||
const NON_ALNUM_REGEX = `[^a-z0-9]+`
|
||||
const MULTI_DASH_REGEX = `-+`
|
||||
const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$`
|
||||
|
||||
// Currently supports only German+Polish specific cases
|
||||
var TRANSLITERATION_TABLE = map[rune]string{
|
||||
// German
|
||||
'ä': "ae",
|
||||
'ö': "oe",
|
||||
'ü': "ue",
|
||||
'ß': "ss",
|
||||
|
||||
// Polish
|
||||
'ą': "a",
|
||||
'ć': "c",
|
||||
'ę': "e",
|
||||
'ł': "l",
|
||||
'ń': "n",
|
||||
'ó': "o",
|
||||
'ś': "s",
|
||||
'ż': "z",
|
||||
'ź': "z",
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"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.
|
||||
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), ".")
|
||||
|
||||
if len(parts) >= 2 {
|
||||
|
||||
39
app/utils/localeExtractor/localeExtractor.go
Normal file
39
app/utils/localeExtractor/localeExtractor.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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.Role, bool) {
|
||||
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok || user_locale.OriginalUser == nil || user_locale.OriginalUser.Role == nil {
|
||||
return model.Role{}, false
|
||||
}
|
||||
return *user_locale.OriginalUser.Role, true
|
||||
}
|
||||
|
||||
func GetCustomer(c fiber.Ctx) (*model.Customer, bool) {
|
||||
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok || user_locale.User == nil {
|
||||
return nil, false
|
||||
}
|
||||
return user_locale.User, true
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
@@ -28,18 +27,13 @@ type Found[T any] struct {
|
||||
Spec map[string]interface{} `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// Wraps given query adding limit, offset clauses and SQL_CALC_FOUND_ROWS to it
|
||||
// and running SELECT FOUND_ROWS() afterwards to fetch the total number
|
||||
// (ignoring LIMIT) of results. The final results are wrapped into the
|
||||
// [find.Found] type.
|
||||
func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error) {
|
||||
var items []T
|
||||
var count uint64
|
||||
var count int64
|
||||
|
||||
// stmt.Debug()
|
||||
stmt.Count(&count)
|
||||
|
||||
err := stmt.
|
||||
Clauses(SqlCalcFound()).
|
||||
Offset(paging.Offset()).
|
||||
Limit(paging.Limit()).
|
||||
Find(&items).
|
||||
@@ -48,22 +42,14 @@ func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error
|
||||
return Found[T]{}, err
|
||||
}
|
||||
|
||||
countInterface, ok := stmt.Get(FOUND_ROWS_CTX_KEY)
|
||||
if !ok {
|
||||
return Found[T]{}, errors.New(FOUND_ROWS_CTX_KEY + " value was not found in the gorm db context")
|
||||
}
|
||||
if count, ok = countInterface.(uint64); !ok {
|
||||
return Found[T]{}, errors.New("failed to cast value under " + FOUND_ROWS_CTX_KEY + " to uint64")
|
||||
}
|
||||
|
||||
columnsSpec := GetColumnsSpec[T](langID)
|
||||
// columnsSpec := GetColumnsSpec[T](langID)
|
||||
|
||||
return Found[T]{
|
||||
Items: items,
|
||||
Count: uint(count),
|
||||
Spec: map[string]interface{}{
|
||||
"columns": columnsSpec,
|
||||
},
|
||||
// Spec: map[string]interface{}{
|
||||
// "columns": columnsSpec,
|
||||
// },
|
||||
}, err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,15 @@ import (
|
||||
|
||||
var (
|
||||
// Typed errors for request validation and authentication
|
||||
ErrInvalidBody = errors.New("invalid request body")
|
||||
ErrNotAuthenticated = errors.New("not authenticated")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserInactive = errors.New("user account is inactive")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenExpired = errors.New("token has expired")
|
||||
ErrTokenRequired = errors.New("token is required")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrInvalidBody = errors.New("invalid request body")
|
||||
ErrNotAuthenticated = errors.New("not authenticated")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserInactive = errors.New("user account is inactive")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenExpired = errors.New("token has expired")
|
||||
ErrTokenRequired = errors.New("token is required")
|
||||
ErrAdminAccessRequired = errors.New("admin access required")
|
||||
|
||||
// Typed errors for logging in and registering
|
||||
ErrInvalidCredentials = errors.New("invalid email or password")
|
||||
@@ -42,6 +44,7 @@ var (
|
||||
// Typed errors for product description handler
|
||||
ErrBadAttribute = errors.New("bad or missing attribute value in header")
|
||||
ErrBadField = errors.New("this field can not be updated")
|
||||
ErrInvalidURLSlug = errors.New("URL slug does not obey the industry standard")
|
||||
ErrInvalidXHTML = errors.New("text is not in xhtml format")
|
||||
ErrAIResponseFail = errors.New("AI responded with failure")
|
||||
ErrAIBadOutput = errors.New("AI response does not obey the format")
|
||||
@@ -50,12 +53,31 @@ var (
|
||||
ErrBadPaging = errors.New("bad or missing paging attribute value in header")
|
||||
|
||||
// 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
|
||||
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
|
||||
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
|
||||
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'")
|
||||
|
||||
// Typed errors for data parsing
|
||||
ErrJSONBody = errors.New("invalid JSON body")
|
||||
|
||||
// Typed errors for addresses
|
||||
ErrMaxAmtOfAddressesReached = errors.New("maximal amount of addresses per user reached")
|
||||
ErrUserHasNoSuchAddress = errors.New("user has no such address")
|
||||
ErrInvalidCountryID = errors.New("invalid country id")
|
||||
ErrInvalidAddressJSON = errors.New("invalid address json")
|
||||
)
|
||||
|
||||
// Error represents an error with HTTP status code
|
||||
@@ -80,6 +102,8 @@ func NewError(err error, status int) *Error {
|
||||
// GetErrorCode returns the error code string for HTTP response mapping
|
||||
func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
switch {
|
||||
case errors.Is(err, ErrForbidden):
|
||||
return i18n.T_(c, "error.err_forbidden")
|
||||
case errors.Is(err, ErrInvalidBody):
|
||||
return i18n.T_(c, "error.err_invalid_body")
|
||||
case errors.Is(err, ErrInvalidCredentials):
|
||||
@@ -108,6 +132,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
return i18n.T_(c, "error.err_token_required")
|
||||
case errors.Is(err, ErrRefreshTokenRequired):
|
||||
return i18n.T_(c, "error.err_refresh_token_required")
|
||||
case errors.Is(err, ErrAdminAccessRequired):
|
||||
return i18n.T_(c, "error.err_admin_access_required")
|
||||
case errors.Is(err, ErrBadLangID):
|
||||
return i18n.T_(c, "error.err_bad_lang_id")
|
||||
case errors.Is(err, ErrBadCountryID):
|
||||
@@ -133,6 +159,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
return i18n.T_(c, "error.err_bad_attribute")
|
||||
case errors.Is(err, ErrBadField):
|
||||
return i18n.T_(c, "error.err_bad_field")
|
||||
case errors.Is(err, ErrInvalidURLSlug):
|
||||
return i18n.T_(c, "error.err_invalid_url_slug")
|
||||
case errors.Is(err, ErrInvalidXHTML):
|
||||
return i18n.T_(c, "error.err_invalid_html")
|
||||
case errors.Is(err, ErrAIResponseFail):
|
||||
@@ -144,14 +172,43 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
return i18n.T_(c, "error.err_bad_paging")
|
||||
|
||||
case errors.Is(err, ErrNoRootFound):
|
||||
return i18n.T_(c, "error.no_root_found")
|
||||
return i18n.T_(c, "error.err_no_root_found")
|
||||
case errors.Is(err, ErrCircularDependency):
|
||||
return i18n.T_(c, "error.err_circular_dependency")
|
||||
case errors.Is(err, ErrStartCategoryNotFound):
|
||||
return i18n.T_(c, "error.err_start_category_not_found")
|
||||
case errors.Is(err, ErrRootNeverReached):
|
||||
return i18n.T_(c, "error.err_root_never_reached")
|
||||
|
||||
case errors.Is(err, ErrMaxAmtOfCartsReached):
|
||||
return i18n.T_(c, "error.max_amt_of_carts_reached")
|
||||
return i18n.T_(c, "error.err_max_amt_of_carts_reached")
|
||||
case errors.Is(err, ErrUserHasNoSuchCart):
|
||||
return i18n.T_(c, "error.user_has_no_such_cart")
|
||||
return i18n.T_(c, "error.err_user_has_no_such_cart")
|
||||
case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||
return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
|
||||
return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist")
|
||||
|
||||
case errors.Is(err, ErrAccessDenied):
|
||||
return i18n.T_(c, "error.err_access_denied")
|
||||
case errors.Is(err, ErrFolderDoesNotExist):
|
||||
return i18n.T_(c, "error.err_folder_does_not_exist")
|
||||
case errors.Is(err, ErrFileDoesNotExist):
|
||||
return i18n.T_(c, "error.err_file_does_not_exist")
|
||||
case errors.Is(err, ErrNameTaken):
|
||||
return i18n.T_(c, "error.err_name_taken")
|
||||
case errors.Is(err, ErrMissingFileFieldDocument):
|
||||
return i18n.T_(c, "error.err_missing_file_field_document")
|
||||
|
||||
case errors.Is(err, ErrJSONBody):
|
||||
return i18n.T_(c, "error.err_json_body")
|
||||
|
||||
case errors.Is(err, ErrMaxAmtOfAddressesReached):
|
||||
return i18n.T_(c, "error.err_max_amt_of_addresses_reached")
|
||||
case errors.Is(err, ErrUserHasNoSuchAddress):
|
||||
return i18n.T_(c, "error.err_user_has_no_such_address")
|
||||
case errors.Is(err, ErrInvalidCountryID):
|
||||
return i18n.T_(c, "error.err_invalid_country_id")
|
||||
case errors.Is(err, ErrInvalidAddressJSON):
|
||||
return i18n.T_(c, "error.err_invalid_address_json")
|
||||
|
||||
default:
|
||||
return i18n.T_(c, "error.err_internal_server_error")
|
||||
@@ -161,6 +218,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
// GetErrorStatus returns the HTTP status code for the given error
|
||||
func GetErrorStatus(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrForbidden):
|
||||
return fiber.StatusForbidden
|
||||
case errors.Is(err, ErrInvalidCredentials),
|
||||
errors.Is(err, ErrNotAuthenticated),
|
||||
errors.Is(err, ErrInvalidToken),
|
||||
@@ -175,6 +234,7 @@ func GetErrorStatus(err error) int {
|
||||
errors.Is(err, ErrEmailPasswordRequired),
|
||||
errors.Is(err, ErrTokenRequired),
|
||||
errors.Is(err, ErrRefreshTokenRequired),
|
||||
errors.Is(err, ErrAdminAccessRequired),
|
||||
errors.Is(err, ErrBadLangID),
|
||||
errors.Is(err, ErrBadCountryID),
|
||||
errors.Is(err, ErrPasswordsDoNotMatch),
|
||||
@@ -186,12 +246,26 @@ func GetErrorStatus(err error) int {
|
||||
errors.Is(err, ErrInvalidPassword),
|
||||
errors.Is(err, ErrBadAttribute),
|
||||
errors.Is(err, ErrBadField),
|
||||
errors.Is(err, ErrInvalidURLSlug),
|
||||
errors.Is(err, ErrInvalidXHTML),
|
||||
errors.Is(err, ErrBadPaging),
|
||||
errors.Is(err, ErrNoRootFound),
|
||||
errors.Is(err, ErrCircularDependency),
|
||||
errors.Is(err, ErrStartCategoryNotFound),
|
||||
errors.Is(err, ErrRootNeverReached),
|
||||
errors.Is(err, ErrMaxAmtOfCartsReached),
|
||||
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),
|
||||
errors.Is(err, ErrJSONBody),
|
||||
errors.Is(err, ErrMaxAmtOfAddressesReached),
|
||||
errors.Is(err, ErrUserHasNoSuchAddress),
|
||||
errors.Is(err, ErrInvalidCountryID),
|
||||
errors.Is(err, ErrInvalidAddressJSON):
|
||||
return fiber.StatusBadRequest
|
||||
case errors.Is(err, ErrEmailExists):
|
||||
return fiber.StatusConflict
|
||||
|
||||
1
bo/.gitignore
vendored
1
bo/.gitignore
vendored
@@ -13,6 +13,7 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
/bo/components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
12
bo/components.d.ts
vendored
12
bo/components.d.ts
vendored
@@ -11,27 +11,18 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
|
||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
|
||||
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
||||
copy: typeof import('./src/components/customer/PageOrders copy.vue')['default']
|
||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.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_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.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']
|
||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||
PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default']
|
||||
PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default']
|
||||
PageOrders: typeof import('./src/components/customer/PageOrders.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']
|
||||
PageProductsList: typeof import('./src/components/admin/PageProductsList.vue')['default']
|
||||
PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default']
|
||||
PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.vue')['default']
|
||||
PageStatistic: typeof import('./src/components/customer/PageStatistic.vue')['default']
|
||||
@@ -39,7 +30,6 @@ declare module 'vue' {
|
||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.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']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
@@ -51,13 +41,11 @@ declare module 'vue' {
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
// import type { NuxtUIOptions } from '@nuxt/ui/unplugin'
|
||||
import type { NuxtUIOptions } from '@nuxt/ui/unplugin'
|
||||
|
||||
// export const uiOptions: NuxtUIOptions = {
|
||||
// ui: {
|
||||
// pagination: {
|
||||
// slots: {
|
||||
// root: '',
|
||||
// }
|
||||
// },
|
||||
// button: {
|
||||
// slots: {
|
||||
// base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
// },
|
||||
// },
|
||||
// input: {
|
||||
// slots: {
|
||||
// base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
// error: 'text-red-600!'
|
||||
// },
|
||||
// },
|
||||
// inputNumber: {
|
||||
// slots: {
|
||||
// base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! pt-2 px-1! w-auto!',
|
||||
// increment: 'border-0! pe-0! ps-0!',
|
||||
// decrement: 'border-0! pe-0! ps-0!'
|
||||
// },
|
||||
// },
|
||||
// select: {
|
||||
// slots: {
|
||||
// base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
// itemLabel: 'text-black! dark:text-white!',
|
||||
// itemTrailingIcon: 'text-black! dark:text-white!'
|
||||
// },
|
||||
// },
|
||||
// formField: {
|
||||
// slots: {
|
||||
// error: 'mt-1! text-[14px] text-error text-red-600! dark:text-red-400!',
|
||||
// label: 'text-[16px]'
|
||||
// },
|
||||
// },
|
||||
// selectMenu: {
|
||||
// slots: {
|
||||
// base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
// content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
|
||||
// itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
||||
// }
|
||||
export const uiOptions: NuxtUIOptions = {
|
||||
ui: {
|
||||
pagination: {
|
||||
slots: {
|
||||
root: '',
|
||||
}
|
||||
},
|
||||
button: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
},
|
||||
},
|
||||
input: {
|
||||
slots: {
|
||||
base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
error: 'text-red-600!'
|
||||
},
|
||||
},
|
||||
inputNumber: {
|
||||
slots: {
|
||||
base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! pt-2 px-1! w-auto!',
|
||||
increment: 'border-0! pe-0! ps-0!',
|
||||
decrement: 'border-0! pe-0! ps-0!'
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slots: {
|
||||
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
itemLabel: 'text-black! dark:text-white!',
|
||||
itemTrailingIcon: 'text-black! dark:text-white!'
|
||||
},
|
||||
},
|
||||
formField: {
|
||||
slots: {
|
||||
error: 'mt-1! text-[14px] text-error text-red-600! dark:text-red-400!',
|
||||
label: 'text-[16px]'
|
||||
},
|
||||
},
|
||||
selectMenu: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
|
||||
itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
||||
}
|
||||
|
||||
// },
|
||||
// table: {
|
||||
// slots: {
|
||||
// base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||
// tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||
// }
|
||||
},
|
||||
table: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||
}
|
||||
|
||||
// },
|
||||
// modal: {
|
||||
// slots: {
|
||||
// content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||
// }
|
||||
},
|
||||
modal: {
|
||||
slots: {
|
||||
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||
}
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,11 @@
|
||||
<template>
|
||||
<suspense>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<!-- <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">
|
||||
<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 class="flex gap-10">
|
||||
<CategoryMenu />
|
||||
<div class="w-full flex flex-col items-center gap-4">
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
|
||||
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
@@ -46,26 +18,20 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
||||
import type { Product } from '@/types/product'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const perPage = ref(15)
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page) || 1,
|
||||
get: () => Number(route.query.p) || 1,
|
||||
set: (val: number) => {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
page: val
|
||||
p: val
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -101,7 +67,6 @@ const sortField = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
@@ -162,17 +127,25 @@ async function fetchProductList() {
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
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 {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
total.value = Number(response.count) || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
@@ -182,16 +155,11 @@ async function fetchProductList() {
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
name: 'customer-product-details',
|
||||
params: { product_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'
|
||||
@@ -205,25 +173,7 @@ 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()
|
||||
})
|
||||
},
|
||||
const columns: TableColumn<Product>[] = [
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: ({ column }) => {
|
||||
@@ -314,108 +264,17 @@ const columns: TableColumn<Payment>[] = [
|
||||
},
|
||||
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)
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
color: 'primary',
|
||||
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')
|
||||
}, () => 'Show product')
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -427,4 +286,4 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,126 +1,130 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container my-10 mx-auto ">
|
||||
<div class="container my-10 mx-auto ">
|
||||
|
||||
<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">
|
||||
<div class="flex items-end gap-3">
|
||||
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!" valueKey="iso_code">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
|
||||
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
|
||||
modelValue)?.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
||||
<span class="text-md">{{ item.flag }}</span>
|
||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</USelect>
|
||||
<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">
|
||||
<div class="flex items-end gap-3">
|
||||
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!"
|
||||
valueKey="iso_code">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
|
||||
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
|
||||
modelValue)?.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
||||
<span class="text-md">{{ item.flag }}</span>
|
||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</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>
|
||||
<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 class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
|
||||
<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">
|
||||
<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" />
|
||||
<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" />
|
||||
</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 v-else-if="productStore.error" class="flex items-center justify-center py-20">
|
||||
<p class="text-red-500">{{ productStore.error }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-[25px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.name || 'Product Name' }}
|
||||
</p>
|
||||
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
|
||||
<div class="space-y-[10px]">
|
||||
<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 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 class="flex flex-col gap-2">
|
||||
<p class="text-[25px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.name || 'Product Name' }}
|
||||
</p>
|
||||
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
|
||||
<div class="space-y-[10px]">
|
||||
<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 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 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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -142,7 +146,7 @@ const isEditing = ref(false)
|
||||
|
||||
const availableLangs = computed(() => langs)
|
||||
|
||||
const selectedLanguage = ref('pl')
|
||||
const selectedLanguage = ref('en')
|
||||
|
||||
const currentLangId = ref(2)
|
||||
const productID = ref<number>(0)
|
||||
@@ -176,7 +180,7 @@ const translateToSelectedLanguage = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id
|
||||
const id = route.params.product_id
|
||||
if (id) {
|
||||
productID.value = Number(id)
|
||||
await fetchForLanguage(selectedLanguage.value)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="flex gap-2">
|
||||
<CategoryMenuListing />
|
||||
<CategoryMenu />
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||
@@ -46,7 +46,7 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
|
||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
@@ -167,7 +167,7 @@ async function fetchProductList() {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/list-products/get-listing?${params}`
|
||||
const url = `/api/v1/restricted/list/list-products?${params}`
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getMenu } from '@/router/menu'
|
||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
let menu = await getMenu() as NavigationMenuItem[]
|
||||
|
||||
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[]) {
|
||||
for (const item of menu) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
console.log(item);
|
||||
item.open = path && path.includes(item.category_id) ? true : openAll.value
|
||||
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 } })
|
||||
item.children.unshift({
|
||||
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 {
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -22,15 +59,10 @@ function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
}
|
||||
|
||||
menu = adaptMenu(menu)
|
||||
|
||||
const items = ref<NavigationMenuItem[][]>([
|
||||
[
|
||||
...menu as NavigationMenuItem[]
|
||||
],
|
||||
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
|
||||
</template>
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -62,14 +62,6 @@ async function changeLang() {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
if (items?.token) {
|
||||
cookie.setCookie('Aurrie', items.token, {
|
||||
days: 60,
|
||||
secure: true,
|
||||
sameSite: 'Lax'
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ async function setRoutes() {
|
||||
const componentName = item.component
|
||||
const [, folder] = componentName.split('/')
|
||||
const componentPath = `/src${componentName}`
|
||||
console.log(componentPath);
|
||||
|
||||
|
||||
let modules =
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useFetchJson } from "@/composable/useFetchJson";
|
||||
import type { MenuItem, Route } from "@/types/menu";
|
||||
import { ref } from "vue";
|
||||
import { settings } from "./settings";
|
||||
|
||||
|
||||
const categoryId = ref()
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
export const getRoutes = async () => {
|
||||
const resp = await useFetchJson<Route[]>('/api/v1/public/menu/get-routes');
|
||||
|
||||
|
||||
@@ -13,24 +13,24 @@ const products = [
|
||||
// type CategoryProducts = {}
|
||||
|
||||
export const useCategoryStore = defineStore('category', () => {
|
||||
const id_category = ref(0)
|
||||
const idCategory = ref(0)
|
||||
const categoryProducts = ref(products)
|
||||
|
||||
|
||||
function setCategoryID(id: number) {
|
||||
id_category.value = id
|
||||
idCategory.value = id
|
||||
}
|
||||
|
||||
async function getCategoryProducts() {
|
||||
return new Promise<typeof products>((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('Fetching products from category id: ', id_category.value);
|
||||
// console.log('Fetching products from category id: ', idCategory.value);
|
||||
resolve(categoryProducts.value)
|
||||
}, 2000 * Math.random())
|
||||
})
|
||||
}
|
||||
return {
|
||||
id_category,
|
||||
idCategory,
|
||||
getCategoryProducts,
|
||||
setCategoryID
|
||||
}
|
||||
|
||||
@@ -34,9 +34,8 @@ export const useProductStore = defineStore('product', () => {
|
||||
|
||||
try {
|
||||
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
|
||||
|
||||
} catch (e: unknown) {
|
||||
|
||||
10
bo/src/types/product.d.ts
vendored
10
bo/src/types/product.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
export interface ProductDescription {
|
||||
export interface ProductDescription {
|
||||
id?: number
|
||||
name?: string
|
||||
description: string
|
||||
@@ -6,4 +6,12 @@
|
||||
meta_description: string
|
||||
available_now: string
|
||||
usage: string
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
1
bo/src/types/settings.d.ts
vendored
1
bo/src/types/settings.d.ts
vendored
@@ -7,6 +7,7 @@ export interface Settings {
|
||||
}
|
||||
|
||||
export interface App {
|
||||
category_tree_root_id: number
|
||||
name: string
|
||||
environment: string
|
||||
base_url: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Change Locales
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Create Search Index
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Delete Index - MeiliSearch
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Search Index Settings
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Search Items
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
@@ -10,12 +10,11 @@ http:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"q": "kinder",
|
||||
"q": "mat",
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
// "filter": "'attr.10'= 71",
|
||||
// "facets":["category_ids", "price", "attr"],
|
||||
"attributesToRetrieve" : ["product_id"]
|
||||
"filter": "'category_ids'= 10",
|
||||
// "facets":["category_ids", "price"]
|
||||
"facets": ["category_ids", "attr", "feat", "price"]
|
||||
}
|
||||
auth:
|
||||
|
||||
29
bruno/api_v1/auth/Login.yml
Normal file
29
bruno/api_v1/auth/Login.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
info:
|
||||
name: Login
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{bas_url}}/public/auth/login"
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"email":"{{email}}",
|
||||
"password":"{{password}}"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: email
|
||||
value: admin@ma-al.com
|
||||
- name: password
|
||||
value: Maal12345678
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/auth/folder.yml
Normal file
7
bruno/api_v1/auth/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: auth
|
||||
type: folder
|
||||
seq: 6
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
22
bruno/api_v1/currency/currency-rate.yml
Normal file
22
bruno/api_v1/currency/currency-rate.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
name: currency-rate
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{bas_url}}/restricted/currency-rate"
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"b2b_id_currency" : 1,
|
||||
"conversion_rate": 4.2
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
20
bruno/api_v1/currency/currency.yml
Normal file
20
bruno/api_v1/currency/currency.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
info:
|
||||
name: currency
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/currency-rate/{{id}}"
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: id
|
||||
value: "1"
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/currency/folder.yml
Normal file
7
bruno/api_v1/currency/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: currency
|
||||
type: folder
|
||||
seq: 8
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
15
bruno/api_v1/customer/Customer (me).yml
Normal file
15
bruno/api_v1/customer/Customer (me).yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: Customer (me)
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/customer"
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
19
bruno/api_v1/customer/Customer (other).yml
Normal file
19
bruno/api_v1/customer/Customer (other).yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: Customer (other)
|
||||
type: http
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/customer?id=1"
|
||||
params:
|
||||
- name: id
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
19
bruno/api_v1/customer/Customer list.yml
Normal file
19
bruno/api_v1/customer/Customer list.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: Customer list
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/customer/list?search="
|
||||
params:
|
||||
- name: search
|
||||
value: ""
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/customer/folder.yml
Normal file
7
bruno/api_v1/customer/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: customer
|
||||
type: folder
|
||||
seq: 9
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
15
bruno/api_v1/product/Get Product.yml
Normal file
15
bruno/api_v1/product/Get Product.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: Get Product
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/product/200/1/5"
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
|
||||
http:
|
||||
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/product/list?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62"
|
||||
params:
|
||||
- name: p
|
||||
value: "1"
|
||||
@@ -25,9 +25,6 @@ http:
|
||||
body:
|
||||
type: json
|
||||
data: ""
|
||||
auth:
|
||||
type: bearer
|
||||
token: "{{token}}"
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
7
bruno/api_v1/product/folder.yml
Normal file
7
bruno/api_v1/product/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: product
|
||||
type: folder
|
||||
seq: 7
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
39
bruno/b2b-daniel/save-product-description.yml
Normal file
39
bruno/b2b-daniel/save-product-description.yml
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user