fix: seguridad de pagos y accesos, integración PayPal y Coolify ampliado
Seguridad (crítico): - Los webhooks de Bold y dLocal solo validaban la firma si el atacante la enviaba: sin cabecera se aceptaba cualquier payload. Ahora es obligatoria. - GET /pago-exitoso marcaba contratos como pagados leyendo un query param del navegador. Ahora solo muestra estado; la confirmación la hace la verificación contra la API de la pasarela o el webhook firmado. - /uploads se servía como estático público: se descargaban RUTs, facturas y entregables sabiendo la ruta. Ahora exige sesión. - Los secretos JWT no se podían sobreescribir por entorno (faltaba el tag env:) y su valor estaba en el repo, permitiendo firmarse una sesión de admin. Ahora son configurables y el arranque se detiene si siguen con el valor publicado. - .env y session.db salen del control de versiones. - Query Runner, gestión de usuarios/roles/módulos y seeds quedan restringidos a administradores; antes bastaba con tener sesión. Pasarelas de pago: - dLocal generaba enlaces que nunca se reconciliaban: mandaba el ID numérico en vez de "contrato-N", la URL de retorno apuntaba a la API de dLocal y nunca se enviaba notification_url, así que su webhook jamás se disparaba. - PayPal solo tenía pantalla de configuración. Se implementa el servicio completo (OAuth, orden, captura, verificación de webhook) y queda seleccionable como pasarela. - La moneda estaba fija en COP: un contrato en USD generaba un cobro por esa cifra en pesos. Contratos: - pago_confirmado nunca volvía a false, así que el segundo ciclo de renovación no se cobraba aunque el cliente pagara. Se reinicia al generar enlace nuevo. - Los contratos vencidos nunca cambiaban de estado y recibían correo a diario de forma indefinida; ahora se cierran tras 30 días de gracia. Otros: - Coolify: coolifyCall ignoraba el status HTTP y reportaba errores como éxito. El agente pasa de 10 a cobertura completa (servicios, bases de datos, variables de entorno, proyectos, equipos y recursos de servidor). - SeedBalanceData ya no corre en cada arranque (recreaba transacciones borradas); ahora se invoca con SEED_BALANCE=1. - Los seeds dejan de devolver permisos revocados en cada despliegue. - Timeouts en las llamadas HTTP a Telegram y dLocal que podían colgarse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5607836261
commit
c8e5afceb2
+49
-36
@@ -163,6 +163,43 @@ func Migrate() {
|
||||
log.Println("Migration Completed...")
|
||||
}
|
||||
|
||||
// asignarSubmodulosSiFaltan agrega submódulos a un rol solo si el rol todavía no
|
||||
// tiene NINGUNO de ellos.
|
||||
//
|
||||
// Antes los seeds hacían Append incondicional en cada arranque: si un admin le
|
||||
// quitaba un módulo a un rol desde el panel, el siguiente despliegue se lo
|
||||
// devolvía y la gestión de permisos era, en la práctica, imposible de sostener.
|
||||
func asignarSubmodulosSiFaltan(rol *models.Roles, subs []models.Submodules) {
|
||||
if rol == nil || len(subs) == 0 {
|
||||
return
|
||||
}
|
||||
db := app.Http.Database.DB
|
||||
|
||||
var ids []uint
|
||||
for _, s := range subs {
|
||||
if s.ID > 0 {
|
||||
ids = append(ids, s.ID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var yaAsignados int64
|
||||
db.Table("roles_submodules").
|
||||
Where("roles_id = ? AND submodules_id IN ?", rol.ID, ids).
|
||||
Count(&yaAsignados)
|
||||
if yaAsignados > 0 {
|
||||
// El rol ya conoce este grupo de submódulos: lo que tenga hoy es una
|
||||
// decisión del administrador y no se toca.
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Model(rol).Association("Submodules").Append(&subs); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos al rol %s: %v", rol.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// SeedStatuspage agrega el submódulo "Statuspage" (Atlassian) al módulo "Integraciones"
|
||||
// y lo asigna a todos los roles. Es idempotente.
|
||||
func SeedStatuspage() {
|
||||
@@ -207,11 +244,7 @@ func SeedStatuspage() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulo Statuspage al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulo Statuspage asignado al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de Statuspage completado.")
|
||||
@@ -407,7 +440,7 @@ func SeedRenovaciones() {
|
||||
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
||||
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
||||
} else {
|
||||
db.Model(&rolAdmin).Association("Submodules").Append(&insertados)
|
||||
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
||||
log.Printf("[SEED] Submódulos de Renovaciones asignados al rol 'Administrador'")
|
||||
}
|
||||
|
||||
@@ -481,7 +514,7 @@ func SeedAutomatizacionIA() {
|
||||
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
||||
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
||||
} else {
|
||||
db.Model(&rolAdmin).Association("Submodules").Append(&insertados)
|
||||
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
||||
log.Printf("[SEED] Submódulos de Automatización IA asignados al rol 'Administrador'")
|
||||
}
|
||||
|
||||
@@ -555,11 +588,7 @@ func SeedIntegraciones() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulos de Integraciones asignados al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de Integraciones completado.")
|
||||
@@ -609,11 +638,7 @@ func SeedPasarelas() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulo Pasarelas asignado al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de Pasarelas completado.")
|
||||
@@ -686,11 +711,7 @@ func SeedServidores() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulos de Servidores asignados al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de Servidores completado.")
|
||||
@@ -757,11 +778,7 @@ func SeedAdministracion() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulos de Administración asignados al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de Administración completado.")
|
||||
@@ -812,11 +829,7 @@ func SeedSaas() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos SaaS al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulos SaaS asignados al rol '%s'", rol.Name)
|
||||
}
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de SaaS completado.")
|
||||
@@ -888,7 +901,7 @@ func SeedTelegram() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
db.Model(&rol).Association("Submodules").Append(&sub)
|
||||
asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub})
|
||||
}
|
||||
log.Println("[SEED] Seed de Telegram completado.")
|
||||
}
|
||||
@@ -946,7 +959,7 @@ func SeedPortalClientes() {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
db.Model(&rol).Association("Submodules").Append(&insertados)
|
||||
asignarSubmodulosSiFaltan(&rol, insertados)
|
||||
}
|
||||
log.Println("[SEED] Seed de Portal de Clientes completado.")
|
||||
}
|
||||
@@ -1030,7 +1043,7 @@ func SeedPartnerRecursos() {
|
||||
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
||||
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
||||
} else {
|
||||
db.Model(&rolAdmin).Association("Submodules").Append(&sub)
|
||||
asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub})
|
||||
log.Printf("[SEED] Submódulo 'Partner Recursos' asignado al rol 'Administrador'")
|
||||
}
|
||||
log.Println("[SEED] Seed de Partner Recursos completado.")
|
||||
@@ -1084,7 +1097,7 @@ func SeedContabilidadMenu() {
|
||||
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
||||
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
||||
} else {
|
||||
db.Model(&rolAdmin).Association("Submodules").Append(&insertados)
|
||||
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
||||
log.Printf("[SEED] Submódulos de Contabilidad asignados al rol 'Administrador'")
|
||||
}
|
||||
log.Println("[SEED] Seed de Contabilidad completado.")
|
||||
|
||||
Reference in New Issue
Block a user