feat: WhatsApp Nucleo con Nuxt 4 + Baileys v7
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s
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:
22
server/api/webhooks/[id]/index.delete.ts
Normal file
22
server/api/webhooks/[id]/index.delete.ts
Normal 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 }
|
||||
})
|
||||
72
server/api/webhooks/[id]/index.put.ts
Normal file
72
server/api/webhooks/[id]/index.put.ts
Normal 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 }
|
||||
})
|
||||
100
server/api/webhooks/[id]/test.post.ts
Normal file
100
server/api/webhooks/[id]/test.post.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
40
server/api/webhooks/index.get.ts
Normal file
40
server/api/webhooks/index.get.ts
Normal 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
|
||||
}))
|
||||
})
|
||||
54
server/api/webhooks/index.post.ts
Normal file
54
server/api/webhooks/index.post.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user