diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 54910ea..454c350 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -10,14 +10,14 @@ import ( "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" ) // AuthMiddleware creates authentication middleware -func AuthMiddleware() fiber.Handler { +func Authenticate() fiber.Handler { authService := authService.NewAuthService() - return func(c fiber.Ctx) error { // Get token from Authorization header authHeader := c.Get("Authorization") @@ -25,17 +25,13 @@ func AuthMiddleware() fiber.Handler { // Try to get from cookie authHeader = c.Cookies("access_token") if authHeader == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "authorization token required", - }) + return c.Next() } } else { // Extract token from "Bearer " parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "invalid authorization header format", - }) + return c.Next() } authHeader = parts[1] } @@ -43,24 +39,18 @@ func AuthMiddleware() fiber.Handler { // Validate token claims, err := authService.ValidateToken(authHeader) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "invalid or expired token", - }) + return c.Next() } // Get user from database user, err := authService.GetUserByID(claims.UserID) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "user not found", - }) + return c.Next() } // Check if user is active if !user.IsActive { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "user account is inactive", - }) + return c.Next() } // Create locale. LangID is overwritten by auth Token @@ -79,9 +69,7 @@ func AuthMiddleware() fiber.Handler { // 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", - }) + return c.Next() } targetUserID, err := strconv.Atoi(targetUserIDAttribute) @@ -114,6 +102,18 @@ func AuthMiddleware() fiber.Handler { } } +func Authorize() fiber.Handler { + return func(c fiber.Ctx) error { + _, ok := localeExtractor.GetUserID(c) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "not authenticated", + }) + } + return c.Next() + } +} + // Webdav func Webdav() fiber.Handler { authService := authService.NewAuthService() diff --git a/app/delivery/web/api/public/auth.go b/app/delivery/web/api/public/auth.go index 5cb4e52..527a587 100644 --- a/app/delivery/web/api/public/auth.go +++ b/app/delivery/web/api/public/auth.go @@ -49,7 +49,7 @@ func AuthHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/google", handler.GoogleLogin) r.Get("/google/callback", handler.GoogleCallback) - authProtected := r.Group("", middleware.AuthMiddleware()) + authProtected := r.Group("", middleware.Authorize()) authProtected.Get("/me", handler.Me) authProtected.Post("/update-choice", handler.UpdateJWTToken) diff --git a/app/delivery/web/api/public/routing.go b/app/delivery/web/api/public/routing.go index bad8746..3a78581 100644 --- a/app/delivery/web/api/public/routing.go +++ b/app/delivery/web/api/public/routing.go @@ -2,6 +2,7 @@ package public import ( "git.ma-al.com/goc_daniel/b2b/app/service/menuService" + 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/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" @@ -31,12 +32,21 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router { } func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { - lang_id, ok := localeExtractor.GetLangID(c) + langId, ok := localeExtractor.GetLangID(c) if !ok { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } - menu, err := h.menuService.GetRoutes(lang_id) + + var roleId uint + customer, ok := localeExtractor.GetCustomer(c) + if !ok { + roleId = constdata.UNLOGGED_USER_ROLE_ID + } else { + roleId = customer.RoleID + } + + menu, err := h.menuService.GetRoutes(langId, roleId) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 8e6eac9..73559aa 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -86,9 +86,10 @@ func (s *Server) Setup() error { // API routes s.api = s.app.Group("/api/v1") + s.api.Use(middleware.Authenticate()) s.public = s.api.Group("/public") s.restricted = s.api.Group("/restricted") - s.restricted.Use(middleware.AuthMiddleware()) + s.restricted.Use(middleware.Authorize()) s.webdav = s.api.Group("/webdav") s.webdav.Use(middleware.Webdav()) diff --git a/app/model/routing.go b/app/model/routing.go index 59090f6..1166ae1 100644 --- a/app/model/routing.go +++ b/app/model/routing.go @@ -7,7 +7,6 @@ type Route struct { Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"` Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"` Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"` - SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"` } func (Route) TableName() string { diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 344b7a8..a6d850d 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -18,7 +18,7 @@ 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, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error) - GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error) + GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error) GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error) AddToFavorites(userID uint, productID uint) error @@ -33,11 +33,11 @@ func New() UIProductsRepo { return &ProductsRepo{} } -func (repo *ProductsRepo) GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error) { +func (repo *ProductsRepo) GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error) { var result view.Product - err := db.DB.Raw(`CALL get_product_base(?,?,?)`, - p_id_product, p_id_shop, p_id_lang). + err := db.DB.Raw(`CALL get_product_base(?,?,?,?)`, + p_id_product, p_id_shop, p_id_lang, p_id_customer). Scan(&result).Error return result, err diff --git a/app/repos/routesRepo/routesRepo.go b/app/repos/routesRepo/routesRepo.go index 09e5754..0b70dd8 100644 --- a/app/repos/routesRepo/routesRepo.go +++ b/app/repos/routesRepo/routesRepo.go @@ -7,7 +7,7 @@ import ( ) type UIRoutesRepo interface { - GetRoutes(langId uint) ([]model.Route, error) + GetRoutes(langId uint, roleId uint) ([]model.Route, error) GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error) } @@ -17,13 +17,18 @@ func New() UIRoutesRepo { return &RoutesRepo{} } -func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { +func (p *RoutesRepo) GetRoutes(langId uint, roleId uint) ([]model.Route, error) { routes := []model.Route{} - err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error - if err != nil { - return nil, err - } - return routes, nil + + err := db. + Get(). + Model(model.Route{}). + Joins("JOIN b2b_route_roles rr ON rr.route_id = b2b_routes.id"). + Where(model.Route{Active: nullable.GetNil(true)}). + Where("rr.role_id = ?", roleId). + Find(&routes).Error + + return routes, err } func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) { diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index e63b6b5..cc98450 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -102,8 +102,8 @@ func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCate return node, true } -func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { - return s.routesRepo.GetRoutes(id_lang) +func (s *MenuService) GetRoutes(id_lang, roleId uint) ([]model.Route, error) { + return s.routesRepo.GetRoutes(id_lang, roleId) } func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go index 3296d88..c41b7ea 100644 --- a/app/service/productService/productService.go +++ b/app/service/productService/productService.go @@ -27,7 +27,7 @@ func (s *ProductService) Get( p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity uint, ) (*json.RawMessage, error) { - product, err := s.productsRepo.GetBase(p_id_product, constdata.SHOP_ID, p_id_lang) + product, err := s.productsRepo.GetBase(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer) if err != nil { return nil, err } diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 7800703..30390a6 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -32,3 +32,5 @@ const WEBDAV_TRIMMED_ROOT = "localhost:3000/api/v1/webdav/storage" const NON_ALNUM_REGEX = `[^a-z0-9]+` const MULTI_DASH_REGEX = `-+` const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` + +const UNLOGGED_USER_ROLE_ID = 4 diff --git a/bruno/api_v1/routes/Routes.yml b/bruno/api_v1/routes/Routes.yml new file mode 100644 index 0000000..d88bdd0 --- /dev/null +++ b/bruno/api_v1/routes/Routes.yml @@ -0,0 +1,15 @@ +info: + name: Routes + type: http + seq: 1 + +http: + method: GET + url: "" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/api_v1/routes/folder.yml b/bruno/api_v1/routes/folder.yml new file mode 100644 index 0000000..bf44876 --- /dev/null +++ b/bruno/api_v1/routes/folder.yml @@ -0,0 +1,7 @@ +info: + name: routes + type: folder + seq: 10 + +request: + auth: inherit diff --git a/i18n/migrations/20260302163100_routes.sql b/i18n/migrations/20260302163100_routes.sql index f29831f..f7992e1 100644 --- a/i18n/migrations/20260302163100_routes.sql +++ b/i18n/migrations/20260302163100_routes.sql @@ -42,6 +42,11 @@ INSERT IGNORE INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `a (3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,JSON_COMPACT('{}'),1,1), (9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,JSON_COMPACT('{"route": {"name": "home", "params":{"locale": ""}}}'),1,1); +CREATE TABLE `b2b_route_roles` ( + `route_id` INT NOT NULL, + `role_id` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`, `role_id`) +); -- +goose Down diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index 5662d6a..e25205d 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -338,6 +338,24 @@ ON b2b_specific_price_customer (b2b_id_customer); CREATE INDEX idx_bsp_country_rel ON b2b_specific_price_country (b2b_id_country); +CREATE TABLE b2b_route_roles ( + route_id INT NOT NULL, + role_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (route_id, role_id), + INDEX idx_role_id (role_id), + INDEX idx_route_id (route_id), + CONSTRAINT FK_b2b_route_roles_route_id + FOREIGN KEY (route_id) + REFERENCES b2b_routes (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_b2b_route_roles_role_id + FOREIGN KEY (role_id) + REFERENCES b2b_roles (id) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB; + DELIMITER // CREATE FUNCTION IF NOT EXISTS slugify_eu(input TEXT) @@ -438,6 +456,7 @@ DROP TABLE IF EXISTS b2b_customer_carts; DROP TABLE IF EXISTS b2b_specific_price_country; DROP TABLE IF EXISTS b2b_specific_price_customer; DROP TABLE IF EXISTS b2b_specific_price_product_attribute; +DROP TABLE IF EXISTS b2b_route_roles; DROP TABLE IF EXISTS b2b_specific_price_category; DROP TABLE IF EXISTS b2b_specific_price_product; DROP TABLE IF EXISTS b2b_specific_price; diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql index 5f7a634..bb9b449 100644 --- a/i18n/migrations/20260302163123_create_tables_data.sql +++ b/i18n/migrations/20260302163123_create_tables_data.sql @@ -10,6 +10,7 @@ VALUES INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('user','1'); INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('admin','2'); INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3'); +INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('unlogged','4'); -- insert sample admin user admin@ma-al.com/Maal12345678 @@ -58,4 +59,32 @@ INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '6' INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '7'); INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '8'); INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '9'); + +INSERT INTO `b2b_route_roles` (`route_id`, `role_id`) VALUES +(1, '1'), +(1, '2'), +(1, '3'), +(2, '1'), +(2, '2'), +(2, '3'), +(3, '1'), +(3, '2'), +(3, '3'), +(3, '4'), +(4, '1'), +(4, '2'), +(4, '3'), +(4, '4'), +(5, '1'), +(5, '2'), +(5, '3'), +(5, '4'), +(6, '1'), +(6, '2'), +(6, '3'), +(6, '4'), +(7, '1'), +(7, '2'), +(7, '3'), +(7, '4'); -- +goose Down \ No newline at end of file diff --git a/i18n/migrations/20260319163200_procedures.sql b/i18n/migrations/20260319163200_procedures.sql index 4f44cc0..267985a 100644 --- a/i18n/migrations/20260319163200_procedures.sql +++ b/i18n/migrations/20260319163200_procedures.sql @@ -319,7 +319,8 @@ DROP PROCEDURE IF EXISTS get_product_base // CREATE PROCEDURE get_product_base( IN p_id_product INT, IN p_id_shop INT, - IN p_id_lang INT + IN p_id_lang INT, + IN p_id_customer INT ) BEGIN SELECT @@ -376,14 +377,12 @@ BEGIN -- Relations m.name AS manufacturer, - cl.name AS category + cl.name AS category, - -- This doesn't fit to base product, I'll add proper is_favorite to product later - - -- EXISTS( - -- SELECT 1 FROM b2b_favorites f - -- WHERE f.user_id = p_id_customer AND f.product_id = p_id_product - -- ) AS is_favorite + EXISTS( + SELECT 1 FROM b2b_favorites f + WHERE f.user_id = p_id_customer AND f.product_id = p_id_product + ) AS is_favorite