Revert "telegram"

This reverts commit 46daf3d4aa.
This commit is contained in:
Lizandro Guarnizo
2026-05-25 12:50:53 -05:00
parent 7b1c260af5
commit 58d98f23aa
6 changed files with 101 additions and 80 deletions
+5
View File
@@ -87,9 +87,12 @@ func Migrate() {
// Sistema de notificaciones por evento
&models.NotifEventoConfig{},
&models.SistemaNotificacion{},
&models.ServidorAlertaUmbral{},
// Submódulo Partner
&models.PartnerRecurso{},
&models.PartnerComunicado{},
// Integración VCard API Admin (Laravel)
&models.VcardApiConfig{},
); err != nil {
log.Fatalf("Error during main migration: %v", err)
}
@@ -422,6 +425,7 @@ func SeedIntegraciones() {
entries := []struct{ title, desc, url string }{
{"Hostinger", "Panel de VPS, dominios, hosting y DNS de Hostinger", "/app/hostinger"},
{"Cloudflare", "Gestión de zonas, DNS, SSL y firewall en Cloudflare", "/app/cloudflare"},
{"VCard API", "Integración con el sistema VCard externo (Laravel + Sanctum): usuarios, membresías, vcards, pagos y más", "/app/vcard-api"},
}
var insertados []models.Submodules
@@ -736,6 +740,7 @@ func MigratePortal() {
&models.ProyectoFase{},
&models.ProyectoAvance{},
&models.ProyectoEntregable{},
&models.ProyectoDocumento{},
&models.ProyectoTicket{},
&models.TicketMensaje{},
&models.PortalUser{},
+26 -3
View File
@@ -75,8 +75,12 @@
<p class="text-sm text-slate-600 mt-1" x-text="av.contenido"></p>
</div>
<div class="flex gap-1 ml-4">
<button @click="toggleAvanceVisible(av)" :title="av.visible ? 'Ocultar' : 'Publicar'" class="btn-icon" :class="av.visible ? 'text-green-500' : 'text-slate-400'">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<button @click="toggleAvanceVisible(av)"
:title="av.visible ? 'Visible en portal · clic para ocultar' : 'Oculto en portal · clic para publicar'"
class="btn-icon flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium border transition-colors"
:class="av.visible ? 'text-green-600 border-green-200 bg-green-50 hover:bg-green-100' : 'text-slate-400 border-slate-200 bg-slate-50 hover:bg-slate-100'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span x-text="av.visible ? 'Visible' : 'Oculto'"></span>
</button>
<button @click="deleteAvance(av.ID)" class="btn-icon text-red-400"><svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button>
</div>
@@ -306,6 +310,17 @@
</div>
</div>
<!-- Toast -->
<div x-show="toast.show" x-cloak x-transition
class="fixed bottom-6 right-6 z-50 px-5 py-3 rounded-xl shadow-lg text-white text-sm font-medium flex items-center gap-2"
:class="toast.type === 'error' ? 'bg-red-500' : 'bg-[#8eb02f]'">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path x-show="toast.type !== 'error'" stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
<path x-show="toast.type === 'error'" stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span x-text="toast.msg"></span>
</div>
</div>
<script>
@@ -325,6 +340,7 @@ function proyectoDetalle(proyectoId, slug) {
showDocumentoModal: false,
documentoForm: { tipo:'contrato', nombre:'', descripcion:'' },
documentoFile: null, documentoError:'',
toast: { show: false, msg: '', type: 'ok' },
async init() {
await Promise.all([this.loadFases(), this.loadAvances(), this.loadEntregables(), this.loadTickets(), this.loadDocumentos()]);
@@ -390,8 +406,10 @@ function proyectoDetalle(proyectoId, slug) {
await this.loadAvances();
},
async toggleAvanceVisible(av) {
await axios.put(`/app/proyectos/${this.proyectoId}/avances/${av.ID}`, {...av, visible:!av.visible});
const nuevoEstado = !av.visible;
await axios.put(`/app/proyectos/${this.proyectoId}/avances/${av.ID}`, {...av, visible:nuevoEstado});
await this.loadAvances();
this.showToast(nuevoEstado ? 'Avance visible en el portal del cliente' : 'Avance oculto en el portal del cliente');
},
// ─── Entregables ───
@@ -462,6 +480,11 @@ function proyectoDetalle(proyectoId, slug) {
t._reply='';
},
showToast(msg, type = 'ok') {
this.toast = { show: true, msg, type };
setTimeout(() => { this.toast.show = false; }, 3000);
},
// ─── Helpers ───
faseBadgeClass(e) { return {pendiente:'badge-slate',en_progreso:'badge-yellow',completado:'badge-green',bloqueado:'badge-red'}[e]||'badge-slate'; },
avanceBadgeClass(t) { return {update:'badge-blue',milestone:'badge-green',nota:'badge-slate',alerta:'badge-yellow'}[t]||'badge-slate'; },
-33
View File
@@ -55,12 +55,6 @@
<span x-show="!webhookLoading[cfg.ID]">🔗 Webhook portal</span>
<span x-show="webhookLoading[cfg.ID]">Registrando…</span>
</button>
<button @click="checkWebhook(cfg)" :disabled="webhookInfoLoading[cfg.ID]"
class="flex-1 text-xs border border-slate-300 text-slate-600 px-3 py-1.5 rounded hover:bg-slate-50 transition-colors disabled:opacity-50"
title="Consulta getWebhookInfo en Telegram para este bot">
<span x-show="!webhookInfoLoading[cfg.ID]">📡 Estado webhook</span>
<span x-show="webhookInfoLoading[cfg.ID]">Consultando…</span>
</button>
<button @click="openEdit(cfg)"
class="text-xs border border-blue-400 text-blue-500 px-3 py-1.5 rounded hover:bg-blue-50">Editar</button>
<button @click="confirmDelete(cfg.ID)"
@@ -234,7 +228,6 @@ function telegramApp() {
form: { nombre: '', bot_token: '', chat_id: '', notas: '', activo: true },
testLoading: {}, testResult: {}, testOk: {},
webhookLoading: {}, webhookResult: {}, webhookOk: {},
webhookInfoLoading: {},
sendForm: { config_ids: [], titulo: '', mensaje: '' },
sendLoading: false, sendResult: '', sendOk: true,
logs: [], logPage: 1, logTotalPages: 1, logTotal: 0,
@@ -319,32 +312,6 @@ function telegramApp() {
this.webhookLoading[cfg.ID] = false;
}
},
async checkWebhook(cfg) {
this.webhookInfoLoading[cfg.ID] = true;
this.webhookResult[cfg.ID] = '';
try {
const res = await fetch(`/app/telegram/${cfg.ID}/webhook-info`);
const d = await res.json();
const info = d.result || {};
const hasPortalURL = (info.url || '').includes('/webhooks/telegram-portal');
const hasError = !!(info.last_error_message || info.last_synchronization_error_date);
this.webhookOk[cfg.ID] = !!d.ok && hasPortalURL && !hasError;
const parts = [];
if (info.url) parts.push(`URL: ${info.url}`);
if (typeof info.pending_update_count !== 'undefined') parts.push(`Pendientes: ${info.pending_update_count}`);
if (info.last_error_message) parts.push(`Último error: ${info.last_error_message}`);
if (!parts.length) parts.push(d.description || 'Sin datos de webhook');
this.webhookResult[cfg.ID] = (this.webhookOk[cfg.ID] ? '✅ ' : '⚠️ ') + parts.join(' | ');
} catch (e) {
this.webhookOk[cfg.ID] = false;
this.webhookResult[cfg.ID] = '❌ Error consultando webhook: ' + e.message;
} finally {
this.webhookInfoLoading[cfg.ID] = false;
}
},
async sendMessage() {
this.sendResult = '';
+8 -42
View File
@@ -822,12 +822,6 @@ func PortalTelegramValidar(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
}
// Si ya está vinculado (por ejemplo, vía webhook), responder éxito de inmediato.
full, err := models.GetPortalUserByID(u.ID)
if err == nil && full != nil && strings.TrimSpace(full.TelegramChatID) != "" {
return c.JSON(fiber.Map{"ok": true, "chat_id": full.TelegramChatID})
}
// Recuperar el token vigente del usuario
tkn, err := models.GetTelegramPortalTokenByUser(u.ID)
if err != nil || tkn == nil {
@@ -835,15 +829,11 @@ func PortalTelegramValidar(c *fiber.Ctx) error {
}
configs, _ := models.GetAllTelegramConfigs()
hadWebhookConflict := false
for _, cfg := range configs {
if !cfg.Activo || cfg.BotToken == "" {
continue
}
chatID, found, reason := searchTokenInUpdates(cfg.BotToken, tkn.Token)
if reason == "webhook_conflict" {
hadWebhookConflict = true
}
chatID, found := searchTokenInUpdates(cfg.BotToken, tkn.Token)
if !found {
continue
}
@@ -858,36 +848,22 @@ func PortalTelegramValidar(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true, "chat_id": fmt.Sprintf("%d", chatID)})
}
// Revalidar por si el webhook hizo la vinculación mientras corría esta petición.
full, err = models.GetPortalUserByID(u.ID)
if err == nil && full != nil && strings.TrimSpace(full.TelegramChatID) != "" {
return c.JSON(fiber.Map{"ok": true, "chat_id": full.TelegramChatID})
}
if hadWebhookConflict {
return c.Status(200).JSON(fiber.Map{
"ok": false,
"error": "El bot está en modo webhook. Envía /vincular " + tkn.Token + " al bot y espera unos segundos; luego vuelve a verificar.",
})
}
return c.Status(200).JSON(fiber.Map{"ok": false, "error": "No se encontró el mensaje. Asegúrate de enviar /vincular " + tkn.Token + " al bot."})
}
// searchTokenInUpdates llama getUpdates y busca un mensaje que contenga el token.
// Retorna el chat_id y true si lo encuentra.
func searchTokenInUpdates(botToken, token string) (int64, bool, string) {
func searchTokenInUpdates(botToken, token string) (int64, bool) {
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?limit=100", botToken)
resp, err := http.Get(apiURL) //nolint:noctx
if err != nil {
log.Printf("[searchTokenInUpdates] error getUpdates: %v", err)
return 0, false, "network_error"
return 0, false
}
defer resp.Body.Close()
var result struct {
OK bool `json:"ok"`
Description string `json:"description"`
OK bool `json:"ok"`
Result []struct {
Message struct {
Chat struct {
@@ -897,18 +873,8 @@ func searchTokenInUpdates(botToken, token string) (int64, bool, string) {
} `json:"message"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, false, "decode_error"
}
if !result.OK {
desc := strings.ToLower(strings.TrimSpace(result.Description))
if strings.Contains(desc, "can't use getupdates method while webhook is active") {
return 0, false, "webhook_conflict"
}
if desc != "" {
log.Printf("[searchTokenInUpdates] Telegram API !ok: %s", result.Description)
}
return 0, false, "api_not_ok"
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK {
return 0, false
}
upperToken := strings.ToUpper(token)
@@ -916,8 +882,8 @@ func searchTokenInUpdates(botToken, token string) (int64, bool, string) {
txt := strings.ToUpper(strings.TrimSpace(upd.Message.Text))
if strings.Contains(txt, upperToken) && upd.Message.Chat.ID != 0 {
log.Printf("[searchTokenInUpdates] token=%s chatID=%d", token, upd.Message.Chat.ID)
return upd.Message.Chat.ID, true, ""
return upd.Message.Chat.ID, true
}
}
return 0, false, ""
return 0, false
}
+17 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"mime/multipart"
"net/url"
"os"
"path/filepath"
"strconv"
@@ -491,10 +492,25 @@ func DownloadDocumento(c *fiber.Ctx) error {
if !strings.HasPrefix(clean, "uploads/") {
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
}
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName))
nombre := item.OriginalName
if nombre == "" {
nombre = item.Nombre
}
c.Set("Content-Disposition", attachmentDisposition(nombre))
return c.SendFile(clean)
}
// attachmentDisposition genera un header Content-Disposition con fallback ASCII y encoding RFC 5987.
func attachmentDisposition(name string) string {
safe := strings.Map(func(r rune) rune {
if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
return '_'
}
return r
}, name)
return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(name))
}
// ─── Tickets (admin) ──────────────────────────────────────────────────────────
func GetTickets(c *fiber.Ctx) error {
+45 -1
View File
@@ -21,7 +21,7 @@ func UserRoutes(app fiber.Router) {
// Rutas de la aplicación
// web me redireccione a /
// web me redireccione a /
protected.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
app.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
protected.Get("/", controllers.App)
@@ -71,17 +71,23 @@ func UserRoutes(app fiber.Router) {
// Rutas de servidor
protected.Get("/servidor", middlewares.MenuMiddleware, controllers.Servidor)
protected.Get("/servidor-dashboard", middlewares.MenuMiddleware, controllers.ServidorDashboard)
protected.Get("/loadservidorselect", controllers.GetServidor)
protected.Get("/loadservidor", controllers.GetServidor)
protected.Post("/servidor", controllers.CreateServidor)
protected.Put("/servidor/:id", controllers.UpdateServidor)
protected.Delete("/servidor/:id", controllers.DeleteServidor)
protected.Get("/servidor-dashboard/:id", controllers.GetServidorDashboard)
protected.Get("/conx-ping/:id", controllers.PingConexion)
protected.Post("/servidor/:id/agent-token", controllers.GenerateAgentToken)
protected.Post("/servidor/:id/sync-hostinger", controllers.SyncServidorFromHostinger)
// Rutas de proveedores de servidor
protected.Get("/prov_servidor", middlewares.MenuMiddleware, controllers.ProvServidor)
protected.Get("/loadprovservidor", controllers.GetProvServidor)
protected.Post("/provservidor", controllers.CreateProvServidor)
protected.Put("/provservidor/:id", controllers.UpdateProvServidor)
protected.Put("/provservidor/:id/integraciones", controllers.SetProvServidorIntegraciones)
protected.Delete("/provservidor/:id", controllers.DeleteProvServidor)
// Rutas de tipos de servidor
@@ -215,6 +221,41 @@ func UserRoutes(app fiber.Router) {
protected.Get("/saas-api/logs", middlewares.MenuMiddleware, controllers.SaasDispatchLogIndex)
protected.Get("/loadsaasdispatchlogs", controllers.GetSaasDispatchLogs)
// ─── VCard API (integración Admin Laravel) ────────────────────────────────
protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex)
protected.Post("/vcard-api/login", controllers.VcardApiLogin)
protected.Get("/vcard-api/config", controllers.VcardApiGetConfig)
protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig)
// Usuarios (GET)
protected.Get("/vcard-api/usuarios", controllers.VcardApiUsuarios)
protected.Get("/vcard-api/usuarios/:id/membresia", controllers.VcardApiMembresia)
protected.Get("/vcard-api/usuarios/:userId/vcards", controllers.VcardApiVcardsByUsuario)
protected.Get("/vcard-api/usuarios/:userId/pagos", controllers.VcardApiPagosByUsuario)
protected.Get("/vcard-api/usuarios/:userId/transacciones", controllers.VcardApiTransaccionesByUsuario)
protected.Get("/vcard-api/usuarios/:userId/logs", controllers.VcardApiLogsByUsuario)
protected.Get("/vcard-api/usuarios/:userId/miniwebs", controllers.VcardApiMiniwebsByUsuario)
protected.Get("/vcard-api/usuarios/:id", controllers.VcardApiUsuario)
// Usuarios (mutaciones)
protected.Put("/vcard-api/usuarios/:id", controllers.VcardApiUsuarioUpdate)
protected.Post("/vcard-api/usuarios/:id/activar", controllers.VcardApiUsuarioActivar)
protected.Post("/vcard-api/usuarios/:id/desactivar", controllers.VcardApiUsuarioDesactivar)
protected.Post("/vcard-api/usuarios/:id/activar-membresia", controllers.VcardApiActivarMembresia)
protected.Post("/vcard-api/usuarios/:id/desactivar-membresia", controllers.VcardApiDesactivarMembresia)
protected.Post("/vcard-api/usuarios/:id/cambiar-plan", controllers.VcardApiCambiarPlan)
// VCards
protected.Get("/vcard-api/vcards", controllers.VcardApiVcards)
protected.Get("/vcard-api/vcards/:id", controllers.VcardApiVcard)
protected.Put("/vcard-api/vcards/:id", controllers.VcardApiVcardUpdate)
// Resto (solo GET)
protected.Get("/vcard-api/planes", controllers.VcardApiPlanes)
protected.Get("/vcard-api/planes/:id", controllers.VcardApiPlan)
protected.Put("/vcard-api/planes/:id", controllers.VcardApiPlanUpdate)
protected.Get("/vcard-api/pagos", controllers.VcardApiPagos)
protected.Get("/vcard-api/transacciones", controllers.VcardApiTransacciones)
protected.Get("/vcard-api/logs", controllers.VcardApiLogs)
protected.Get("/vcard-api/miniwebs", controllers.VcardApiMiniwebs)
protected.Get("/vcard-api/miniwebs/:id", controllers.VcardApiMiniweb)
// ─── Telegram ─────────────────────────────────────────────────────────────
protected.Get("/telegram", middlewares.MenuMiddleware, controllers.TelegramIndex)
protected.Get("/loadtelegram", controllers.GetTelegramConfigs)
@@ -305,6 +346,9 @@ func UserRoutes(app fiber.Router) {
protected.Get("/notif-config", middlewares.MenuMiddleware, controllers.NotifConfigIndex)
protected.Get("/notif-config/data", controllers.GetNotifConfigs)
protected.Post("/notif-config", controllers.SaveNotifConfig)
// Umbrales de alerta de servidores
protected.Get("/servidor-alerta-config", controllers.GetServidorAlertaConfig)
protected.Post("/servidor-alerta-config", controllers.SaveServidorAlertaConfig)
// Partner Recursos (documentación y comunicados para partners)
protected.Get("/partner-recursos", middlewares.MenuMiddleware, controllers.PartnerRecursosIndex)