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,29 @@
/**
* GET /api/auth/status
* Verifica el estado de autenticacion del usuario via Authentik headers
*/
export default defineEventHandler((event) => {
const headers = getHeaders(event)
const username = headers['x-authentik-username']
const email = headers['x-authentik-email']
const name = headers['x-authentik-name']
const groups = headers['x-authentik-groups']
if (!username) {
return {
authenticated: false,
user: null
}
}
return {
authenticated: true,
user: {
username,
email,
name,
groups: groups ? groups.split('|').filter((g: string) => g.trim()) : []
}
}
})

View File

@@ -0,0 +1,66 @@
/**
* GET /api/events/stream
* Server-Sent Events endpoint for real-time updates
*/
import { baileysManager } from '../../services/baileys/manager'
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
// Set SSE headers
setHeader(event, 'Content-Type', 'text/event-stream')
setHeader(event, 'Cache-Control', 'no-cache')
setHeader(event, 'Connection', 'keep-alive')
// Get the raw response
const res = event.node.res
// Send initial connection message
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`)
// Event handlers
const handlers = {
'instance.status': (data: any) => {
res.write(`event: instance.status\ndata: ${JSON.stringify(data)}\n\n`)
},
'instance.qr': (data: any) => {
res.write(`event: instance.qr\ndata: ${JSON.stringify(data)}\n\n`)
},
'instance.pairing': (data: any) => {
res.write(`event: instance.pairing\ndata: ${JSON.stringify(data)}\n\n`)
},
'message.received': (data: any) => {
res.write(`event: message.received\ndata: ${JSON.stringify(data)}\n\n`)
},
'message.sent': (data: any) => {
res.write(`event: message.sent\ndata: ${JSON.stringify(data)}\n\n`)
},
'message.status': (data: any) => {
res.write(`event: message.status\ndata: ${JSON.stringify(data)}\n\n`)
}
}
// Register listeners
for (const [eventName, handler] of Object.entries(handlers)) {
baileysManager.on(eventName, handler)
}
// Keep-alive ping
const pingInterval = setInterval(() => {
res.write(`: ping\n\n`)
}, 30000)
// Cleanup on close
event.node.req.on('close', () => {
clearInterval(pingInterval)
for (const [eventName, handler] of Object.entries(handlers)) {
baileysManager.off(eventName, handler)
}
})
// Don't close the connection
return new Promise(() => {})
})

11
server/api/health.get.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* GET /api/health
* Health check endpoint
*/
export default defineEventHandler(() => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0'
}
})

View File

@@ -0,0 +1,54 @@
/**
* POST /api/instances/:id/connect
* Connect an instance (generates QR code)
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
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')
// Check if instance exists
const result = await query<{ id: string; status: string }>(
'SELECT id, status FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
const instance = result.rows[0]
// Don't connect if already connected
if (instance.status === 'connected') {
throw createError({ statusCode: 400, message: 'Instance already connected' })
}
// Start connection
try {
await baileysManager.connect(id!)
// Wait a bit for QR to generate
await new Promise(resolve => setTimeout(resolve, 2000))
const qrCode = baileysManager.getQRCode(id!)
const status = baileysManager.getStatus(id!)
return {
success: true,
status: status?.status || 'connecting',
qrCode
}
} catch (error) {
throw createError({
statusCode: 500,
message: `Failed to connect: ${(error as Error).message}`
})
}
})

View File

@@ -0,0 +1,35 @@
/**
* POST /api/instances/:id/disconnect
* Disconnect an instance
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
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')
// Check if instance exists
const result = await query<{ id: string }>(
'SELECT id FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
try {
await baileysManager.disconnect(id!)
return { success: true, status: 'disconnected' }
} catch (error) {
throw createError({
statusCode: 500,
message: `Failed to disconnect: ${(error as Error).message}`
})
}
})

View File

@@ -0,0 +1,41 @@
/**
* DELETE /api/instances/:id
* Delete an instance
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
import { clearAuthState } from '../../../services/baileys/auth-state'
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')
// Check if instance exists
const result = await query<{ id: string }>(
'SELECT id FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
// Disconnect if connected
try {
await baileysManager.disconnect(id!)
} catch (error) {
// Ignore disconnect errors
}
// Clear auth state
await clearAuthState(id!)
// Delete from database (cascades to related tables)
await query('DELETE FROM instances WHERE id = $1', [id])
return { success: true }
})

View File

@@ -0,0 +1,53 @@
/**
* GET /api/instances/:id
* Get instance details
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
interface InstanceRow {
id: string
name: string
phone_number: string | null
status: string
qr_code: string | null
pairing_code: string | null
last_connected_at: Date | null
created_by: string
created_at: Date
}
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<InstanceRow>(
`SELECT id, name, phone_number, status, qr_code, pairing_code,
last_connected_at, created_by, created_at
FROM instances WHERE id = $1`,
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
const instance = result.rows[0]
const liveStatus = baileysManager.getStatus(instance.id)
return {
id: instance.id,
name: instance.name,
phoneNumber: instance.phone_number,
status: liveStatus?.status || instance.status,
qrCode: liveStatus?.qrCode || instance.qr_code,
pairingCode: liveStatus?.pairingCode || instance.pairing_code,
lastConnectedAt: instance.last_connected_at,
createdBy: instance.created_by,
createdAt: instance.created_at
}
})

View File

@@ -0,0 +1,69 @@
/**
* POST /api/instances/:id/pairing-code
* Request a pairing code for connection without QR
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
interface PairingCodeBody {
phoneNumber: string
}
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<PairingCodeBody>(event)
if (!body.phoneNumber?.trim()) {
throw createError({ statusCode: 400, message: 'Phone number is required' })
}
// Clean phone number (remove +, spaces, dashes)
const cleanPhone = body.phoneNumber.replace(/[^0-9]/g, '')
// Check if instance exists
const result = await query<{ id: string; status: string }>(
'SELECT id, status FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
const instance = result.rows[0]
if (instance.status === 'connected') {
throw createError({ statusCode: 400, message: 'Instance already connected' })
}
try {
// Connect with pairing code mode
await baileysManager.connect(id!, true, cleanPhone)
// Wait for pairing code
await new Promise(resolve => setTimeout(resolve, 5000))
const pairingCode = baileysManager.getPairingCode(id!)
const status = baileysManager.getStatus(id!)
if (!pairingCode) {
throw new Error('Failed to generate pairing code')
}
return {
success: true,
code: pairingCode,
status: status?.status || 'pairing'
}
} catch (error) {
throw createError({
statusCode: 500,
message: `Failed to request pairing code: ${(error as Error).message}`
})
}
})

View File

@@ -0,0 +1,34 @@
/**
* GET /api/instances/:id/qr
* Get current QR code for an instance
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
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')
// Check if instance exists
const result = await query<{ id: string; qr_code: string | null; status: string }>(
'SELECT id, qr_code, status FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
// Get live QR from manager
const qrCode = baileysManager.getQRCode(id!)
const status = baileysManager.getStatus(id!)
return {
qrCode: qrCode || result.rows[0].qr_code,
status: status?.status || result.rows[0].status
}
})

View File

@@ -0,0 +1,36 @@
/**
* GET /api/instances/:id/status
* Get instance connection status
*/
import { query } from '../../../utils/database'
import { baileysManager } from '../../../services/baileys/manager'
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')
// Check if instance exists
const result = await query<{ id: string; status: string; phone_number: string | null }>(
'SELECT id, status, phone_number FROM instances WHERE id = $1',
[id]
)
if (result.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
const instance = result.rows[0]
const liveStatus = baileysManager.getStatus(id!)
return {
instanceId: id,
status: liveStatus?.status || instance.status,
phoneNumber: liveStatus?.phoneNumber || instance.phone_number,
hasQR: !!liveStatus?.qrCode,
hasPairingCode: !!liveStatus?.pairingCode
}
})

View File

@@ -0,0 +1,42 @@
/**
* GET /api/instances
* List all instances
*/
import { query } from '../../utils/database'
import { baileysManager } from '../../services/baileys/manager'
interface InstanceRow {
id: string
name: string
phone_number: string | null
status: string
last_connected_at: Date | null
created_at: Date
}
export default defineEventHandler(async (event) => {
// Check auth
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const result = await query<InstanceRow>(
`SELECT id, name, phone_number, status, last_connected_at, created_at
FROM instances
ORDER BY created_at DESC`
)
// Enrich with live status from manager
return result.rows.map(row => {
const liveStatus = baileysManager.getStatus(row.id)
return {
id: row.id,
name: row.name,
phoneNumber: row.phone_number,
status: liveStatus?.status || row.status,
lastConnectedAt: row.last_connected_at,
createdAt: row.created_at
}
})
})

View File

@@ -0,0 +1,48 @@
/**
* POST /api/instances
* Create a new instance
*/
import { query } from '../../utils/database'
interface CreateInstanceBody {
name: string
}
interface InstanceRow {
id: string
name: string
phone_number: string | null
status: string
created_at: Date
}
export default defineEventHandler(async (event) => {
// Check auth
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const body = await readBody<CreateInstanceBody>(event)
if (!body.name?.trim()) {
throw createError({ statusCode: 400, message: 'Name is required' })
}
const result = await query<InstanceRow>(
`INSERT INTO instances (name, created_by)
VALUES ($1, $2)
RETURNING id, name, phone_number, status, created_at`,
[body.name.trim(), username]
)
const instance = result.rows[0]
return {
id: instance.id,
name: instance.name,
phoneNumber: instance.phone_number,
status: instance.status,
createdAt: instance.created_at
}
})

View File

@@ -0,0 +1,72 @@
/**
* GET /api/messages/:instanceId/:chatId
* Get messages for a chat
*/
import { query } from '../../../../utils/database'
interface MessageRow {
id: string
message_id: string
from_jid: string
from_me: boolean
message_type: string
content: string | null
caption: string | null
media_url: string | null
timestamp: Date
status: string
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const instanceId = getRouterParam(event, 'instanceId')
const chatId = getRouterParam(event, 'chatId')
// Get query params for pagination
const queryParams = getQuery(event)
const limit = Math.min(parseInt(queryParams.limit as string) || 50, 100)
const offset = parseInt(queryParams.offset as string) || 0
// Verify chat exists and belongs to instance
const chatCheck = await query(
'SELECT id FROM chats WHERE id = $1 AND instance_id = $2',
[chatId, instanceId]
)
if (chatCheck.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Chat not found' })
}
// Get messages
const result = await query<MessageRow>(
`SELECT id, message_id, from_jid, from_me, message_type,
content, caption, media_url, timestamp, status
FROM messages
WHERE chat_id = $1
ORDER BY timestamp DESC
LIMIT $2 OFFSET $3`,
[chatId, limit, offset]
)
// Mark as read
await query(
'UPDATE chats SET unread_count = 0 WHERE id = $1',
[chatId]
)
return result.rows.map(row => ({
id: row.id,
messageId: row.message_id,
fromJid: row.from_jid,
fromMe: row.from_me,
type: row.message_type,
content: row.content,
caption: row.caption,
mediaUrl: row.media_url,
timestamp: row.timestamp,
status: row.status
}))
})

View File

@@ -0,0 +1,53 @@
/**
* POST /api/messages/:instanceId/:chatId/send
* Send a message to a chat (from UI)
*/
import { query } from '../../../../utils/database'
import { baileysManager } from '../../../../services/baileys/manager'
interface SendMessageBody {
message: string
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const instanceId = getRouterParam(event, 'instanceId')
const chatId = getRouterParam(event, 'chatId')
const body = await readBody<SendMessageBody>(event)
if (!body.message?.trim()) {
throw createError({ statusCode: 400, message: 'Message is required' })
}
// Get chat JID
const chatResult = await query<{ jid: string }>(
'SELECT jid FROM chats WHERE id = $1 AND instance_id = $2',
[chatId, instanceId]
)
if (chatResult.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Chat not found' })
}
const jid = chatResult.rows[0].jid
try {
const result = await baileysManager.sendMessage(instanceId!, jid, {
text: body.message
})
return {
success: true,
messageId: result.key.id
}
} catch (error) {
throw createError({
statusCode: 500,
message: `Failed to send message: ${(error as Error).message}`
})
}
})

View File

@@ -0,0 +1,47 @@
/**
* GET /api/messages/:instanceId/chats
* Get all chats for an instance
*/
import { query } from '../../../utils/database'
interface ChatRow {
id: string
jid: string
name: string | null
is_group: boolean
unread_count: number
last_message_at: Date | null
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const instanceId = getRouterParam(event, 'instanceId')
// Verify instance exists
const instanceCheck = await query('SELECT id FROM instances WHERE id = $1', [instanceId])
if (instanceCheck.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Instance not found' })
}
// Get chats with last message
const result = await query<ChatRow>(
`SELECT c.id, c.jid, c.name, c.is_group, c.unread_count, c.last_message_at
FROM chats c
WHERE c.instance_id = $1
ORDER BY c.last_message_at DESC NULLS LAST`,
[instanceId]
)
return result.rows.map(row => ({
id: row.id,
jid: row.jid,
name: row.name || row.jid.split('@')[0],
isGroup: row.is_group,
unreadCount: row.unread_count,
lastMessageAt: row.last_message_at
}))
})

View File

@@ -0,0 +1,92 @@
/**
* POST /api/messages/send
* Send a message (External API - uses API Key)
*/
import { query } from '../../utils/database'
import { baileysManager } from '../../services/baileys/manager'
interface SendMessageBody {
instanceId: string
to: string
message: string
}
export default defineEventHandler(async (event) => {
// Check API Key authentication
const authHeader = getHeader(event, 'authorization')
if (!authHeader?.startsWith('Bearer ')) {
throw createError({ statusCode: 401, message: 'API Key required' })
}
const apiKey = authHeader.slice(7)
const config = useRuntimeConfig()
// Check master API key
if (config.masterApiKey && apiKey === config.masterApiKey) {
// Master key - allowed
} else {
// Check database API keys
const crypto = await import('crypto')
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex')
const keyResult = await query(
`SELECT id, instance_id FROM api_keys
WHERE key_hash = $1 AND is_active = TRUE
AND (expires_at IS NULL OR expires_at > NOW())`,
[keyHash]
)
if (keyResult.rows.length === 0) {
throw createError({ statusCode: 401, message: 'Invalid API Key' })
}
// Update last used
await query(
'UPDATE api_keys SET last_used_at = NOW() WHERE id = $1',
[keyResult.rows[0].id]
)
}
// Parse body
const body = await readBody<SendMessageBody>(event)
if (!body.instanceId) {
throw createError({ statusCode: 400, message: 'instanceId is required' })
}
if (!body.to) {
throw createError({ statusCode: 400, message: 'to is required' })
}
if (!body.message?.trim()) {
throw createError({ statusCode: 400, message: 'message is required' })
}
// Format JID
let jid = body.to.replace(/[^0-9]/g, '')
if (!jid.includes('@')) {
jid = `${jid}@s.whatsapp.net`
}
// Check instance exists and is connected
const status = baileysManager.getStatus(body.instanceId)
if (!status || status.status !== 'connected') {
throw createError({ statusCode: 400, message: 'Instance not connected' })
}
try {
const result = await baileysManager.sendMessage(body.instanceId, jid, {
text: body.message
})
return {
success: true,
messageId: result.key.id,
to: jid
}
} catch (error) {
throw createError({
statusCode: 500,
message: `Failed to send message: ${(error as Error).message}`
})
}
})

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
}
})