From 967b101f9bcdb0be35a783a0b1bcc61cdcd43017 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 14 Apr 2026 09:49:37 +0200 Subject: [PATCH 1/4] feat: make routing per role, add unlogged role --- app/delivery/middleware/auth.go | 39 +++++++++---------- app/delivery/web/api/public/auth.go | 2 +- app/delivery/web/api/public/routing.go | 18 +++++++-- app/delivery/web/init.go | 3 +- app/model/routing.go | 1 - app/repos/routesRepo/routesRepo.go | 19 +++++---- app/service/menuService/menuService.go | 4 +- app/utils/const_data/consts.go | 2 + bruno/api_v1/routes/Routes.yml | 15 +++++++ bruno/api_v1/routes/folder.yml | 7 ++++ i18n/migrations/20260302163100_routes.sql | 5 +++ .../20260302163122_create_tables.sql | 19 +++++++++ .../20260302163123_create_tables_data.sql | 30 ++++++++++++++ 13 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 bruno/api_v1/routes/Routes.yml create mode 100644 bruno/api_v1/routes/folder.yml diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 756e79f..73f0a1f 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -16,9 +16,8 @@ import ( ) // 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") @@ -26,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] } @@ -44,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 @@ -80,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) @@ -115,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() + } +} + // RequireAdmin creates admin-only middleware func RequireAdmin() fiber.Handler { return func(c fiber.Ctx) error { 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 29fcd71..bbefc10 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/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 de2498d..13ef15e 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -88,8 +88,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/utils/const_data/consts.go b/app/utils/const_data/consts.go index 5633b4d..ca732cb 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -46,3 +46,5 @@ var TRANSLITERATION_TABLE = map[rune]string{ 'ż': "z", 'ź': "z", } + +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 c03014a..58b7f01 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -310,6 +310,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) @@ -415,3 +433,4 @@ DROP TABLE IF EXISTS b2b_specific_price; DROP TABLE IF EXISTS b2b_specific_price_product; DROP TABLE IF EXISTS b2b_specific_price_category; DROP TABLE IF EXISTS b2b_specific_price_product_attribute; +DROP TABLE IF EXISTS b2b_route_roles; diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql index bb7fde3..16d3ee2 100644 --- a/i18n/migrations/20260302163123_create_tables_data.sql +++ b/i18n/migrations/20260302163123_create_tables_data.sql @@ -46,4 +46,34 @@ INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '2' INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '3'); INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '4'); INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '5'); + +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 -- 2.49.1 From f14d60d67bca47b625590e0c262e05d1e19f6a41 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 14 Apr 2026 10:17:05 +0200 Subject: [PATCH 2/4] chore: swap permission string in handler to consts --- app/delivery/web/api/restricted/specificPrice.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/delivery/web/api/restricted/specificPrice.go b/app/delivery/web/api/restricted/specificPrice.go index bece83d..e007f57 100644 --- a/app/delivery/web/api/restricted/specificPrice.go +++ b/app/delivery/web/api/restricted/specificPrice.go @@ -5,6 +5,7 @@ import ( "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/specificPriceService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" @@ -30,13 +31,13 @@ func NewSpecificPriceHandler() *SpecificPriceHandler { func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router { handler := NewSpecificPriceHandler() - r.Post("/", middleware.Require("specific_price.manage"), handler.Create) - r.Put("/:id", middleware.Require("specific_price.manage"), handler.Update) - r.Delete("/:id", middleware.Require("specific_price.manage"), handler.Delete) - r.Get("/", middleware.Require("specific_price.manage"), handler.List) - r.Get("/:id", middleware.Require("specific_price.manage"), handler.GetByID) - r.Patch("/:id/activate", middleware.Require("specific_price.manage"), handler.Activate) - r.Patch("/:id/deactivate", middleware.Require("specific_price.manage"), handler.Deactivate) + r.Post("/", middleware.Require(perms.SpecificPriceManage), handler.Create) + r.Put("/:id", middleware.Require(perms.SpecificPriceManage), handler.Update) + r.Delete("/:id", middleware.Require(perms.SpecificPriceManage), handler.Delete) + r.Get("/", middleware.Require(perms.SpecificPriceManage), handler.List) + r.Get("/:id", middleware.Require(perms.SpecificPriceManage), handler.GetByID) + r.Patch("/:id/activate", middleware.Require(perms.SpecificPriceManage), handler.Activate) + r.Patch("/:id/deactivate", middleware.Require(perms.SpecificPriceManage), handler.Deactivate) return r } -- 2.49.1 From d173af29fe90f746406896c6def9e1a91e98daf5 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 14 Apr 2026 10:18:12 +0200 Subject: [PATCH 3/4] fix: actually add the unlogged role to migration --- i18n/migrations/20260302163123_create_tables_data.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql index 16d3ee2..a472c2c 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 -- 2.49.1 From ab783b599de84f2ccb7e09293841c29570844c58 Mon Sep 17 00:00:00 2001 From: Wiktor Date: Tue, 14 Apr 2026 11:07:55 +0200 Subject: [PATCH 4/4] chore: add favorite field to base product query --- app/repos/productsRepo/productsRepo.go | 8 ++++---- app/service/productService/productService.go | 2 +- i18n/migrations/20260319163200_procedures.sql | 15 +++++++-------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 5f0e1a9..02f36df 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/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/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 -- 2.49.1