package middlewares import ( "fmt" "log" "sort" "strings" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/auth" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) func MenuMiddleware(c *fiber.Ctx) error { // Verificar si el usuario está logueado if !auth.IsLoggedIn(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Usuario no autenticado", }) } // Obtener los datos del usuario autenticado user, err := auth.User(c) if err != nil { // Si ocurre un error al obtener el usuario, finalizar la solicitud fmt.Printf("Error al obtener usuario: %v\n", err) return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Usuario no válido o sesión expirada", }) } // Buscar los módulos y submódulos del usuario userModules, err := models.FindUserByID(user.ID) if err != nil { fmt.Printf("Error al buscar datos del usuario: %v\n", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "message": "Error al obtener datos del usuario", }) } // Organizar los módulos y submódulos moduleMap := make(map[string][]map[string]string) // El administrador entra a todas las rutas (ver más abajo), pero el menú // se armaba solo con los submódulos de SU rol: podía acceder a una // pantalla y no tener el enlace por ningún lado. Es lo que hacía que un // módulo existente —con ruta, vista y seed— pareciera no existir. visibles := userModules.Role.Submodules if user.IsAdmin { if todos, err := models.AllSubmodulesConModulo(); err == nil { visibles = todos } else { log.Printf("[MENU] no se pudo cargar el menú completo para el administrador: %v", err) } } for _, submodule := range visibles { // Recuperar el nombre del módulo moduleName := submodule.Module.Title // Crear un mapa para cada submódulo que contenga el título y la URL submoduleData := map[string]string{ "title": submodule.Title, "url": submodule.Url, // Suponiendo que el submódulo tiene un campo `URL` } // Agregar el submódulo a la lista correspondiente del módulo moduleMap[moduleName] = append(moduleMap[moduleName], submoduleData) } // Ordenar los módulos alfabéticamente var sortedModules []string for module := range moduleMap { sortedModules = append(sortedModules, module) } sort.Strings(sortedModules) // Ordenar los submódulos de cada módulo alfabéticamente for _, moduleName := range sortedModules { // Ordenar los submódulos alfabéticamente por título sort.Slice(moduleMap[moduleName], func(i, j int) bool { return moduleMap[moduleName][i]["title"] < moduleMap[moduleName][j]["title"] }) } c.Locals("userModules", moduleMap) // Obtener un array solo con las URLs var urls []string for _, submodules := range moduleMap { for _, submoduleData := range submodules { urls = append(urls, submoduleData["url"]) } } // Opcional: adjuntar la lista de URLs al contexto c.Locals("urls", urls) // Los administradores tienen acceso a todas las rutas if user.IsAdmin { return c.Next() } // Se compara la ruta COMPLETA contra las URLs permitidas. // // Antes se comparaba solo el último segmento (lo que va después del último // '/'), y eso rompía en las dos direcciones: dos rutas distintas que // terminan igual quedaban indistinguibles — con permiso sobre // /app/doc/categorias se entraba a cualquier otra ruta terminada en // "categorias" — y a la vez el permiso parecía no aplicarse donde sí // debía, porque el segmento coincidía por casualidad. urlAllowed := PuedeVerRuta(c.Path(), urls) // Si la URL no está permitida, devolver un error if !urlAllowed { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": true, "message": "Acceso no permitido a esta ruta", }) } // Continuar con la siguiente función de middleware o manejador return c.Next() } // PuedeVerRuta decide si una ruta está cubierta por alguna de las URLs que el // rol tiene asignadas. // // Coincide la ruta exacta o cualquier ruta por debajo de ella: quien tiene // /app/doc/paginas también puede entrar a /app/doc/paginas/7, que es la misma // pantalla con un detalle. No coincide /app/doc/paginas-privadas, porque el // corte se hace en el separador y no en el prefijo de texto — si no, un // permiso abriría rutas vecinas que solo comparten el comienzo del nombre. func PuedeVerRuta(ruta string, permitidas []string) bool { ruta = strings.TrimRight(ruta, "/") if ruta == "" { ruta = "/" } for _, u := range permitidas { u = strings.TrimRight(strings.TrimSpace(u), "/") if u == "" { continue } if ruta == u || strings.HasPrefix(ruta, u+"/") { return true } } return false } // RequiereModulo protege un endpoint declarando a qué pantalla pertenece. // // MenuMiddleware compara la ruta pedida contra los submódulos del rol, y eso // alcanza para las páginas porque su URL ES la del submódulo. Pero los // endpoints de datos viven en otra ruta (/app/loadusers alimenta la pantalla // /app/users), así que compararlos contra sí mismos nunca coincidiría y // dejaría afuera hasta a quien sí tiene el permiso. // // Por eso el submódulo se declara en la ruta: // // protected.Get("/loadusers", middlewares.RequiereModulo("/app/users"), controllers.GetUsers) // // Sin esto, cualquier usuario con sesión podía pedir los datos de una pantalla // que no tiene asignada escribiendo la URL a mano: el permiso solo tapaba el // HTML, no la información. func RequiereModulo(urlSubmodulo string) fiber.Handler { return func(c *fiber.Ctx) error { user, err := auth.User(c) if err != nil || user == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Usuario no autenticado", }) } // Mismo criterio que MenuMiddleware: el administrador entra a todo. if user.IsAdmin { return c.Next() } datos, err := models.FindUserByID(user.ID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "message": "Error al obtener datos del usuario", }) } var urls []string for _, s := range datos.Role.Submodules { urls = append(urls, s.Url) } if !PuedeVerRuta(urlSubmodulo, urls) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": true, "message": "No tenés permiso para acceder a esta información", }) } return c.Next() } }