feat: add OpenClaw read-only Datto RMM API endpoints
GET /api/openclaw/datto-rmm/sites GET /api/openclaw/datto-rmm/devices (filters: siteUid, online, deleted, page, limit) GET /api/openclaw/datto-rmm/devices/[uid] GET /api/openclaw/datto-rmm/devices/[uid]/audit (always live) GET /api/openclaw/datto-rmm/alerts (filters: resolved, siteUid, deviceUid, limit) GET /api/openclaw/datto-rmm/alerts/open - All protected by x-openclaw-key header - Default: queries Pulse DB (datto_rmm_devices/alerts/sites tables) - ?live=true: proxies to Datto RMM API via DattoRMMClient - Responses include source:'db'|'live' for data freshness awareness
This commit is contained in:
parent
ff9e34cafe
commit
679fe3871c
6 changed files with 426 additions and 0 deletions
78
app/api/openclaw/datto-rmm/alerts/open/route.ts
Normal file
78
app/api/openclaw/datto-rmm/alerts/open/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const params = request.nextUrl.searchParams;
|
||||
const live = params.get('live') === 'true';
|
||||
const siteUid = params.get('siteUid');
|
||||
const deviceUid = params.get('deviceUid');
|
||||
const limit = Math.min(1000, Math.max(1, parseInt(params.get('limit') || '200', 10)));
|
||||
|
||||
try {
|
||||
if (live) {
|
||||
const client = getDattoRMMClient();
|
||||
let alerts = await client.getAllOpenAlerts();
|
||||
|
||||
if (siteUid) alerts = alerts.filter((a: any) => a.siteUid === siteUid);
|
||||
if (deviceUid) alerts = alerts.filter((a: any) => a.deviceUid === deviceUid);
|
||||
|
||||
return NextResponse.json({
|
||||
data: alerts.slice(0, limit),
|
||||
total: alerts.length,
|
||||
source: 'live',
|
||||
});
|
||||
}
|
||||
|
||||
const conditions: string[] = ['resolved = false'];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (siteUid) {
|
||||
values.push(siteUid);
|
||||
conditions.push(`site_uid = $${values.length}`);
|
||||
}
|
||||
if (deviceUid) {
|
||||
values.push(deviceUid);
|
||||
conditions.push(`device_uid = $${values.length}`);
|
||||
}
|
||||
|
||||
values.push(limit);
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
alert_uid, alert_category, alert_type, alert_message_en,
|
||||
device_uid, device_hostname, device_ip, device_os, device_description,
|
||||
site_uid, site_name,
|
||||
resolved, muted, ticket_number,
|
||||
timestamp, platform, triggered,
|
||||
alert_context, alert_monitor_info,
|
||||
last_user, ping_target,
|
||||
synced_at, updated_at
|
||||
FROM datto_rmm_alerts
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT $${values.length}`,
|
||||
values
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
data: result.rows,
|
||||
total: result.rowCount,
|
||||
source: 'db',
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
96
app/api/openclaw/datto-rmm/alerts/route.ts
Normal file
96
app/api/openclaw/datto-rmm/alerts/route.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const params = request.nextUrl.searchParams;
|
||||
const live = params.get('live') === 'true';
|
||||
const resolved = params.get('resolved');
|
||||
const siteUid = params.get('siteUid');
|
||||
const deviceUid = params.get('deviceUid');
|
||||
const limit = Math.min(1000, Math.max(1, parseInt(params.get('limit') || '200', 10)));
|
||||
|
||||
try {
|
||||
if (live) {
|
||||
const client = getDattoRMMClient();
|
||||
const openAlerts = await client.getAllOpenAlerts();
|
||||
let alerts: typeof openAlerts = [];
|
||||
|
||||
if (resolved === 'true') {
|
||||
const resolvedAlerts = await client.getRecentResolvedAlerts();
|
||||
alerts = resolvedAlerts as typeof openAlerts;
|
||||
} else if (resolved === 'false' || resolved === null) {
|
||||
alerts = openAlerts;
|
||||
} else {
|
||||
const resolvedAlerts = await client.getRecentResolvedAlerts();
|
||||
alerts = [...openAlerts, ...(resolvedAlerts as typeof openAlerts)];
|
||||
}
|
||||
|
||||
if (siteUid) alerts = alerts.filter((a: any) => a.siteUid === siteUid);
|
||||
if (deviceUid) alerts = alerts.filter((a: any) => a.deviceUid === deviceUid);
|
||||
|
||||
return NextResponse.json({
|
||||
data: alerts.slice(0, limit),
|
||||
total: alerts.length,
|
||||
source: 'live',
|
||||
});
|
||||
}
|
||||
|
||||
const conditions: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (resolved !== null) {
|
||||
values.push(resolved === 'true');
|
||||
conditions.push(`resolved = $${values.length}`);
|
||||
}
|
||||
if (siteUid) {
|
||||
values.push(siteUid);
|
||||
conditions.push(`site_uid = $${values.length}`);
|
||||
}
|
||||
if (deviceUid) {
|
||||
values.push(deviceUid);
|
||||
conditions.push(`device_uid = $${values.length}`);
|
||||
}
|
||||
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
values.push(limit);
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
alert_uid, alert_category, alert_type, alert_message_en,
|
||||
device_uid, device_hostname, device_ip, device_os, device_description,
|
||||
site_uid, site_name,
|
||||
resolved, resolved_by, resolved_on, muted,
|
||||
ticket_number, timestamp, platform, triggered,
|
||||
alert_context, alert_monitor_info,
|
||||
last_user, ping_target,
|
||||
synced_at, updated_at
|
||||
FROM datto_rmm_alerts
|
||||
${where}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT $${values.length}`,
|
||||
values
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
data: result.rows,
|
||||
total: result.rowCount,
|
||||
source: 'db',
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
30
app/api/openclaw/datto-rmm/devices/[uid]/audit/route.ts
Normal file
30
app/api/openclaw/datto-rmm/devices/[uid]/audit/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ uid: string }> }
|
||||
) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const { uid } = await params;
|
||||
|
||||
try {
|
||||
const client = getDattoRMMClient();
|
||||
const audit = await client.getDeviceAudit(uid);
|
||||
|
||||
if (!audit) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Audit data not found for this device' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: audit, source: 'live' });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
61
app/api/openclaw/datto-rmm/devices/[uid]/route.ts
Normal file
61
app/api/openclaw/datto-rmm/devices/[uid]/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
});
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ uid: string }> }
|
||||
) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const { uid } = await params;
|
||||
const live = request.nextUrl.searchParams.get('live') === 'true';
|
||||
|
||||
try {
|
||||
if (live) {
|
||||
const client = getDattoRMMClient();
|
||||
const device = await client.getDeviceById(uid);
|
||||
if (!device) {
|
||||
return NextResponse.json({ error: 'Device not found' }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ data: device, source: 'live' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, uid, site_id, site_uid, site_name, hostname, description,
|
||||
device_type_category, device_type, operating_system, domain,
|
||||
int_ip_address, ext_ip_address, last_logged_in_user,
|
||||
last_seen, last_reboot, last_audit_date, creation_date,
|
||||
online, suspended, deleted, reboot_required, a64_bit,
|
||||
cag_version, display_version,
|
||||
antivirus_product, antivirus_status,
|
||||
patch_status, patches_approved_pending, patches_not_approved, patches_installed,
|
||||
software_status, portal_url, web_remote_url, warranty_date,
|
||||
snmp_enabled, device_class, network_probe,
|
||||
udf, synced_at, updated_at
|
||||
FROM datto_rmm_devices
|
||||
WHERE uid = $1`,
|
||||
[uid]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return NextResponse.json({ error: 'Device not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: result.rows[0], source: 'db' });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
110
app/api/openclaw/datto-rmm/devices/route.ts
Normal file
110
app/api/openclaw/datto-rmm/devices/route.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const params = request.nextUrl.searchParams;
|
||||
const live = params.get('live') === 'true';
|
||||
const siteUid = params.get('siteUid');
|
||||
const online = params.get('online');
|
||||
const deleted = params.get('deleted');
|
||||
const page = Math.max(1, parseInt(params.get('page') || '1', 10));
|
||||
const limit = Math.min(500, Math.max(1, parseInt(params.get('limit') || '100', 10)));
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
if (live) {
|
||||
const client = getDattoRMMClient();
|
||||
let devices = siteUid
|
||||
? await client.getDevicesBySite(siteUid)
|
||||
: await client.getAllDevices();
|
||||
|
||||
if (online !== null) {
|
||||
const onlineBool = online === 'true';
|
||||
devices = devices.filter(d => d.online === onlineBool);
|
||||
}
|
||||
if (deleted !== null) {
|
||||
const deletedBool = deleted === 'true';
|
||||
devices = devices.filter(d => d.deleted === deletedBool);
|
||||
}
|
||||
|
||||
const total = devices.length;
|
||||
const paged = devices.slice(offset, offset + limit);
|
||||
|
||||
return NextResponse.json({
|
||||
data: paged,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
source: 'live',
|
||||
});
|
||||
}
|
||||
|
||||
const conditions: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (siteUid) {
|
||||
values.push(siteUid);
|
||||
conditions.push(`site_uid = $${values.length}`);
|
||||
}
|
||||
if (online !== null) {
|
||||
values.push(online === 'true');
|
||||
conditions.push(`online = $${values.length}`);
|
||||
}
|
||||
if (deleted !== null) {
|
||||
values.push(deleted === 'true');
|
||||
conditions.push(`deleted = $${values.length}`);
|
||||
}
|
||||
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) FROM datto_rmm_devices ${where}`,
|
||||
values
|
||||
);
|
||||
const total = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
values.push(limit, offset);
|
||||
const dataResult = await pool.query(
|
||||
`SELECT
|
||||
id, uid, site_id, site_uid, site_name, hostname, description,
|
||||
device_type_category, device_type, operating_system, domain,
|
||||
int_ip_address, ext_ip_address, last_logged_in_user,
|
||||
last_seen, last_reboot, last_audit_date, creation_date,
|
||||
online, suspended, deleted, reboot_required,
|
||||
cag_version, display_version,
|
||||
antivirus_product, antivirus_status,
|
||||
patch_status, patches_approved_pending, patches_not_approved, patches_installed,
|
||||
portal_url, web_remote_url, warranty_date, device_class,
|
||||
udf, synced_at, updated_at
|
||||
FROM datto_rmm_devices
|
||||
${where}
|
||||
ORDER BY hostname
|
||||
LIMIT $${values.length - 1} OFFSET $${values.length}`,
|
||||
values
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
data: dataResult.rows,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
source: 'db',
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
51
app/api/openclaw/datto-rmm/sites/route.ts
Normal file
51
app/api/openclaw/datto-rmm/sites/route.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const live = request.nextUrl.searchParams.get('live') === 'true';
|
||||
|
||||
try {
|
||||
if (live) {
|
||||
const client = getDattoRMMClient();
|
||||
const sites = await client.getAllSites();
|
||||
return NextResponse.json({
|
||||
data: sites,
|
||||
total: sites.length,
|
||||
source: 'live',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
id, uid, name, description, notes, on_demand,
|
||||
autotask_company_id, autotask_company_name,
|
||||
number_of_devices, number_of_online_devices, number_of_offline_devices,
|
||||
portal_url, synced_at, updated_at
|
||||
FROM datto_rmm_sites
|
||||
ORDER BY name
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
data: result.rows,
|
||||
total: result.rowCount,
|
||||
source: 'db',
|
||||
synced_at: result.rows[0]?.synced_at ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue