Files
soft_usite/rest/middlewares/menu.go
T
Lizandro GuarnizoandClaude Sonnet 5 0100986a46 fix(menu): el administrador no veía los módulos a los que sí puede entrar
El módulo Facturas tiene ruta, vista y seed, pero no aparecía en el menú.
No faltaba: MenuMiddleware arma el menú solo con los submódulos del rol del
usuario, y esa parte corre ANTES del bypass de IsAdmin.

O sea que un administrador entra a cualquier ruta escribiéndola, pero solo
ve en el menú lo que su rol tenga asignado. Un módulo completo puede
parecer inexistente por eso, y no hay nada en pantalla que lo explique.

Ahora, si el usuario es administrador, el menú se arma con todos los
submódulos: coincide con el acceso que ya tiene. Para el resto no cambia
nada — siguen viendo solo lo suyo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:56:21 -05:00

198 lines
6.3 KiB
Go
Executable File

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()
}
}