Alternativa al ADMIN_API_KEY único de entorno, que sigue funcionando como llave maestra para no romper integraciones existentes. - Modelo ApiKey: token hasheado, IP/CIDR obligatoria (fail-closed sin IP), scopes habilitados, último uso (fecha + IP). - AdminApiAuth() acepta ahora tanto la llave maestra como una ApiKey; nuevo middleware RequireScope(scope) para gatear grupos de rutas. - Fase 1: scopes aplicados a lo más sensible — oss (archivos), query_runner (SQL arbitrario), usuarios (usuarios/roles/módulos), pasarelas (credenciales de pago). El resto de /api/v2 sigue con la llave maestra hasta una fase 2. - Panel /app/api-keys: crear/editar/revocar, token visible solo al crear, scopes por checkbox, IP obligatoria.
220 lines
7.4 KiB
Go
Executable File
220 lines
7.4 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/pyroscope-io/pyroscope/pkg/agent/profiler"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/config"
|
|
"github.com/sujit-baniya/fiber-boilerplate/migrations"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
"github.com/sujit-baniya/fiber-boilerplate/rest/routes"
|
|
"github.com/sujit-baniya/fiber-boilerplate/utils"
|
|
)
|
|
|
|
func main() {
|
|
configFile := flag.String("config", "config.yml", "User Config file from user")
|
|
migrate := flag.Bool("migrate", false, "Update db structure")
|
|
flag.Parse()
|
|
|
|
// Cargar configuración
|
|
app.Load(*configFile)
|
|
|
|
// Configurar Pyroscope profiler
|
|
if app.Http.Profiler.Enabled {
|
|
_, _ = profiler.Start(profiler.Config{
|
|
ApplicationName: app.Http.Server.Name,
|
|
ServerAddress: app.Http.Profiler.Server,
|
|
})
|
|
}
|
|
|
|
// Actualizar versión del servidor
|
|
app.Http.Server.Version = app.Version
|
|
|
|
// Los archivos de /uploads dejan de ser públicos: solo se sirven a usuarios
|
|
// con sesión iniciada en el panel. Los clientes del portal siguen bajando sus
|
|
// documentos por los endpoints dedicados, que validan que les pertenezcan.
|
|
config.UploadsGuard = func(c *fiber.Ctx) bool {
|
|
return auth.IsLoggedIn(c)
|
|
}
|
|
|
|
// Habilitar CORS
|
|
app.Http.Server.App.Use(cors.New(cors.Config{
|
|
AllowOrigins: "*",
|
|
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
|
AllowHeaders: "Content-Type, Authorization",
|
|
}))
|
|
|
|
if *migrate {
|
|
// Ejecutar migraciones
|
|
migrations.Migrate()
|
|
} else {
|
|
// Crear/actualizar tablas de Renovaciones e idempotente)
|
|
migrations.MigrateRenovaciones()
|
|
// Crear/actualizar tablas de Portal, Telegram y Proyectos
|
|
migrations.MigratePortal()
|
|
// Crear tabla de Landing Generator
|
|
migrations.MigrateLanding()
|
|
// Crear tablas de integraciones y pasarelas si no existen.
|
|
// AutoMigrate(a, b, c...) se detiene en el primer modelo que falle: todo lo
|
|
// que viene después en la lista se queda sin tabla, en silencio (fue la
|
|
// causa real de que plantillas_documento/tarifas desaparecieran de
|
|
// producción — un modelo antes en la lista falló y todo lo posterior no se
|
|
// llegó a crear). Migrando uno por uno, un modelo roto no bloquea al resto.
|
|
modelosBase := []interface{}{
|
|
// Tablas base del sistema (columnas nuevas)
|
|
&models.Users{},
|
|
&models.Roles{},
|
|
&models.Submodules{},
|
|
&models.QueryHistory{},
|
|
&models.OssApi{},
|
|
&models.QrVcard{},
|
|
&models.VcfVcard{},
|
|
&models.HostingerConfig{},
|
|
&models.CloudflareConfig{},
|
|
&models.BoldConfig{},
|
|
&models.BoldWebhookLog{},
|
|
&models.BoldCallbackLog{},
|
|
&models.DlocalPaymentLog{},
|
|
// Documentación por SaaS
|
|
&models.SaasProducto{},
|
|
&models.DocCategoria{},
|
|
&models.DocPagina{},
|
|
// Integraciones SaaS (dispatcher de pagos)
|
|
&models.SaasApiConfig{},
|
|
&models.SaasDispatchLog{},
|
|
// Notificaciones
|
|
&models.NotifEventoConfig{},
|
|
&models.SistemaNotificacion{},
|
|
&models.ServidorAlertaUmbral{},
|
|
// Shield
|
|
&models.ShieldConfig{},
|
|
// Partner
|
|
&models.PartnerRecurso{},
|
|
&models.PartnerComunicado{},
|
|
// Portal: recuperación de contraseña
|
|
&models.PortalPasswordResetToken{},
|
|
// Integración VCard API Admin
|
|
&models.VcardApiConfig{},
|
|
// Integración Coolify (multi-instancia)
|
|
&models.CoolifyConfig{},
|
|
// AI Config: campos del agente Telegram
|
|
&models.AiConfig{},
|
|
// Agente Telegram: historial y chats autorizados
|
|
&models.TelegramAgentHistory{},
|
|
&models.TelegramAgentAuth{},
|
|
// Servidores: nuevos campos de agente + tabla join de integraciones
|
|
&models.Servidor{},
|
|
&models.ConxDb{},
|
|
&models.ProvServidor{},
|
|
// SMTP
|
|
&models.SmtpConfig{},
|
|
// WebSMS (LabsMobile)
|
|
&models.WebSmsConfig{},
|
|
&models.WebSmsLog{},
|
|
&models.WebSmsWebhookLog{},
|
|
// Contabilidad
|
|
&models.Cuenta{},
|
|
&models.Entidad{},
|
|
&models.Transaccion{},
|
|
&models.CuentaCobro{},
|
|
&models.CuentaPagar{},
|
|
&models.ConsolidadoMensual{},
|
|
// Historial de métricas del agente
|
|
&models.ServidorMetricasHistory{},
|
|
// Monitor de disponibilidad de URLs
|
|
&models.UrlMonitor{},
|
|
&models.UrlMonitorLog{},
|
|
// Tablero de tareas
|
|
&models.Tarea{},
|
|
&models.TareaComentario{},
|
|
// Pagos externos: API de cobros para aplicaciones de terceros
|
|
&models.ServicioPagoExterno{},
|
|
&models.SolicitudPagoExterna{},
|
|
// Soporte: webhook de correo entrante
|
|
&models.SoporteWebhookConfig{},
|
|
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
|
|
&models.PlantillaDocumento{},
|
|
&models.Tarifa{},
|
|
&models.DocumentoGenerado{},
|
|
&models.Arquitectura{},
|
|
// Vinculación de Telegram para staff interno
|
|
&models.TelegramStaffToken{},
|
|
// uMind: chat con IA embebible por tenant (widget web + RAG)
|
|
&models.UmindTenant{},
|
|
&models.UmindDocumento{},
|
|
&models.UmindChunk{},
|
|
&models.UmindMensaje{},
|
|
// API Keys de /api/v2 (token + IP obligatoria + scopes)
|
|
&models.ApiKey{},
|
|
}
|
|
for _, m := range modelosBase {
|
|
if err := app.Http.Database.DB.AutoMigrate(m); err != nil {
|
|
log.Printf("[MIGRATE] Error migrando %T: %v", m, err)
|
|
}
|
|
}
|
|
// Seed automático (idempotente) de módulos del sistema
|
|
migrations.SeedRenovaciones()
|
|
migrations.SeedIntegraciones()
|
|
migrations.SeedPasarelas()
|
|
migrations.SeedSaas()
|
|
migrations.SeedServidores()
|
|
migrations.SeedAdministracion()
|
|
migrations.SeedPlantillasBase()
|
|
migrations.SeedTelegram()
|
|
migrations.SeedPortalClientes()
|
|
migrations.SeedNotifDefaults()
|
|
migrations.SeedShield()
|
|
migrations.SeedPartnerRecursos()
|
|
models.SeedContabilidad()
|
|
// SeedBalanceData carga transacciones contables reales de 2026. Ya no se
|
|
// ejecuta en cada arranque: si el contador editaba o borraba una de esas
|
|
// filas, el siguiente reinicio la recreaba y el balance quedaba duplicado.
|
|
// Para recargarla puntualmente: SEED_BALANCE=1 ./apiv2
|
|
if os.Getenv("SEED_BALANCE") == "1" {
|
|
log.Println("[SEED] SEED_BALANCE=1 — cargando datos de balance")
|
|
models.SeedBalanceData()
|
|
}
|
|
migrations.SeedContabilidadMenu()
|
|
migrations.SeedWebSms()
|
|
migrations.SeedUrlMonitor()
|
|
migrations.SeedTareas()
|
|
migrations.SeedPagosExternos()
|
|
migrations.SeedAutomatizacionIA()
|
|
migrations.SeedUmind()
|
|
migrations.SeedApiKeys()
|
|
if n, err := models.RepararEstadosTareaInvalidos(); err != nil {
|
|
log.Printf("[FIX] Error reparando estados de tareas: %v", err)
|
|
} else if n > 0 {
|
|
log.Printf("[FIX] %d tarea(s) con estado inválido reparadas (ahora visibles en el Kanban)", n)
|
|
}
|
|
// Cargar config SMTP desde BD (sobreescribe valores del .env/config.yml si hay registro activo)
|
|
if smtpCfg, err := models.GetSmtpConfig(); err == nil {
|
|
app.Http.Mail.Host = smtpCfg.Host
|
|
app.Http.Mail.Port = smtpCfg.Port
|
|
app.Http.Mail.Username = smtpCfg.Username
|
|
app.Http.Mail.Password = utils.Decrypt(smtpCfg.Password, app.Http.Server.Key)
|
|
app.Http.Mail.Encryption = smtpCfg.Encryption
|
|
app.Http.Mail.FromAddress = smtpCfg.FromAddress
|
|
app.Http.Mail.FromName = smtpCfg.FromName
|
|
app.Http.Mail.SetupMailer()
|
|
log.Println("[SMTP] Configuración cargada desde base de datos")
|
|
} else {
|
|
log.Println("[SMTP] Sin config en BD, usando valores del entorno")
|
|
}
|
|
// Iniciar cron de vencimientos
|
|
services.IniciarCron()
|
|
defer services.DetenerCron()
|
|
// Cargar rutas
|
|
routes.LoadRoutes(app.Http.Server.App)
|
|
app.Http.Route404()
|
|
log.Fatal(app.Http.Server.ServeWithGraceFullShutdown())
|
|
}
|
|
}
|