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,68 @@
/**
* Composable para leer información de usuario de Authentik
* Los headers son inyectados por Authentik Proxy Outpost
*/
interface AuthentikUser {
username: string
email: string | undefined
name: string | undefined
groups: string[]
uid: string | undefined
avatar: string
}
export const useAuthentik = () => {
const authentikUser = useState<AuthentikUser | null>('authentikUser', () => {
if (import.meta.server) {
const headers = useRequestHeaders()
const username = headers['x-authentik-username']
const email = headers['x-authentik-email']
const name = headers['x-authentik-name']
const groups = headers['x-authentik-groups']
const uid = headers['x-authentik-uid']
if (!username) {
return null
}
return {
username,
email,
name,
groups: groups ? groups.split('|').filter(g => g.trim()) : [],
uid,
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(name || username)}&background=075e54&color=fff&size=128`
}
}
return null
})
const user = computed(() => authentikUser.value)
const isAuthenticated = computed(() => !!user.value)
const logout = () => {
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
navigateTo(`${authentikUrl}/flows/-/default/invalidation/`, { external: true })
}
const goToProfile = () => {
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
navigateTo(`${authentikUrl}/if/user/`, { external: true, open: { target: '_blank' } })
}
const hasGroup = (groupName: string): boolean => {
if (!user.value) return false
return user.value.groups.includes(groupName)
}
return {
user,
isAuthenticated,
logout,
goToProfile,
hasGroup
}
}

View File

@@ -0,0 +1,124 @@
/**
* Composable for managing WhatsApp instances
*/
export interface Instance {
id: string
name: string
phoneNumber: string | null
status: 'disconnected' | 'connecting' | 'connected' | 'qr_ready' | 'pairing'
qrCode: string | null
pairingCode: string | null
lastConnectedAt: Date | null
createdAt: Date
}
export const useInstances = () => {
const instances = useState<Instance[]>('instances', () => [])
const loading = useState('instancesLoading', () => false)
const error = useState<string | null>('instancesError', () => null)
const fetchInstances = async () => {
loading.value = true
error.value = null
try {
const data = await $fetch<Instance[]>('/api/instances')
instances.value = data
} catch (e) {
error.value = (e as Error).message
console.error('Error fetching instances:', e)
} finally {
loading.value = false
}
}
const createInstance = async (name: string): Promise<Instance | null> => {
try {
const instance = await $fetch<Instance>('/api/instances', {
method: 'POST',
body: { name }
})
instances.value.push(instance)
return instance
} catch (e) {
console.error('Error creating instance:', e)
throw e
}
}
const deleteInstance = async (id: string) => {
try {
await $fetch(`/api/instances/${id}`, { method: 'DELETE' })
instances.value = instances.value.filter(i => i.id !== id)
} catch (e) {
console.error('Error deleting instance:', e)
throw e
}
}
const connectInstance = async (id: string): Promise<{ qrCode: string | null; status: string }> => {
const result = await $fetch<{ qrCode: string | null; status: string }>(`/api/instances/${id}/connect`, {
method: 'POST'
})
// Update local state
const idx = instances.value.findIndex(i => i.id === id)
if (idx !== -1) {
instances.value[idx].status = result.status as Instance['status']
instances.value[idx].qrCode = result.qrCode
}
return result
}
const disconnectInstance = async (id: string) => {
await $fetch(`/api/instances/${id}/disconnect`, { method: 'POST' })
// Update local state
const idx = instances.value.findIndex(i => i.id === id)
if (idx !== -1) {
instances.value[idx].status = 'disconnected'
instances.value[idx].qrCode = null
}
}
const getQRCode = async (id: string): Promise<string | null> => {
const result = await $fetch<{ qrCode: string | null }>(`/api/instances/${id}/qr`)
return result.qrCode
}
const requestPairingCode = async (id: string, phoneNumber: string): Promise<string> => {
const result = await $fetch<{ code: string }>(`/api/instances/${id}/pairing-code`, {
method: 'POST',
body: { phoneNumber }
})
return result.code
}
const getInstanceStatus = async (id: string) => {
return await $fetch(`/api/instances/${id}/status`)
}
// Computed helpers
const connectedCount = computed(() =>
instances.value.filter(i => i.status === 'connected').length
)
const totalCount = computed(() => instances.value.length)
return {
instances,
loading,
error,
fetchInstances,
createInstance,
deleteInstance,
connectInstance,
disconnectInstance,
getQRCode,
requestPairingCode,
getInstanceStatus,
connectedCount,
totalCount
}
}

View File

@@ -0,0 +1,22 @@
/**
* Composable para gestionar el estado del sidebar
*/
export const useSidebarState = () => {
const isOpen = useState('sidebarOpen', () => true)
const isCollapsed = useState('sidebarCollapsed', () => false)
const toggle = () => {
isOpen.value = !isOpen.value
}
const collapse = () => {
isCollapsed.value = !isCollapsed.value
}
return {
isOpen,
isCollapsed,
toggle,
collapse
}
}

View File

@@ -0,0 +1,4 @@
/**
* Re-export useToast from Nuxt UI
*/
export { useToast } from '#imports'