fix(permisos): compara la ruta completa y avisa de los menús que dan 404

Auditoría del sistema de permisos a partir del 404 en /app/doc/paginas.

1. La comparación de permisos usaba solo el último segmento de la URL
   (lo que va después del último '/'). Rompía en las dos direcciones: 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í correspondía porque el segmento coincidía por casualidad.
   Ahora se compara la ruta completa, cortando en el separador para que
   /app/doc/paginas no habilite /app/doc/paginas-privadas.

2. El submódulo de Documentación nunca se sembró, aunque las rutas y las
   vistas existen desde mayo. Había que crear el ítem del menú a mano, y
   una URL mal tipeada ahí se ve exactamente igual que un permiso mal
   asignado: un 404. Ahora se siembra con la URL correcta.

3. El submódulo "Statuspage" apuntaba a /app/statuspage, que no existe en
   ninguna parte — la página real es la pública /status. Se corrige el
   seed y también el registro ya creado en la base.

4. Chequeo en el arranque que recorre los submódulos de la base y avisa
   cuáles apuntan a una URL sin ruta. Cubre los creados a mano, que es
   justo donde el compilador y los tests no llegan.

5. Test que cruza las URLs sembradas contra las rutas registradas: fue el
   que encontró lo de statuspage.

Queda pendiente y es más grande: de 454 rutas protegidas solo 54 pasan por
MenuMiddleware. El permiso gatea la página HTML pero no los endpoints de
datos — cualquier usuario autenticado puede llamar a
GET /app/doc/loadpaginas o DELETE /app/doc/paginas/:id sin tener el
submódulo asignado. Se reporta antes de tocarlo porque cerrarlo de golpe
puede dejar gente afuera.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-15 09:21:44 -05:00
co-authored by Claude Sonnet 5
parent ad3458bb71
commit 105ab44759
5 changed files with 278 additions and 20 deletions
+90 -1
View File
@@ -3,6 +3,7 @@ package migrations
import (
"fmt"
"log"
"strings"
"time"
"github.com/sujit-baniya/fiber-boilerplate/app"
@@ -239,9 +240,15 @@ func SeedStatuspage() {
}
entries := []struct{ title, desc, url string }{
{"Statuspage", "Monitoreo de estado de servicios vía Atlassian Statuspage", "/app/statuspage"},
// La página real es la pública /status (rest/routes/publicas.go):
// /app/statuspage nunca existió y el ítem daba 404 al entrar.
{"Statuspage", "Monitoreo de estado de servicios vía Atlassian Statuspage", "/status"},
}
// La URL vieja apuntaba a /app/statuspage, que no existe: se corrige el
// registro ya creado en vez de dejar un ítem de menú que da 404.
db.Model(&models.Submodules{}).Where("url = ?", "/app/statuspage").Update("url", "/status")
var insertados []models.Submodules
for _, e := range entries {
var sub models.Submodules
@@ -1533,3 +1540,85 @@ func LiberarColumnasHuerfanasUmind() {
log.Printf("[MIGRACION] %s.%s ya no es NOT NULL (columna huérfana del refactor multi-agente)", h.Tabla, h.Columna)
}
}
// VerificarUrlsDeSubmodulos avisa en el arranque de los ítems del menú cuya
// URL no corresponde a ninguna ruta registrada.
//
// Un submódulo es una URL escrita a mano (en un seed o desde /app/submodules).
// Si no coincide con una ruta real, el ítem igual aparece en el menú y se le
// puede asignar permiso a un rol — y recién al hacer clic aparece un 404, sin
// nada que explique por qué. Esto lo convierte en una línea de log al
// arrancar, en vez de un misterio en producción.
//
// Solo informa: no borra ni modifica nada, porque una URL "rota" puede ser una
// ruta servida por otro lado.
func VerificarUrlsDeSubmodulos(rutasGET map[string]bool) {
var subs []models.Submodules
if err := app.Http.Database.DB.Find(&subs).Error; err != nil {
log.Printf("[MENU] no se pudieron revisar las URLs de los submódulos: %v", err)
return
}
rotos := 0
for _, s := range subs {
url := strings.TrimSpace(s.Url)
if url == "" {
log.Printf("[MENU] el submódulo %q (ID %d) no tiene URL", s.Title, s.ID)
rotos++
continue
}
if !rutasGET[url] {
log.Printf("[MENU] el submódulo %q (ID %d) apunta a %q, que no es ninguna ruta: va a dar 404", s.Title, s.ID, url)
rotos++
}
}
if rotos == 0 {
log.Printf("[MENU] %d submódulos revisados, todas las URLs resuelven", len(subs))
} else {
log.Printf("[MENU] %d de %d submódulos apuntan a una URL inexistente (ver líneas anteriores)", rotos, len(subs))
}
}
// SeedDocumentacion registra los submódulos de Documentación.
//
// Existían las rutas y las vistas pero nunca el seed, así que había que crear
// el ítem del menú a mano desde /app/submodules — y una URL mal tipeada ahí es
// indistinguible de un permiso mal asignado: en los dos casos se ve un 404.
func SeedDocumentacion() {
db := app.Http.Database.DB
var modulo models.Modules
if err := db.Where("title = ?", "Documentación").First(&modulo).Error; err != nil {
modulo = models.Modules{
Title: "Documentación",
Description: "Páginas y categorías de documentación interna",
ModifiedAt: time.Now(),
}
if err := db.Create(&modulo).Error; err != nil {
log.Printf("[SEED] Error creando módulo Documentación: %v", err)
return
}
}
entries := []struct{ title, desc, url string }{
{"Doc: Páginas", "Páginas de documentación", "/app/doc/paginas"},
{"Doc: Categorías", "Categorías de documentación", "/app/doc/categorias"},
}
for _, e := range entries {
var sub models.Submodules
if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil {
sub = models.Submodules{
Title: e.title,
Description: e.desc,
Url: e.url,
ModuleId: modulo.ID,
ModifiedAt: time.Now(),
}
if err := db.Create(&sub).Error; err != nil {
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err)
continue
}
log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID)
}
}
}