/** * POST /api/zabbix/webhook * Receives Zabbix alert/recovery notifications and writes them to zabbix_events. * Enriches each event with company/site context from the local zabbix_wan_hosts cache. * * Expected JSON payload (sent by the Zabbix webhook media type script): * { * event_id: string -- {EVENT.ID} * event_name: string -- {EVENT.NAME} * event_value: string -- "1" = PROBLEM, "0" = RESOLVED * event_severity: string -- numeric severity "0"–"5" * event_clock: string -- Unix timestamp string * trigger_id: string -- {TRIGGER.ID} * host_id: string -- {HOST.ID} * host_name: string -- {HOST.HOST} (technical name) * r_event_id?: string -- {EVENT.RECOVERY.ID} (only on recovery) * r_clock?: string -- {EVENT.RECOVERY.DATE} as unix ts (only on recovery) * } * * Auth: Bearer token in Authorization header, matched against ZABBIX_WEBHOOK_SECRET env var. */ import { NextRequest, NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; export async function POST(request: NextRequest) { // Optional shared secret — skip check if not configured const secret = process.env.ZABBIX_WEBHOOK_SECRET; if (secret) { const auth = request.headers.get('authorization') ?? ''; const token = auth.startsWith('Bearer ') ? auth.slice(7) : auth; if (token !== secret) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } } let body: Record; try { const raw = await request.text(); console.log('[ZABBIX-WEBHOOK] Incoming request — auth:', request.headers.get('authorization') ? 'present' : 'none', '— body:', raw.substring(0, 500)); body = JSON.parse(raw); } catch { console.log('[ZABBIX-WEBHOOK] Failed to parse JSON'); return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } const { event_id, event_name, event_value, event_severity, event_clock, trigger_id, host_id, r_event_id, r_clock, } = body; if (!event_id || !host_id || !event_clock) { return NextResponse.json({ error: 'Missing required fields: event_id, host_id, event_clock' }, { status: 400 }); } try { // Enrich with local host context const hostRow = await postgresClient.query<{ display_name: string; wan_ip: string | null; autotask_company_id: number | null; autotask_company_name: string | null; rmm_site_uid: string | null; isp_name: string | null; }>( 'SELECT display_name, wan_ip, autotask_company_id, autotask_company_name, rmm_site_uid, isp_name FROM zabbix_wan_hosts WHERE hostid = $1', [host_id] ); const host = hostRow.rows[0] ?? null; const toInt = (v: string | undefined) => { const n = Number(v); return Number.isFinite(n) ? n : null; }; const toTs = (v: string | undefined) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? new Date(n * 1000) : null; }; const isMacro = (v: string | undefined) => !v || v.startsWith('{'); const isResolved = event_value === '0'; const clockTs = toTs(event_clock) ?? new Date(); // {EVENT.RECOVERY.CLOCK} often doesn't resolve in webhook params — fall back to NOW() for recoveries const rClockTs = toTs(r_clock) ?? (isResolved ? new Date() : null); const duration = rClockTs ? Math.round((rClockTs.getTime() - clockTs.getTime()) / 1000) : null; // {EVENT.RECOVERY.ID} also may not resolve — use event_id as marker so the row is flagged resolved const rEventId = !isMacro(r_event_id) && r_event_id !== '0' ? r_event_id : (isResolved ? event_id : null); await postgresClient.query( `INSERT INTO zabbix_events ( eventid, objectid, name, severity, clock, r_eventid, r_clock, duration_seconds, hostid, host_name, wan_ip, autotask_company_id, autotask_company_name, rmm_site_uid, isp_name, last_synced_at ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW()) ON CONFLICT (eventid) DO UPDATE SET name = EXCLUDED.name, r_eventid = COALESCE(EXCLUDED.r_eventid, zabbix_events.r_eventid), r_clock = COALESCE(EXCLUDED.r_clock, zabbix_events.r_clock), duration_seconds = COALESCE(EXCLUDED.duration_seconds, zabbix_events.duration_seconds), host_name = EXCLUDED.host_name, wan_ip = EXCLUDED.wan_ip, autotask_company_id = EXCLUDED.autotask_company_id, autotask_company_name = EXCLUDED.autotask_company_name, rmm_site_uid = EXCLUDED.rmm_site_uid, isp_name = EXCLUDED.isp_name, last_synced_at = NOW()`, [ event_id, trigger_id ?? null, event_name ?? null, toInt(event_severity), clockTs, rEventId, rClockTs, duration, host_id, host?.display_name ?? body.host_name ?? null, host?.wan_ip ?? null, host?.autotask_company_id ?? null, host?.autotask_company_name ?? null, host?.rmm_site_uid ?? null, host?.isp_name ?? null, ] ); return NextResponse.json({ ok: true, event_id, resolved: isResolved }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error('[ZABBIX-WEBHOOK]', msg); return NextResponse.json({ error: msg }, { status: 500 }); } } /** * GET /api/zabbix/webhook * Returns the Zabbix media type script and parameter config ready to paste. */ export async function GET() { const baseUrl = process.env.WEBHOOK_BASE_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? process.env.APP_URL ?? 'https://your-pulse-url'; const secret = process.env.ZABBIX_WEBHOOK_SECRET ?? ''; const script = `// Pulse WAN Correlation Webhook var params = JSON.parse(value); var req = new HttpRequest(); req.addHeader('Content-Type: application/json'); ${secret ? "req.addHeader('Authorization: Bearer ' + params.webhook_secret);" : '// No auth configured — set ZABBIX_WEBHOOK_SECRET env var to enable'} var payload = JSON.stringify({ event_id: params.event_id, event_name: params.event_name, event_value: params.event_value, event_severity: params.event_severity, event_clock: params.event_clock, trigger_id: params.trigger_id, host_id: params.host_id, host_name: params.host_name, r_event_id: params.r_event_id, r_clock: params.r_clock }); var response = req.post(params.webhook_url, payload); if (req.getStatus() !== 200) { throw 'Pulse webhook failed: HTTP ' + req.getStatus() + ' — ' + response; } return 'OK';`; const parameters = [ { name: 'webhook_url', value: `${baseUrl}/api/zabbix/webhook` }, { name: 'webhook_secret', value: secret || '(set ZABBIX_WEBHOOK_SECRET env var)' }, { name: 'event_id', value: '{EVENT.ID}' }, { name: 'event_name', value: '{EVENT.NAME}' }, { name: 'event_value', value: '{EVENT.VALUE}' }, { name: 'event_severity', value: '{EVENT.SEVERITY.NUM}' }, { name: 'event_clock', value: '{EVENT.CLOCK}' }, { name: 'trigger_id', value: '{TRIGGER.ID}' }, { name: 'host_id', value: '{HOST.ID}' }, { name: 'host_name', value: '{HOST.HOST}' }, { name: 'r_event_id', value: '{EVENT.RECOVERY.ID}' }, { name: 'r_clock', value: '{EVENT.RECOVERY.CLOCK}' }, ]; return NextResponse.json({ script, parameters, webhook_url: `${baseUrl}/api/zabbix/webhook` }); }