wulf-pulse/app/api/webhooks/datto-rmm/route.ts

100 lines
3.1 KiB
TypeScript

/**
* 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';
/**
* 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<string, string> = {};
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,
]
);
// 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',
});
}