/** * Datto RMM Webhook Receiver * Generic endpoint that accepts any payload from Datto RMM and logs it raw. * No processing logic yet — refine after inspecting real payloads. */ import { NextRequest, NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; import '@/lib/services/pipeline-steps'; import { pipelineEngine } from '@/lib/services/pipeline-engine'; import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory'; async function getDattoPingTarget(alertUid: string): Promise { try { const client = getDattoRMMClient(); const target = await client.getPingAlertTarget(alertUid); return target; } catch { return null; } } /** * POST /api/webhooks/datto-rmm * Accepts any payload, stores it for inspection, returns 200. */ export async function POST(request: NextRequest) { const receivedAt = new Date(); try { const sourceIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0].trim() || request.headers.get('x-real-ip') || 'unknown'; const userAgent = request.headers.get('user-agent') || 'unknown'; // Verify shared secret header const secret = process.env.DATTO_RMM_WEBHOOK_SECRET; if (secret) { const provided = request.headers.get('x-datto-webhook-secret'); if (provided !== secret) { console.warn(`[DATTO-RMM-WEBHOOK] Invalid or missing X-Datto-Webhook-Secret from ${sourceIp}`); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } } // Capture all headers as a plain object const headers: Record = {}; request.headers.forEach((value, key) => { headers[key] = value; }); // Read raw body const rawBody = await request.text(); // Try to parse as JSON; fall back to null let payload: any = null; try { payload = JSON.parse(rawBody); } catch { // Not valid JSON — store raw_body only } console.log( `[DATTO-RMM-WEBHOOK] Received payload from ${sourceIp} (${rawBody.length} bytes)` ); // Store in database await postgresClient.query( `INSERT INTO datto_rmm_webhook_logs (received_at, source_ip, user_agent, headers, payload, raw_body, status) VALUES ($1, $2, $3, $4, $5, $6, 'received')`, [ receivedAt, sourceIp, userAgent, JSON.stringify(headers), payload ? JSON.stringify(payload) : null, rawBody || null, ] ); // Upsert into datto_rmm_alerts if payload looks like a real alert if (payload && typeof payload === 'object' && payload.alert_uid && !payload.alert_uid.startsWith('[')) { const pingTarget = payload.alert_type === 'PING' ? await getDattoPingTarget(payload.alert_uid) : null; const isResolved = String(payload.triggered).toLowerCase() === 'false'; const str = (v: unknown) => (v && String(v).trim() !== '' ? String(v) : null); await postgresClient.query( `INSERT INTO datto_rmm_alerts ( alert_uid, device_uid, device_name, device_hostname, device_ip, device_os, device_description, device_id, site_uid, site_name, site_id, platform, priority, alert_category, alert_type, alert_message_en, last_user, triggered, resolved, resolved_on, device_udf1, device_udf2, device_udf3, device_udf4, device_udf5, device_udf6, device_udf7, device_udf8, device_udf9, device_udf10, device_udf11, device_udf12, device_udf13, device_udf14, device_udf15, device_udf16, device_udf17, device_udf18, device_udf19, device_udf20, device_udf21, device_udf22, device_udf23, device_udf24, device_udf25, device_udf26, device_udf27, device_udf28, device_udf29, ping_target, timestamp, synced_at ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20, $21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40, $41,$42,$43,$44,$45,$46,$47,$48,$49, $50, $51,$52 ) ON CONFLICT (alert_uid) DO UPDATE SET resolved = EXCLUDED.resolved, resolved_on = CASE WHEN EXCLUDED.resolved AND datto_rmm_alerts.resolved_on IS NULL THEN NOW() ELSE datto_rmm_alerts.resolved_on END, triggered = EXCLUDED.triggered, alert_message_en = COALESCE(EXCLUDED.alert_message_en, datto_rmm_alerts.alert_message_en), ping_target = COALESCE(EXCLUDED.ping_target, datto_rmm_alerts.ping_target), synced_at = NOW()`, [ payload.alert_uid, str(payload.device_uid), str(payload.device_hostname), str(payload.device_hostname), str(payload.device_ip), str(payload.device_os), str(payload.device_description), str(payload.device_id), str(payload.site_uid), str(payload.site_name), str(payload.site_id), str(payload.platform), str(payload.alert_priority), str(payload.alert_category), str(payload.alert_type), str(payload.alert_message_en), str(payload.last_user), String(payload.triggered), isResolved, isResolved ? receivedAt : null, str(payload.device_udf1), str(payload.device_udf2), str(payload.device_udf3), str(payload.device_udf4), str(payload.device_udf5), str(payload.device_udf6), str(payload.device_udf7), str(payload.device_udf8), str(payload.device_udf9), str(payload.device_udf10), str(payload.device_udf11), str(payload.device_udf12), str(payload.device_udf13), str(payload.device_udf14), str(payload.device_udf15), str(payload.device_udf16), str(payload.device_udf17), str(payload.device_udf18), str(payload.device_udf19), str(payload.device_udf20), str(payload.device_udf21), str(payload.device_udf22), str(payload.device_udf23), str(payload.device_udf24), str(payload.device_udf25), str(payload.device_udf26), str(payload.device_udf27), str(payload.device_udf28), str(payload.device_udf29), pingTarget, receivedAt, receivedAt, ] ); console.log(`[DATTO-RMM-WEBHOOK] Upserted alert ${payload.alert_uid} — resolved=${isResolved}`); } // Fire matching pipelines (fire-and-forget) if (payload && typeof payload === 'object') { pipelineEngine.processTrigger('datto_rmm', payload).catch(err => console.error('[DATTO-RMM-WEBHOOK] Pipeline processing error:', err) ); } return NextResponse.json({ success: true, received_at: receivedAt.toISOString() }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); console.error('[DATTO-RMM-WEBHOOK] Error storing webhook:', msg); // Still return 200 to avoid Datto disabling the webhook return NextResponse.json({ success: false, error: msg }, { status: 200 }); } } /** * GET /api/webhooks/datto-rmm * Health check */ export async function GET() { return NextResponse.json({ status: 'active', endpoint: '/api/webhooks/datto-rmm', message: 'Datto RMM webhook receiver is ready', }); }