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>
113 lines
3.4 KiB
Go
Executable File
113 lines
3.4 KiB
Go
Executable File
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/ilyakaznacheev/cleanenv"
|
|
"github.com/joho/godotenv"
|
|
"github.com/oarkflow/log"
|
|
"github.com/sujit-baniya/flash"
|
|
)
|
|
|
|
// Config is a application configuration structure
|
|
type AppConfig struct {
|
|
Auth AuthConfig `yaml:"auth"`
|
|
Mail Mail `yaml:"mail"`
|
|
Hash Hash
|
|
View ViewConfig `yaml:"view"`
|
|
Cache CacheConfig `yaml:"cache"`
|
|
Database DatabaseConfig `yaml:"database"`
|
|
Session SessionConfig `yaml:"session"`
|
|
Queue QueueConfig `yaml:"queue"`
|
|
JwtSecrets JwtSecrets `yaml:"jwt"`
|
|
Storage StorageConfig `yaml:"storage"`
|
|
Server ServerConfig `yaml:"server"`
|
|
Log LogConfig `yaml:"log"`
|
|
Token Token `yaml:"token"`
|
|
Profiler ProfilerConfig `yaml:"profiler"`
|
|
Flash *flash.Flash
|
|
ConfigFile string
|
|
}
|
|
|
|
func (cfg *AppConfig) Setup() {
|
|
err := godotenv.Load()
|
|
// read configuration from the file and environment variables
|
|
if err = cleanenv.ReadConfig(cfg.ConfigFile, cfg); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(2)
|
|
}
|
|
cfg.VerificarSecretos()
|
|
cfg.Server.LoadPath()
|
|
cfg.View.Load(cfg.Server.Path)
|
|
cfg.Mail.View = &cfg.View
|
|
cfg.Server.TemplateEngine = cfg.View.Template.TemplateEngine
|
|
cfg.Server.Setup()
|
|
cfg.LoadComponents()
|
|
if cfg.Auth.Type == "casbin" {
|
|
modelFile := filepath.Join(cfg.Server.AssetPath, "rbac_model.conf")
|
|
cfg.Auth.Setup(cfg.Database.DB, modelFile)
|
|
}
|
|
}
|
|
|
|
func (cfg *AppConfig) PrepareLog() {
|
|
writer := &log.MultiWriter{}
|
|
path := MakeDir(filepath.Join(cfg.Server.Path, cfg.Log.InfoLevel.Path))
|
|
writer.InfoWriter = &log.FileWriter{Filename: filepath.Join(path, "INFO.log"), EnsureFolder: true, TimeFormat: cfg.Log.InfoLevel.TimeFormat}
|
|
|
|
path = MakeDir(filepath.Join(cfg.Server.Path, cfg.Log.WarnLevel.Path))
|
|
writer.WarnWriter = &log.FileWriter{Filename: filepath.Join(cfg.Server.Path, cfg.Log.WarnLevel.Path, "WARN.log"), EnsureFolder: true, TimeFormat: cfg.Log.WarnLevel.TimeFormat}
|
|
|
|
path = MakeDir(filepath.Join(cfg.Server.Path, cfg.Log.ErrorLevel.Path))
|
|
writer.ErrorWriter = &log.FileWriter{Filename: filepath.Join(cfg.Server.Path, cfg.Log.ErrorLevel.Path, "ERROR.log"), EnsureFolder: true, TimeFormat: cfg.Log.ErrorLevel.TimeFormat}
|
|
if cfg.Log.ConsoleLog.Show {
|
|
writer.ConsoleWriter = &log.IOWriter{Writer: os.Stderr}
|
|
writer.ConsoleLevel = log.InfoLevel
|
|
}
|
|
log.DefaultLogger = log.Logger{
|
|
TimeField: cfg.Log.TimeField,
|
|
TimeFormat: cfg.Log.TimeFormat,
|
|
Writer: writer,
|
|
}
|
|
}
|
|
|
|
func (cfg *AppConfig) Route404() {
|
|
cfg.Server.Use(func(c *fiber.Ctx) error {
|
|
return c.Status(fiber.StatusNotFound).SendString("Page not found")
|
|
})
|
|
}
|
|
|
|
func (cfg *AppConfig) LoadComponents() {
|
|
cfg.Flash = flash.New(flash.Config{
|
|
Name: "fiber",
|
|
HTTPOnly: true,
|
|
})
|
|
cfg.LoadStatic()
|
|
cfg.PrepareLog()
|
|
_ = cfg.Database.Setup()
|
|
_ = cfg.Session.Setup(cfg.Database.Default)
|
|
cfg.Cache.Setup()
|
|
cfg.Storage.Setup()
|
|
}
|
|
|
|
func (cfg *AppConfig) LoadStatic() {
|
|
cfg.Server.Static("/websocket", "./resources/views/websocket.html")
|
|
// El guard se registra antes que el estático para que corra primero.
|
|
cfg.Server.Use("/uploads", uploadsProtegidos)
|
|
cfg.Server.Static("/uploads", "./uploads", fiber.Static{
|
|
ByteRange: true,
|
|
})
|
|
cfg.Server.Static("/", filepath.Join(cfg.Server.Path, cfg.Server.PublicPath), fiber.Static{
|
|
Compress: true,
|
|
ByteRange: true,
|
|
CacheDuration: 24 * time.Hour,
|
|
})
|
|
}
|
|
|
|
func (cfg *AppConfig) LoadSpamDetectionEngine() {
|
|
|
|
}
|