Feature: Receptor de webhooks interno para debug
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
- Endpoint POST /api/debug/webhook-receiver para recibir webhooks - Almacenamiento en memoria de ultimos 100 eventos - Endpoint GET/DELETE para consultar/limpiar eventos - Nueva tab Webhooks en seccion Debug con polling cada 5s
This commit is contained in:
181
app/components/debug/WebhookReceiverSection.vue
Normal file
181
app/components/debug/WebhookReceiverSection.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="space-y-6 p-4">
|
||||
<!-- Setup Info -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium text-[var(--wa-text)]">Receptor de Webhooks</h3>
|
||||
<p class="text-sm text-[var(--wa-text-muted)]">
|
||||
Configura un webhook con la siguiente URL para recibir eventos aqui:
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 p-3 rounded bg-gray-900 text-green-400 text-sm font-mono">
|
||||
{{ receiverUrl }}
|
||||
</code>
|
||||
<button
|
||||
@click="copyToClipboard(receiverUrl)"
|
||||
class="px-3 py-3 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||
title="Copiar URL"
|
||||
>
|
||||
<UIcon name="i-lucide-copy" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-[var(--wa-border)]" />
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
:loading="loading"
|
||||
variant="soft"
|
||||
@click="fetchEvents"
|
||||
>
|
||||
<UIcon name="i-lucide-refresh-cw" class="w-4 h-4 mr-2" />
|
||||
Actualizar
|
||||
</UButton>
|
||||
<UButton
|
||||
variant="soft"
|
||||
color="error"
|
||||
:disabled="events.length === 0"
|
||||
@click="clearEvents"
|
||||
>
|
||||
<UIcon name="i-lucide-trash-2" class="w-4 h-4 mr-2" />
|
||||
Limpiar
|
||||
</UButton>
|
||||
</div>
|
||||
<span class="text-sm text-[var(--wa-text-muted)]">
|
||||
{{ events.length }} eventos
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Events List -->
|
||||
<div v-if="events.length === 0" class="text-center py-8">
|
||||
<UIcon name="i-lucide-webhook" class="w-12 h-12 text-[var(--wa-text-muted)] mx-auto mb-3" />
|
||||
<p class="text-[var(--wa-text-muted)]">No hay eventos recibidos</p>
|
||||
<p class="text-sm text-[var(--wa-text-muted)] mt-1">
|
||||
Configura un webhook apuntando a la URL de arriba
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 max-h-[500px] overflow-y-auto">
|
||||
<div
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
class="p-3 rounded bg-gray-900 border border-gray-700"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 rounded bg-blue-900/50 text-blue-400 text-xs font-medium">
|
||||
{{ event.event }}
|
||||
</span>
|
||||
<span class="text-xs text-[var(--wa-text-muted)]">
|
||||
{{ formatTime(event.receivedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="copyToClipboard(JSON.stringify(event, null, 2))"
|
||||
class="px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||
title="Copiar evento"
|
||||
>
|
||||
<UIcon name="i-lucide-copy" class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleExpand(event.id)"
|
||||
class="text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-text)] mb-1"
|
||||
>
|
||||
{{ expandedEvents.has(event.id) ? 'Colapsar' : 'Expandir' }} datos
|
||||
</button>
|
||||
<pre
|
||||
v-if="expandedEvents.has(event.id)"
|
||||
class="text-xs font-mono text-green-400 whitespace-pre-wrap overflow-x-auto max-h-60 mt-2"
|
||||
>{{ JSON.stringify(event.data, null, 2) }}</pre>
|
||||
<pre
|
||||
v-else
|
||||
class="text-xs font-mono text-green-400 truncate"
|
||||
>{{ JSON.stringify(event.data) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface WebhookEvent {
|
||||
id: string
|
||||
receivedAt: string
|
||||
event: string
|
||||
timestamp?: string
|
||||
data: any
|
||||
headers: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
const events = ref<WebhookEvent[]>([])
|
||||
const loading = ref(false)
|
||||
const expandedEvents = ref(new Set<string>())
|
||||
|
||||
// Build receiver URL
|
||||
const receiverUrl = computed(() => {
|
||||
if (import.meta.client) {
|
||||
return `${window.location.origin}/api/debug/webhook-receiver`
|
||||
}
|
||||
return '/api/debug/webhook-receiver'
|
||||
})
|
||||
|
||||
const fetchEvents = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await $fetch<{ events: WebhookEvent[] }>('/api/debug/webhook-events')
|
||||
events.value = result.events
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearEvents = async () => {
|
||||
try {
|
||||
await $fetch('/api/debug/webhook-events', { method: 'DELETE' })
|
||||
events.value = []
|
||||
expandedEvents.value.clear()
|
||||
} catch (error) {
|
||||
console.error('Error clearing events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
if (expandedEvents.value.has(id)) {
|
||||
expandedEvents.value.delete(id)
|
||||
} else {
|
||||
expandedEvents.value.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleTimeString('es-AR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch events on mount and set up polling
|
||||
onMounted(() => {
|
||||
fetchEvents()
|
||||
|
||||
// Poll every 5 seconds
|
||||
const interval = setInterval(fetchEvents, 5000)
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
})
|
||||
</script>
|
||||
@@ -33,8 +33,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div v-if="selectedInstance" class="instance-card">
|
||||
<div class="instance-card">
|
||||
<UTabs :items="tabs" class="w-full">
|
||||
<template #webhooks>
|
||||
<DebugWebhookReceiverSection />
|
||||
</template>
|
||||
<template #blocklist>
|
||||
<DebugBlocklistSection
|
||||
:instance-id="selectedInstance?.value"
|
||||
@@ -121,6 +124,7 @@ watch(instanceOptions, (opts) => {
|
||||
}, { immediate: true })
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Webhooks', slot: 'webhooks', icon: 'i-lucide-webhook' },
|
||||
{ label: 'Blocklist', slot: 'blocklist', icon: 'i-lucide-ban' },
|
||||
{ label: 'Privacy', slot: 'privacy', icon: 'i-lucide-shield' },
|
||||
{ label: 'Groups', slot: 'groups', icon: 'i-lucide-users' },
|
||||
|
||||
16
server/api/debug/webhook-events.delete.ts
Normal file
16
server/api/debug/webhook-events.delete.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* DELETE /api/debug/webhook-events
|
||||
* Clear all stored debug webhook events
|
||||
*/
|
||||
import { debugWebhookStore } from '../../services/debug/webhook-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const username = getHeader(event, 'x-authentik-username')
|
||||
if (!username) {
|
||||
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
debugWebhookStore.clear()
|
||||
|
||||
return { success: true, message: 'Events cleared' }
|
||||
})
|
||||
23
server/api/debug/webhook-events.get.ts
Normal file
23
server/api/debug/webhook-events.get.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* GET /api/debug/webhook-events
|
||||
* Get stored debug webhook events
|
||||
*/
|
||||
import { debugWebhookStore } from '../../services/debug/webhook-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const username = getHeader(event, 'x-authentik-username')
|
||||
if (!username) {
|
||||
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const limit = parseInt(query.limit as string) || 50
|
||||
|
||||
const events = debugWebhookStore.getEvents(limit)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: events.length,
|
||||
events
|
||||
}
|
||||
})
|
||||
32
server/api/debug/webhook-receiver.post.ts
Normal file
32
server/api/debug/webhook-receiver.post.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* POST /api/debug/webhook-receiver
|
||||
* Internal endpoint to receive webhooks for debugging
|
||||
* Stores events in memory and emits via SSE
|
||||
*/
|
||||
import { debugWebhookStore } from '../../services/debug/webhook-store'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const headers = getHeaders(event)
|
||||
|
||||
const webhookEvent = {
|
||||
id: crypto.randomUUID(),
|
||||
receivedAt: new Date().toISOString(),
|
||||
event: body.event || 'unknown',
|
||||
timestamp: body.timestamp,
|
||||
data: body.data || body,
|
||||
headers: {
|
||||
'x-webhook-event': headers['x-webhook-event'],
|
||||
'x-webhook-timestamp': headers['x-webhook-timestamp'],
|
||||
'x-webhook-signature': headers['x-webhook-signature'],
|
||||
'content-type': headers['content-type'],
|
||||
}
|
||||
}
|
||||
|
||||
// Store the event
|
||||
debugWebhookStore.addEvent(webhookEvent)
|
||||
|
||||
console.log(`[Debug Webhook] Received event: ${webhookEvent.event}`)
|
||||
|
||||
return { success: true, message: 'Event received' }
|
||||
})
|
||||
41
server/services/debug/webhook-store.ts
Normal file
41
server/services/debug/webhook-store.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Debug Webhook Store
|
||||
* Stores webhook events in memory for debugging
|
||||
*/
|
||||
|
||||
interface WebhookEvent {
|
||||
id: string
|
||||
receivedAt: string
|
||||
event: string
|
||||
timestamp?: string
|
||||
data: any
|
||||
headers: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
class DebugWebhookStore {
|
||||
private events: WebhookEvent[] = []
|
||||
private maxEvents = 100
|
||||
|
||||
addEvent(event: WebhookEvent) {
|
||||
this.events.unshift(event)
|
||||
|
||||
// Keep only the last N events
|
||||
if (this.events.length > this.maxEvents) {
|
||||
this.events = this.events.slice(0, this.maxEvents)
|
||||
}
|
||||
}
|
||||
|
||||
getEvents(limit: number = 50): WebhookEvent[] {
|
||||
return this.events.slice(0, limit)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events = []
|
||||
}
|
||||
|
||||
getCount(): number {
|
||||
return this.events.length
|
||||
}
|
||||
}
|
||||
|
||||
export const debugWebhookStore = new DebugWebhookStore()
|
||||
Reference in New Issue
Block a user