feat: WhatsApp Nucleo con Nuxt 4 + Baileys v7
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s

Reemplazo completo de Evolution API por implementación directa con Baileys.

Características:
- Dashboard completo con Nuxt UI v4
- Soporte para múltiples instancias de WhatsApp
- Conexión via QR code o pairing code
- Persistencia de mensajes en PostgreSQL
- API REST para integraciones externas
- Webhooks con firma HMAC
- SSE para actualizaciones en tiempo real
- Autenticación con Authentik
This commit is contained in:
2025-12-02 17:54:31 -06:00
parent 327118440b
commit faedec47d7
62 changed files with 4489 additions and 92 deletions

View File

@@ -0,0 +1,91 @@
<template>
<UModal v-model:open="isOpen">
<template #content>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-[var(--wa-text)]">Nueva Instancia</h3>
<UButton
variant="ghost"
icon="i-lucide-x"
@click="isOpen = false"
/>
</div>
</template>
<form @submit.prevent="handleSubmit">
<div class="space-y-4">
<UFormField label="Nombre de la instancia" required>
<UInput
v-model="form.name"
placeholder="Ej: Ventas, Soporte, Marketing..."
/>
</UFormField>
<div class="text-sm text-[var(--wa-text-muted)]">
<p>Despues de crear la instancia, podras conectarla escaneando un codigo QR con WhatsApp.</p>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<UButton
type="button"
variant="ghost"
@click="isOpen = false"
>
Cancelar
</UButton>
<UButton
type="submit"
:loading="loading"
:disabled="!form.name.trim()"
>
Crear Instancia
</UButton>
</div>
</form>
</UCard>
</template>
</UModal>
</template>
<script setup lang="ts">
const props = defineProps<{
open: boolean
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
created: [instance: any]
}>()
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value)
})
const loading = ref(false)
const form = ref({
name: ''
})
const handleSubmit = async () => {
if (!form.value.name.trim()) return
loading.value = true
try {
const instance = await $fetch('/api/instances', {
method: 'POST',
body: { name: form.value.name }
})
emit('created', instance)
form.value.name = ''
} catch (error) {
console.error('Error creating instance:', error)
// TODO: Show error toast
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,117 @@
<template>
<div class="instance-card p-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-[var(--wa-green-dark)] flex items-center justify-center">
<UIcon name="i-lucide-smartphone" class="w-6 h-6 text-white" />
</div>
<div>
<h3 class="font-semibold text-[var(--wa-text)]">{{ instance.name }}</h3>
<p class="text-sm text-[var(--wa-text-muted)]">
{{ instance.phoneNumber || 'Sin numero asignado' }}
</p>
</div>
</div>
<StatusBadge :status="instance.status" />
</div>
<!-- Connection info -->
<div v-if="instance.lastConnectedAt" class="text-xs text-[var(--wa-text-muted)] mb-4">
Ultima conexion: {{ formatDate(instance.lastConnectedAt) }}
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<template v-if="instance.status === 'disconnected'">
<UButton
size="sm"
icon="i-lucide-qr-code"
@click="$emit('connect', instance.id)"
>
Conectar
</UButton>
</template>
<template v-else-if="instance.status === 'connected'">
<UButton
size="sm"
variant="soft"
icon="i-lucide-message-square"
:to="`/messages?instance=${instance.id}`"
>
Mensajes
</UButton>
<UButton
size="sm"
variant="ghost"
color="red"
icon="i-lucide-unplug"
@click="$emit('disconnect', instance.id)"
>
Desconectar
</UButton>
</template>
<template v-else-if="instance.status === 'qr_ready'">
<UButton
size="sm"
icon="i-lucide-qr-code"
@click="$emit('connect', instance.id)"
>
Ver QR
</UButton>
</template>
<template v-else>
<UButton
size="sm"
variant="ghost"
loading
>
Procesando...
</UButton>
</template>
<!-- Delete button -->
<UButton
size="sm"
variant="ghost"
color="red"
icon="i-lucide-trash-2"
class="ml-auto"
@click="confirmDelete"
/>
</div>
</div>
</template>
<script setup lang="ts">
interface Instance {
id: string
name: string
phoneNumber: string | null
status: 'connected' | 'disconnected' | 'connecting' | 'qr_ready' | 'pairing'
lastConnectedAt: Date | null
}
interface Props {
instance: Instance
}
defineProps<Props>()
const emit = defineEmits<{
connect: [instanceId: string]
disconnect: [instanceId: string]
delete: [instanceId: string]
}>()
const formatDate = (date: Date) => {
return new Date(date).toLocaleString('es-AR')
}
const confirmDelete = () => {
// TODO: Show confirmation modal
emit('delete', props.instance.id)
}
</script>

View File

@@ -0,0 +1,111 @@
<template>
<UModal v-model:open="isOpen" :ui="{ width: 'max-w-md' }">
<template #content>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-[var(--wa-text)]">Escanear Codigo QR</h3>
<UButton
variant="ghost"
icon="i-lucide-x"
@click="isOpen = false"
/>
</div>
</template>
<div class="text-center">
<!-- QR Code Display -->
<div v-if="qrCode" class="mb-4">
<div class="bg-white p-4 rounded-lg inline-block">
<img :src="qrCode" alt="QR Code" class="w-64 h-64" />
</div>
</div>
<div v-else class="mb-4 py-16">
<UIcon
name="i-lucide-loader-2"
class="w-12 h-12 text-[var(--wa-text-muted)] animate-spin mx-auto"
/>
<p class="text-[var(--wa-text-muted)] mt-4">Generando codigo QR...</p>
</div>
<!-- Instructions -->
<div class="text-sm text-[var(--wa-text-muted)] space-y-2">
<p>1. Abre WhatsApp en tu telefono</p>
<p>2. Ve a Configuracion > Dispositivos vinculados</p>
<p>3. Toca "Vincular un dispositivo"</p>
<p>4. Escanea este codigo QR</p>
</div>
<!-- Alternative: Pairing Code -->
<div class="mt-6 pt-6 border-t border-[var(--wa-border)]">
<p class="text-sm text-[var(--wa-text-muted)] mb-3">
O usa un codigo de emparejamiento
</p>
<UButton
variant="soft"
@click="requestPairingCode"
:loading="loadingPairing"
>
Obtener Codigo
</UButton>
<div v-if="pairingCode" class="mt-4">
<p class="text-2xl font-mono font-bold text-[var(--wa-green-light)] tracking-widest">
{{ pairingCode }}
</p>
<p class="text-xs text-[var(--wa-text-muted)] mt-2">
Ingresa este codigo en WhatsApp > Dispositivos vinculados > Vincular con numero de telefono
</p>
</div>
</div>
</div>
</UCard>
</template>
</UModal>
</template>
<script setup lang="ts">
interface Props {
open: boolean
instanceId: string | null
qrCode: string | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value)
})
const loadingPairing = ref(false)
const pairingCode = ref<string | null>(null)
const requestPairingCode = async () => {
if (!props.instanceId) return
loadingPairing.value = true
try {
const response = await $fetch<{ code: string }>(`/api/instances/${props.instanceId}/pairing-code`, {
method: 'POST'
})
pairingCode.value = response.code
} catch (error) {
console.error('Error requesting pairing code:', error)
} finally {
loadingPairing.value = false
}
}
// Reset pairing code when modal closes
watch(isOpen, (value) => {
if (!value) {
pairingCode.value = null
}
})
</script>