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,22 @@
/**
* DELETE /api/webhooks/:id
* Delete a webhook
*/
import { query } from '../../../utils/database'
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const id = getRouterParam(event, 'id')
const result = await query('DELETE FROM webhooks WHERE id = $1 RETURNING id', [id])
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Webhook not found' })
}
return { success: true }
})

View File

@@ -0,0 +1,72 @@
/**
* PUT /api/webhooks/:id
* Update a webhook
*/
import { query } from '../../../utils/database'
interface UpdateWebhookBody {
name?: string
url?: string
secret?: string
events?: string[]
instanceId?: string | null
isActive?: boolean
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const id = getRouterParam(event, 'id')
const body = await readBody<UpdateWebhookBody>(event)
// Check if webhook exists
const existing = await query('SELECT id FROM webhooks WHERE id = $1', [id])
if (existing.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Webhook not found' })
}
// Build update query dynamically
const updates: string[] = []
const values: any[] = []
let paramIndex = 1
if (body.name !== undefined) {
updates.push(`name = $${paramIndex++}`)
values.push(body.name)
}
if (body.url !== undefined) {
updates.push(`url = $${paramIndex++}`)
values.push(body.url)
}
if (body.secret !== undefined) {
updates.push(`secret = $${paramIndex++}`)
values.push(body.secret || null)
}
if (body.events !== undefined) {
updates.push(`events = $${paramIndex++}`)
values.push(body.events)
}
if (body.instanceId !== undefined) {
updates.push(`instance_id = $${paramIndex++}`)
values.push(body.instanceId || null)
}
if (body.isActive !== undefined) {
updates.push(`is_active = $${paramIndex++}`)
values.push(body.isActive)
}
if (updates.length === 0) {
throw createError({ statusCode: 400, message: 'No fields to update' })
}
values.push(id)
await query(
`UPDATE webhooks SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${paramIndex}`,
values
)
return { success: true }
})

View File

@@ -0,0 +1,100 @@
/**
* POST /api/webhooks/:id/test
* Test a webhook with a sample payload
*/
import { query } from '../../../utils/database'
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const id = getRouterParam(event, 'id')
// Get webhook
const result = await query<{ url: string; secret: string | null; headers: any }>(
'SELECT url, secret, headers FROM webhooks WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Webhook not found' })
}
const webhook = result.rows[0]
// Create test payload
const testPayload = {
event: 'test',
timestamp: new Date().toISOString(),
data: {
message: 'This is a test webhook delivery',
webhookId: id,
sentBy: username
}
}
const body = JSON.stringify(testPayload)
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Webhook-Event': 'test',
'X-Webhook-Timestamp': Date.now().toString(),
...(webhook.headers || {})
}
// Add signature if secret exists
if (webhook.secret) {
const crypto = await import('crypto')
const signature = crypto
.createHmac('sha256', webhook.secret)
.update(body)
.digest('hex')
headers['X-Webhook-Signature'] = `sha256=${signature}`
}
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000)
const response = await fetch(webhook.url, {
method: 'POST',
headers,
body,
signal: controller.signal
})
clearTimeout(timeout)
const responseText = await response.text()
// Log the test
await query(
`INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, response_body, attempt, delivered_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[id, 'test', testPayload, response.status, responseText.slice(0, 1000), 1, new Date()]
)
return {
success: response.ok,
status: response.status,
statusText: response.statusText,
response: responseText.slice(0, 500)
}
} catch (error) {
const errorMessage = (error as Error).message
// Log the failure
await query(
`INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, error_message, attempt)
VALUES ($1, $2, $3, $4, $5, $6)`,
[id, 'test', testPayload, 0, errorMessage, 1]
)
return {
success: false,
error: errorMessage
}
}
})

View File

@@ -0,0 +1,40 @@
/**
* GET /api/webhooks
* List all webhooks
*/
import { query } from '../../utils/database'
interface WebhookRow {
id: string
name: string
url: string
events: string[]
is_active: boolean
instance_id: string | null
created_at: Date
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const result = await query<WebhookRow>(
`SELECT w.id, w.name, w.url, w.events, w.is_active, w.instance_id, w.created_at,
i.name as instance_name
FROM webhooks w
LEFT JOIN instances i ON w.instance_id = i.id
ORDER BY w.created_at DESC`
)
return result.rows.map(row => ({
id: row.id,
name: row.name,
url: row.url,
events: row.events,
isActive: row.is_active,
instanceId: row.instance_id,
createdAt: row.created_at
}))
})

View File

@@ -0,0 +1,54 @@
/**
* POST /api/webhooks
* Create a webhook
*/
import { query } from '../../utils/database'
interface CreateWebhookBody {
name: string
url: string
secret?: string
events: string[]
instanceId?: string | null
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const body = await readBody<CreateWebhookBody>(event)
if (!body.name?.trim()) {
throw createError({ statusCode: 400, message: 'Name is required' })
}
if (!body.url?.trim()) {
throw createError({ statusCode: 400, message: 'URL is required' })
}
if (!body.events?.length) {
throw createError({ statusCode: 400, message: 'At least one event is required' })
}
const result = await query<{ id: string }>(
`INSERT INTO webhooks (name, url, secret, events, instance_id, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id`,
[
body.name.trim(),
body.url.trim(),
body.secret || null,
body.events,
body.instanceId || null,
username
]
)
return {
id: result.rows[0].id,
name: body.name,
url: body.url,
events: body.events,
instanceId: body.instanceId || null
}
})