import { NextRequest, NextResponse } from 'next/server'; import { Pool } from 'pg'; import { getAutotaskClient } from '@/lib/services/autotask-factory'; import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory'; import { getAuvikClient } from '@/lib/services/auvik-factory'; import { getAddigyClient } from '@/lib/services/addigy-factory'; import { apiCache } from '@/lib/services/cache'; import { DattoRMMDevice } from '@/lib/types/datto-rmm'; import { AuvikDevice } from '@/lib/types/auvik'; import { AddigyDevice } from '@/lib/types/addigy'; import { ConfigurationItem } from '@/lib/types/autotask'; 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, }); interface DeviceComparison { autotaskDevice?: ConfigurationItem; rmmDevice?: DattoRMMDevice; auvikDevice?: AuvikDevice; addigyDevice?: AddigyDevice; status: 'matched' | 'autotask-only' | 'rmm-only'; matchedBy?: string; // What field was used to match } // Helper function to normalize MAC address for comparison function normalizeMacAddress(mac: string): string { return mac.replace(/[:-]/g, '').toLowerCase(); } // Helper function to match Addigy device to Autotask device function matchAddigyDevice( autotaskDevice: ConfigurationItem, addigyDevices: AddigyDevice[] ): AddigyDevice | null { // Priority 1: Serial number (primary matching method for Apple devices) if (autotaskDevice.serialNumber) { const autotaskSerial = autotaskDevice.serialNumber?.toLowerCase().trim(); console.log(`Trying to match Autotask device "${autotaskDevice.referenceTitle}" with serial: ${autotaskSerial}`); console.log(`Checking against ${addigyDevices.length} Addigy devices`); const match = addigyDevices.find((d) => { const addigySerial = d['Serial Number']?.toLowerCase().trim(); if (addigySerial) { console.log(` Comparing with Addigy device "${d['Device Name']}" serial: ${addigySerial}`); } return addigySerial === autotaskSerial; }); if (match) { console.log( `✓ Matched Addigy device by serial: ${match['Device Name']} (${match['Serial Number']}) -> ${autotaskDevice.referenceTitle} (${autotaskDevice.serialNumber})` ); return match; } else { console.log(`✗ No Addigy serial match found for ${autotaskDevice.serialNumber}`); } } // Priority 2: Device name/hostname const hostname = autotaskDevice.rmmDeviceAuditHostname || autotaskDevice.referenceTitle; if (hostname) { const match = addigyDevices.find( (d) => d['Device Name']?.toLowerCase().trim() === hostname.toLowerCase().trim() ); if (match) { console.log( `Matched Addigy device by name: ${match['Device Name']} -> ${autotaskDevice.referenceTitle}` ); return match; } } return null; } // Helper function to match Auvik device to Autotask device function matchAuvikDevice( autotaskDevice: ConfigurationItem, auvikDevices: AuvikDevice[] ): AuvikDevice | null { // Priority 1: Serial number if (autotaskDevice.serialNumber) { const match = auvikDevices.find( (d) => d.serialNumber?.toLowerCase().trim() === autotaskDevice.serialNumber?.toLowerCase().trim() ); if (match) { console.log( `Matched Auvik device by serial: ${match.deviceName} -> ${autotaskDevice.referenceTitle}` ); return match; } } // Priority 2: Hostname const hostname = autotaskDevice.rmmDeviceAuditHostname || autotaskDevice.referenceTitle; if (hostname) { const match = auvikDevices.find( (d) => d.deviceName?.toLowerCase().trim() === hostname.toLowerCase().trim() ); if (match) { console.log( `Matched Auvik device by hostname: ${match.deviceName} -> ${autotaskDevice.referenceTitle}` ); return match; } } // Priority 3: MAC address const macAddress = autotaskDevice.rmmDeviceAuditMacAddress; if (macAddress && macAddress.length > 0) { const normalizedMac = normalizeMacAddress(macAddress); const match = auvikDevices.find((d) => d.macAddresses?.some( (mac) => normalizeMacAddress(mac) === normalizedMac ) ); if (match) { console.log( `Matched Auvik device by MAC: ${match.deviceName} -> ${autotaskDevice.referenceTitle}` ); return match; } } return null; } export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; const companyId = searchParams.get('companyId'); const companyName = searchParams.get('companyName'); const activeFilter = searchParams.get('activeFilter') || 'active'; const skipCache = searchParams.get('skipCache') === 'true'; // Get mapping counts to include in cache key (so cache invalidates when mappings change) let rmmMappingCount = 0; let auvikMappingCount = 0; let addigyMappingCount = 0; if (companyId) { const rmmMappingsResult = await pool.query( 'SELECT COUNT(*) as count FROM rmm_site_mappings WHERE company_id = $1', [parseInt(companyId)] ); rmmMappingCount = parseInt(rmmMappingsResult.rows[0]?.count || '0'); const auvikMappingsResult = await pool.query( 'SELECT COUNT(*) as count FROM auvik_tenant_mappings WHERE autotask_company_id = $1', [parseInt(companyId)] ); auvikMappingCount = parseInt(auvikMappingsResult.rows[0]?.count || '0'); const addigyMappingsResult = await pool.query( 'SELECT COUNT(*) as count FROM addigy_org_mappings WHERE autotask_company_id = $1', [parseInt(companyId)] ); addigyMappingCount = parseInt(addigyMappingsResult.rows[0]?.count || '0'); } // Check cache first (unless skipCache is true) const cacheKey = `rmm-devices:${companyId}:${activeFilter}:rmm-${rmmMappingCount}:auvik-${auvikMappingCount}:addigy-${addigyMappingCount}`; if (!skipCache) { const cached = apiCache.get(cacheKey); if (cached) { console.log(`Cache hit for ${cacheKey}`); return NextResponse.json(cached); } } else { console.log(`Skipping cache for ${cacheKey}`); } if (!companyId) { return NextResponse.json({ rmmDevices: [], autotaskDevices: [], comparison: [], message: 'Please select a company to view devices' }); } // Get Autotask devices based on active filter const autotaskClient = getAutotaskClient(); let autotaskDevices: ConfigurationItem[] = []; if (activeFilter === 'all') { // Get all devices regardless of status const allItems = await autotaskClient.queryEntity('ConfigurationItems', { filter: [{ op: 'eq', field: 'companyID', value: parseInt(companyId) }], }); autotaskDevices = allItems; } else if (activeFilter === 'inactive') { // Get only inactive devices const inactiveItems = await autotaskClient.queryEntity('ConfigurationItems', { filter: [ { op: 'eq', field: 'companyID', value: parseInt(companyId) }, { op: 'eq', field: 'isActive', value: false } ], }); autotaskDevices = inactiveItems; } else { // Default: get only active devices autotaskDevices = await autotaskClient.getConfigurationItemsByCompany(parseInt(companyId)); } // Get RMM devices let rmmDevices: DattoRMMDevice[] = []; try { const rmmClient = getDattoRMMClient(); // First, check if we have site mappings for this company if (companyId) { const mappingsResult = await pool.query( 'SELECT rmm_site_uid FROM rmm_site_mappings WHERE company_id = $1', [parseInt(companyId)] ); if (mappingsResult.rows.length > 0) { // Use the new multi-site method if mappings exist const siteUids = mappingsResult.rows.map(row => row.rmm_site_uid); console.log(`Found ${siteUids.length} mapped RMM sites for company ${companyId}`); rmmDevices = await rmmClient.getDevicesForSites(siteUids); } else if (companyName) { // Fall back to old method if no mappings exist console.log(`No RMM site mappings found for company ${companyId}, using name-based matching`); rmmDevices = await rmmClient.getDevicesByCompanyName(companyName); } } else if (companyName) { // Try to get devices by company name (matching site name) rmmDevices = await rmmClient.getDevicesByCompanyName(companyName); } else { // If no company info, get all devices and try to match rmmDevices = await rmmClient.getAllDevices(); } // Filter RMM devices based on activeFilter if (activeFilter === 'active') { // Only show non-deleted, non-suspended RMM devices when filtering for active rmmDevices = rmmDevices.filter(device => !device.deleted && !device.suspended); } else if (activeFilter === 'inactive') { // Only show deleted or suspended RMM devices when filtering for inactive rmmDevices = rmmDevices.filter(device => device.deleted || device.suspended); } // If 'all', show all RMM devices (no filtering) // Deduplicate RMM devices by ID (in case the same device appears in multiple sites) const uniqueRmmDevices = new Map(); rmmDevices.forEach(device => { const deviceId = String(device.id); if (!uniqueRmmDevices.has(deviceId)) { uniqueRmmDevices.set(deviceId, device); } }); rmmDevices = Array.from(uniqueRmmDevices.values()); console.log(`After deduplication: ${rmmDevices.length} unique RMM devices`) } catch (rmmError) { console.error('Error fetching RMM devices:', rmmError); // Continue with empty RMM devices array } // Get Auvik devices let auvikDevices: AuvikDevice[] = []; try { const auvikClient = getAuvikClient(); if (companyId) { // Try to find tenant using company ID mapping first (most accurate) const tenant = await auvikClient.findTenantByCompanyId(parseInt(companyId)); if (tenant) { console.log(`Found Auvik tenant via mapping: ${tenant.domainPrefix} for company ID: ${companyId}`); auvikDevices = await auvikClient.getDevicesByTenant(tenant.id); } else if (companyName) { // Fallback to name-based matching console.log(`No mapping found, trying name match for: ${companyName}`); const tenantByName = await auvikClient.findTenantByName(companyName); if (tenantByName) { console.log(`Found Auvik tenant by name: ${tenantByName.domainPrefix} for company: ${companyName}`); auvikDevices = await auvikClient.getDevicesByTenant(tenantByName.id); } else { console.log(`No Auvik tenant found for company: ${companyName}`); } } } else if (companyName) { // If no company ID, try name matching const tenant = await auvikClient.findTenantByName(companyName); if (tenant) { console.log(`Found Auvik tenant: ${tenant.domainPrefix} for company: ${companyName}`); auvikDevices = await auvikClient.getDevicesByTenant(tenant.id); } } else { // If no company info, get all devices auvikDevices = await auvikClient.getAllDevices(); } console.log(`Fetched ${auvikDevices.length} Auvik devices before filtering`); // Filter Auvik devices to only show those with valid hostnames // Exclude devices without deviceName or with names starting with "Device@" auvikDevices = auvikDevices.filter(device => { if (!device.deviceName) { return false; } if (device.deviceName.startsWith('Device@')) { return false; } return true; }); console.log(`Filtered to ${auvikDevices.length} Auvik devices with valid hostnames`); } catch (auvikError) { console.error('Error fetching Auvik devices:', auvikError); // Continue with empty Auvik devices array } // Get Addigy devices (Apple RMM) let addigyDevices: AddigyDevice[] = []; try { const addigyClient = getAddigyClient(); if (companyId) { // Try to find policy using company ID mapping first const mappingsResult = await pool.query( 'SELECT addigy_org_id FROM addigy_org_mappings WHERE autotask_company_id = $1', [parseInt(companyId)] ); if (mappingsResult.rows.length > 0) { // Get all devices and filter by policy IDs in code const policyIds = new Set(mappingsResult.rows.map(row => row.addigy_org_id)); console.log(`Found ${policyIds.size} mapped Addigy policies for company ${companyId}:`, Array.from(policyIds)); // Fetch all devices (without filter) const allDevices = await addigyClient.getAllDevices(); console.log(`Fetched ${allDevices.length} total Addigy devices`); // Filter devices by policy_id in code addigyDevices = allDevices.filter(device => policyIds.has(device.policy_id)); console.log(`Filtered to ${addigyDevices.length} devices matching mapped policies`); } else { console.log(`No Addigy policy mappings found for company ${companyId}`); } } console.log(`Final Addigy devices count: ${addigyDevices.length}`); } catch (addigyError) { console.error('Error fetching Addigy devices:', addigyError); // Continue with empty Addigy devices array } // Compare and match devices const comparison: DeviceComparison[] = []; const matchedAutotaskIds = new Set(); const matchedRmmIds = new Set(); // Try to match devices for (const rmmDevice of rmmDevices) { // Skip if this RMM device has already been matched if (matchedRmmIds.has(String(rmmDevice.id))) { continue; } let matched = false; // Try to match by RMM Device UID if (rmmDevice.uid) { const autotaskMatch = autotaskDevices.find( at => at.rmmDeviceUID === rmmDevice.uid && !matchedAutotaskIds.has(at.id) ); if (autotaskMatch) { comparison.push({ autotaskDevice: autotaskMatch, rmmDevice: rmmDevice, status: 'matched', matchedBy: 'RMM UID' }); matchedAutotaskIds.add(autotaskMatch.id); matchedRmmIds.add(String(rmmDevice.id)); matched = true; } } // Try to match by serial number if (!matched && rmmDevice.serialNumber) { const autotaskMatch = autotaskDevices.find( at => (at.serialNumber === rmmDevice.serialNumber || at.dattoSerialNumber === rmmDevice.serialNumber) && !matchedAutotaskIds.has(at.id) ); if (autotaskMatch) { comparison.push({ autotaskDevice: autotaskMatch, rmmDevice: rmmDevice, status: 'matched', matchedBy: 'Serial Number' }); matchedAutotaskIds.add(autotaskMatch.id); matchedRmmIds.add(String(rmmDevice.id)); matched = true; } } // Try to match by hostname if (!matched && rmmDevice.hostname) { const autotaskMatch = autotaskDevices.find( at => (at.rmmDeviceAuditHostname?.toLowerCase() === rmmDevice.hostname.toLowerCase() || at.dattoHostname?.toLowerCase() === rmmDevice.hostname.toLowerCase() || at.referenceTitle?.toLowerCase().includes(rmmDevice.hostname.toLowerCase())) && !matchedAutotaskIds.has(at.id) ); if (autotaskMatch) { comparison.push({ autotaskDevice: autotaskMatch, rmmDevice: rmmDevice, status: 'matched', matchedBy: 'Hostname' }); matchedAutotaskIds.add(autotaskMatch.id); matchedRmmIds.add(String(rmmDevice.id)); matched = true; } } // Try to match by IP address if (!matched && (rmmDevice.intIpAddress || rmmDevice.extIpAddress)) { const autotaskMatch = autotaskDevices.find( at => (at.rmmDeviceAuditIPAddress === rmmDevice.intIpAddress || at.rmmDeviceAuditIPAddress === rmmDevice.extIpAddress || at.dattoInternalIP === rmmDevice.intIpAddress || at.dattoRemoteIP === rmmDevice.extIpAddress) && !matchedAutotaskIds.has(at.id) ); if (autotaskMatch) { comparison.push({ autotaskDevice: autotaskMatch, rmmDevice: rmmDevice, status: 'matched', matchedBy: 'IP Address' }); matchedAutotaskIds.add(autotaskMatch.id); matchedRmmIds.add(String(rmmDevice.id)); matched = true; } } // If no match found, add as RMM-only if (!matched) { comparison.push({ rmmDevice: rmmDevice, status: 'rmm-only' }); } } // Add Autotask-only devices and match with Auvik and Addigy for (const autotaskDevice of autotaskDevices) { if (!matchedAutotaskIds.has(autotaskDevice.id)) { // Try to match with Auvik device const auvikMatch = matchAuvikDevice(autotaskDevice, auvikDevices); // Try to match with Addigy device const addigyMatch = matchAddigyDevice(autotaskDevice, addigyDevices); comparison.push({ autotaskDevice: autotaskDevice, auvikDevice: auvikMatch || undefined, addigyDevice: addigyMatch || undefined, status: 'autotask-only' }); } else { // For already matched devices, also try to match with Auvik and Addigy const existingComparison = comparison.find( (c) => c.autotaskDevice?.id === autotaskDevice.id ); if (existingComparison) { if (!existingComparison.auvikDevice) { const auvikMatch = matchAuvikDevice(autotaskDevice, auvikDevices); if (auvikMatch) { existingComparison.auvikDevice = auvikMatch; } } if (!existingComparison.addigyDevice) { const addigyMatch = matchAddigyDevice(autotaskDevice, addigyDevices); if (addigyMatch) { existingComparison.addigyDevice = addigyMatch; } } } } } // Track which Auvik and Addigy devices have been matched const matchedAuvikIds = new Set(); const matchedAddigyIds = new Set(); comparison.forEach(item => { if (item.auvikDevice?.id) { matchedAuvikIds.add(item.auvikDevice.id); } if (item.addigyDevice?.agentid) { matchedAddigyIds.add(item.addigyDevice.agentid); } }); // Skip unmatched Auvik devices (NMS-only) - don't add them to comparison // They will still be counted in stats but won't appear in the device list for (const auvikDevice of auvikDevices) { if (!matchedAuvikIds.has(auvikDevice.id)) { // Mark as matched so it's counted but don't add to comparison matchedAuvikIds.add(auvikDevice.id); } } // Add unmatched Addigy devices (ARMM-only) for (const addigyDevice of addigyDevices) { if (!matchedAddigyIds.has(addigyDevice.agentid)) { comparison.push({ addigyDevice: addigyDevice, status: 'rmm-only' // Using rmm-only status for non-PSA devices }); } } // Sort comparison results comparison.sort((a, b) => { // Sort by status first (matched, then autotask-only, then rmm-only) const statusOrder = { 'matched': 0, 'autotask-only': 1, 'rmm-only': 2 }; const statusDiff = statusOrder[a.status] - statusOrder[b.status]; if (statusDiff !== 0) return statusDiff; // Then sort by device name (check all possible sources) const aName = a.autotaskDevice?.referenceTitle || a.rmmDevice?.hostname || a.auvikDevice?.deviceName || a.addigyDevice?.['Device Name'] || ''; const bName = b.autotaskDevice?.referenceTitle || b.rmmDevice?.hostname || b.auvikDevice?.deviceName || b.addigyDevice?.['Device Name'] || ''; return aName.localeCompare(bName); }); // Fetch contacts for the company to avoid individual API calls const contacts: Record = {}; try { const contactIds = new Set(); autotaskDevices.forEach(device => { if (device.contactID) { contactIds.add(device.contactID); } }); if (contactIds.size > 0) { console.log(`Fetching ${contactIds.size} contacts for company ${companyId}`); // Fetch all contacts for the company in one query const companyContacts = await autotaskClient.queryEntity('Contacts', { filter: [{ op: 'eq', field: 'companyID', value: parseInt(companyId) }], }); // Map contacts by ID companyContacts.forEach((contact: any) => { contacts[contact.id] = contact; }); console.log(`Fetched ${Object.keys(contacts).length} contacts`); } } catch (contactError) { console.error('Error fetching contacts:', contactError); // Continue without contacts } const response = { rmmDevices, autotaskDevices, auvikDevices, addigyDevices, comparison, contacts, // Include contacts in response stats: { totalRmm: rmmDevices.length, totalAutotask: autotaskDevices.length, totalAuvik: auvikDevices.length, totalAddigy: addigyDevices.length, matched: comparison.filter(c => c.status === 'matched').length, autotaskOnly: comparison.filter(c => c.status === 'autotask-only').length, rmmOnly: comparison.filter(c => c.status === 'rmm-only').length, } }; // Cache for 2 minutes apiCache.set(cacheKey, response, 120); // 120 seconds = 2 minutes return NextResponse.json(response); } catch (error) { console.error('Error in RMM devices endpoint:', error); if (error instanceof Error) { return NextResponse.json( { error: 'Failed to fetch devices', message: error.message, }, { status: 500 } ); } return NextResponse.json( { error: 'An unexpected error occurred' }, { status: 500 } ); } }