Files
soft_usite/resources/views/soporte_webhook.html
T
Lizandro GDandClaude Sonnet 5 d2bf699b60 fix: seguridad del webhook de soporte, pagos en contabilidad y Telegram para tareas
- soporte: el webhook de correo entrante era público sin ninguna validación;
  ahora exige una API key (query ?key= o header) comparada en tiempo constante.
  Además evita tickets duplicados por reintentos del proveedor (dedup por
  Message-Id) y enhebra respuestas del mismo remitente en vez de abrir un
  ticket nuevo por cada correo.
- contabilidad: marcar una cuenta por cobrar/pagar como pagada ahora crea y
  vincula la Transaccion correspondiente (antes el dashboard de ingresos/
  egresos nunca reflejaba esos pagos). Se corrige además que actualizar una
  cuenta por cobrar borraba su transaccion_id en cada PUT.
- tareas: se activa por defecto el canal Telegram para tarea_asignada (estaba
  apagado desde el seed original) y se agrega un flujo real de vinculación de
  Telegram para el staff interno (código temporal + verificación), igual al
  que ya existía para los usuarios del portal — sin esto el chat_id de cada
  usuario había que pegarlo a mano y la notificación nunca llegaba.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 01:25:17 +00:00

207 lines
9.1 KiB
HTML

<div x-data="soporteWebhook()" x-init="init()" class="p-6 max-w-2xl">
<h1 class="text-xl font-bold text-slate-800 mb-1">Webhook de correo entrante</h1>
<p class="text-sm text-slate-500 mb-6">Configura la integración para recibir correos de soporte@u-s.app y convertirlos automáticamente en tickets.</p>
<div class="bg-white border border-slate-200 rounded-xl p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Proveedor</label>
<select x-model="cfg.provider" class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<option value="sendgrid">SendGrid</option>
<option value="mailgun">Mailgun</option>
<option value="generic">Genérico</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Email destino</label>
<input x-model="cfg.email_destino" type="text" placeholder="soporte@u-s.app"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<p class="text-xs text-slate-400 mt-1">Correo al que llegarán los mensajes. Ej: soporte@u-s.app</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">API Key / Secreto</label>
<div class="flex gap-2">
<input x-model="cfg.api_key" type="text" placeholder="requerido para validar el webhook"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<button type="button" @click="cfg.api_key = generarClave()"
class="px-3 py-2 rounded-lg text-xs font-medium border border-slate-200 text-slate-600 hover:bg-slate-50 whitespace-nowrap">
Generar
</button>
</div>
<p class="text-xs text-slate-400 mt-1">Obligatoria: las peticiones sin esta clave (como <code>?key=...</code>) son rechazadas. Configúrala también en el proveedor si soporta enviarla como parámetro/header.</p>
</div>
<div class="flex items-center gap-3">
<input x-model="cfg.responder_auto" type="checkbox" id="resp-auto"
class="rounded border-slate-300 text-[#8eb02f] focus:ring-[#8eb02f]">
<label for="resp-auto" class="text-sm text-slate-700">Responder automáticamente con acuse de recibo</label>
</div>
<div x-show="cfg.responder_auto">
<label class="block text-sm font-medium text-slate-700 mb-1">Mensaje de auto-respuesta</label>
<textarea x-model="cfg.mensaje_auto" rows="3" placeholder="Hemos recibido tu solicitud y te responderemos a la brevedad."
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Auto-asignar a</label>
<select x-model="cfg.asignar_a" class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<option value="">Sin asignar</option>
<template x-for="a in admins" :key="a.id">
<option :value="a.id" x-text="a.name"></option>
</template>
</select>
<p class="text-xs text-slate-400 mt-1">Los tickets creados por email se asignarán automáticamente a este admin.</p>
</div>
<!-- ─── SMTP salida ──────────────────────────────────────────── -->
<div class="border-t border-slate-100 pt-4 mt-4">
<p class="text-sm font-semibold text-slate-700 mb-3">SMTP salida (notificaciones y auto-respuesta)</p>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-1">
<label class="block text-xs font-medium text-slate-600 mb-1">Host</label>
<input x-model="cfg.smtp_host" type="text" placeholder="smtp.example.com"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
<div class="col-span-1">
<label class="block text-xs font-medium text-slate-600 mb-1">Puerto</label>
<input x-model="cfg.smtp_port" type="number" placeholder="587"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
</div>
<div class="grid grid-cols-2 gap-3 mt-2">
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Usuario</label>
<input x-model="cfg.smtp_username" type="text" placeholder="correo@example.com"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Contraseña</label>
<input x-model="cfg.smtp_password" type="password" placeholder="••••••••"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
</div>
<div class="grid grid-cols-2 gap-3 mt-2">
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Email remitente</label>
<input x-model="cfg.smtp_from_addr" type="text" placeholder="soporte@u-s.app"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
<div>
<label class="block text-xs font-medium text-slate-600 mb-1">Nombre remitente</label>
<input x-model="cfg.smtp_from_name" type="text" placeholder="Soporte U-site"
class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
</div>
</div>
<div class="mt-2">
<label class="block text-xs font-medium text-slate-600 mb-1">Encriptación</label>
<select x-model="cfg.smtp_encryption" class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<option value="tls">TLS (puerto 465)</option>
<option value="starttls">STARTTLS (puerto 587)</option>
<option value="none">Sin encriptación</option>
</select>
</div>
<p class="text-xs text-slate-400 mt-2">Si se deja vacío, se usará la configuración SMTP general del sistema.</p>
</div>
<div class="pt-4">
<button @click="guardar()"
class="px-6 py-2 rounded-xl text-white text-sm font-medium transition-colors"
style="background:#8eb02f"
onmouseover="this.style.background='#6d8c24'" onmouseout="this.style.background='#8eb02f'">
Guardar configuración
</button>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mt-4">
<p class="text-xs font-medium text-slate-600 mb-1">URL del webhook</p>
<p class="text-sm text-slate-800 font-mono break-all" x-text="webhookUrl()"></p>
<p class="text-xs text-slate-400 mt-1">Configura esta URL exacta (con la clave incluida) en el proveedor de correo para enviar los emails entrantes.</p>
</div>
</div>
</div>
<script>
function soporteWebhook() {
return {
cfg: {
id: 0,
nombre: 'Soporte',
provider: 'sendgrid',
email_destino: 'soporte@u-s.app',
api_key: '',
responder_auto: true,
mensaje_auto: '',
asignar_a: '',
smtp_host: '',
smtp_port: 587,
smtp_username: '',
smtp_password: '',
smtp_encryption: 'starttls',
smtp_from_addr: '',
smtp_from_name: '',
},
admins: [],
async init() {
try {
const r = await axios.get('/app/soporte/webhook/data');
if (r.data && r.data.data) {
this.cfg = { ...this.cfg, ...r.data.data };
this.cfg.asignar_a = r.data.data.asignar_a || '';
}
} catch {}
try {
const r = await axios.get('/app/tickets/admins');
this.admins = r.data || [];
} catch {}
if (!this.cfg.api_key) {
this.cfg.api_key = this.generarClave();
}
},
generarClave() {
if (window.crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '');
return Array.from(crypto.getRandomValues(new Uint8Array(24))).map(b => b.toString(16).padStart(2, '0')).join('');
},
webhookUrl() {
const base = window.location.origin + '/webhooks/soporte/' + this.cfg.provider;
return this.cfg.api_key ? `${base}?key=${this.cfg.api_key}` : base;
},
async guardar() {
const payload = {
id: this.cfg.id || 0,
nombre: this.cfg.nombre || 'Soporte',
provider: this.cfg.provider,
email_destino: this.cfg.email_destino,
api_key: this.cfg.api_key,
responder_auto: this.cfg.responder_auto,
mensaje_auto: this.cfg.mensaje_auto,
asignar_a: this.cfg.asignar_a ? parseInt(this.cfg.asignar_a) : null,
smtp_host: this.cfg.smtp_host || '',
smtp_port: parseInt(this.cfg.smtp_port) || 587,
smtp_username: this.cfg.smtp_username || '',
smtp_password: this.cfg.smtp_password || '',
smtp_encryption: this.cfg.smtp_encryption || 'starttls',
smtp_from_addr: this.cfg.smtp_from_addr || '',
smtp_from_name: this.cfg.smtp_from_name || '',
};
try {
await axios.post('/app/soporte/webhook', payload);
alert('Configuración guardada');
} catch (e) {
alert('Error al guardar: ' + (e.response?.data?.error || e.message));
}
},
};
}
</script>