- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
/**
|
|
* Resolve linkages from a LogLift webhook payload to Pulse's identity model.
|
|
*
|
|
* clientId (Datto site uid) → datto_rmm_sites.id, autotask_company_id
|
|
* computerName (hostname) → datto_rmm_devices.uid (the real Datto uid)
|
|
* hostname + company → itg_configurations.id (single match only)
|
|
*
|
|
* Auto-audit only fires when a Configuration matches exactly one row —
|
|
* multi-match is logged but skipped to avoid auditing the wrong asset.
|
|
*/
|
|
|
|
import postgresClient from '@/lib/services/postgres-client';
|
|
|
|
export interface DattoSiteResolution {
|
|
datto_site_id: number;
|
|
datto_site_uid: string;
|
|
autotask_company_id: string | null;
|
|
autotask_company_name: string | null;
|
|
}
|
|
|
|
export async function resolveDattoSiteByUid(
|
|
siteUid: string
|
|
): Promise<DattoSiteResolution | null> {
|
|
// datto_rmm_sites.uid is the Datto-side site uuid; the autotask_company_id
|
|
// FK is sometimes null (only the company NAME is populated). Mirror the
|
|
// Phase 4.2 fallback: match by company name if FK is null.
|
|
const res = await postgresClient.query<{
|
|
id: number;
|
|
uid: string;
|
|
autotask_company_id: string | null;
|
|
autotask_company_name: string | null;
|
|
by_name_company_id: string | null;
|
|
}>(
|
|
`SELECT s.id,
|
|
s.uid,
|
|
s.autotask_company_id::text AS autotask_company_id,
|
|
s.autotask_company_name,
|
|
(SELECT c.id::text FROM companies c
|
|
WHERE LOWER(c.company_name) = LOWER(s.autotask_company_name)
|
|
LIMIT 1) AS by_name_company_id
|
|
FROM datto_rmm_sites s
|
|
WHERE s.uid = $1
|
|
LIMIT 1`,
|
|
[siteUid]
|
|
);
|
|
if (res.rowCount === 0) return null;
|
|
const r = res.rows[0];
|
|
return {
|
|
datto_site_id: r.id,
|
|
datto_site_uid: r.uid,
|
|
autotask_company_id: r.autotask_company_id ?? r.by_name_company_id,
|
|
autotask_company_name: r.autotask_company_name,
|
|
};
|
|
}
|
|
|
|
export async function resolveDattoDeviceByHostname(
|
|
siteId: number,
|
|
hostname: string
|
|
): Promise<{ uid: string; hostname: string | null; online: boolean } | null> {
|
|
const res = await postgresClient.query<{
|
|
uid: string;
|
|
hostname: string | null;
|
|
online: boolean;
|
|
}>(
|
|
`SELECT uid, hostname, online
|
|
FROM datto_rmm_devices
|
|
WHERE site_id = $1
|
|
AND LOWER(hostname) = LOWER($2)
|
|
LIMIT 1`,
|
|
[siteId, hostname]
|
|
);
|
|
return res.rowCount === 0 ? null : res.rows[0];
|
|
}
|
|
|
|
export interface ConfigurationMatch {
|
|
id: string;
|
|
hostname: string | null;
|
|
name: string;
|
|
/** True when the company has exactly one Configuration with this hostname. */
|
|
single_match: boolean;
|
|
}
|
|
|
|
export async function resolveConfigurationByHostname(
|
|
autotaskCompanyId: string | number,
|
|
hostname: string
|
|
): Promise<ConfigurationMatch | null> {
|
|
// Two-pass: count + fetch. Avoids ambiguous auto-audits when multiple
|
|
// Configurations share a hostname (rare but possible after a sync glitch).
|
|
const countRes = await postgresClient.query<{ n: string }>(
|
|
`SELECT COUNT(*)::text AS n
|
|
FROM itg_configurations c
|
|
JOIN companies comp ON LOWER(comp.company_name) = LOWER(c.organization_name)
|
|
WHERE comp.id = $1
|
|
AND (
|
|
LOWER(c.hostname) = LOWER($2)
|
|
OR LOWER(c.name) = LOWER($2)
|
|
)`,
|
|
[autotaskCompanyId, hostname]
|
|
);
|
|
const n = Number(countRes.rows[0]?.n ?? 0);
|
|
if (n === 0) return null;
|
|
|
|
const fetchRes = await postgresClient.query<{
|
|
id: string;
|
|
hostname: string | null;
|
|
name: string;
|
|
}>(
|
|
`SELECT c.id::text AS id, c.hostname, c.name
|
|
FROM itg_configurations c
|
|
JOIN companies comp ON LOWER(comp.company_name) = LOWER(c.organization_name)
|
|
WHERE comp.id = $1
|
|
AND (
|
|
LOWER(c.hostname) = LOWER($2)
|
|
OR LOWER(c.name) = LOWER($2)
|
|
)
|
|
LIMIT 1`,
|
|
[autotaskCompanyId, hostname]
|
|
);
|
|
if (fetchRes.rowCount === 0) return null;
|
|
const r = fetchRes.rows[0];
|
|
return {
|
|
id: r.id,
|
|
hostname: r.hostname,
|
|
name: r.name,
|
|
single_match: n === 1,
|
|
};
|
|
}
|