340 lines
9.6 KiB
TypeScript
340 lines
9.6 KiB
TypeScript
import {
|
|
DattoRMMConfig,
|
|
DattoRMMDevice,
|
|
DattoRMMSite,
|
|
DattoRMMApiResponse,
|
|
DattoRMMError,
|
|
} from '@/lib/types/datto-rmm';
|
|
|
|
export class DattoRMMClient {
|
|
private config: DattoRMMConfig;
|
|
private accessToken: string | null = null;
|
|
private tokenExpiry: Date | null = null;
|
|
|
|
constructor(config: DattoRMMConfig) {
|
|
this.config = config;
|
|
}
|
|
|
|
/**
|
|
* Get OAuth2 access token
|
|
* Datto RMM API v2 uses OAuth2 with password grant type
|
|
* The API Access Key is the username and API Secret Key is the password
|
|
*/
|
|
private async getAccessToken(): Promise<string> {
|
|
// Check if we have a valid token
|
|
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
|
return this.accessToken;
|
|
}
|
|
|
|
// OAuth2 token endpoint
|
|
const authUrl = 'https://concord-api.centrastage.net/auth/oauth/token';
|
|
|
|
// According to Datto RMM API v2 docs:
|
|
// - Basic auth with client_id: "public-client" and client_secret: "public"
|
|
// - grant_type: "password"
|
|
// - username: Your API Access Key
|
|
// - password: Your API Secret Key
|
|
const clientCredentials = Buffer.from('public-client:public').toString('base64');
|
|
|
|
const formData = new URLSearchParams();
|
|
formData.append('grant_type', 'password');
|
|
formData.append('username', this.config.apiKey);
|
|
formData.append('password', this.config.apiSecret);
|
|
|
|
try {
|
|
console.log('Authenticating with Datto RMM API v2...');
|
|
const response = await fetch(authUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Basic ${clientCredentials}`,
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: formData.toString(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
console.error('Datto RMM auth failed:', response.status, error);
|
|
throw new Error(`Authentication failed: ${response.status} - ${error}`);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(responseText);
|
|
} catch (parseError) {
|
|
console.error('Failed to parse auth response. Response was:', responseText.substring(0, 500));
|
|
throw new Error('Invalid response from auth server - expected JSON but got HTML/text');
|
|
}
|
|
|
|
this.accessToken = data.access_token;
|
|
|
|
// Set token expiry (usually 1 hour, but we'll refresh after 50 minutes to be safe)
|
|
this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000);
|
|
|
|
console.log('Successfully authenticated with Datto RMM');
|
|
return this.accessToken as string;
|
|
} catch (error) {
|
|
console.error('Failed to get Datto RMM access token:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make an authenticated API request
|
|
*/
|
|
private async makeApiCall<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const token = await this.getAccessToken();
|
|
|
|
// Use the base URL directly with v2 API
|
|
const url = `https://concord-api.centrastage.net/api/v2${endpoint}`;
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`API call failed: ${response.status} ${response.statusText}`);
|
|
console.error('Error response:', errorText);
|
|
|
|
let errorMessage = `API Error: ${response.status}`;
|
|
|
|
try {
|
|
const errorData = JSON.parse(errorText) as DattoRMMError;
|
|
errorMessage = `${errorData.error}: ${errorData.message}`;
|
|
} catch {
|
|
errorMessage += ` - ${errorText.substring(0, 200)}`;
|
|
}
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
if (!responseText) {
|
|
return {} as T;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(responseText) as T;
|
|
} catch (error) {
|
|
console.error('Failed to parse response:', responseText);
|
|
throw new Error('Invalid JSON response from API');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all sites
|
|
*/
|
|
async getSites(): Promise<DattoRMMSite[]> {
|
|
const response = await this.makeApiCall<any>(
|
|
'/account/sites',
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
// The API returns sites in a 'sites' field
|
|
return response.sites || [];
|
|
}
|
|
|
|
/**
|
|
* Get site by name (for matching with Autotask company)
|
|
*/
|
|
async getSiteByName(name: string): Promise<DattoRMMSite | null> {
|
|
const sites = await this.getSites();
|
|
const site = sites.find(s =>
|
|
s.name.toLowerCase() === name.toLowerCase() ||
|
|
s.name.toLowerCase().includes(name.toLowerCase())
|
|
);
|
|
|
|
return site || null;
|
|
}
|
|
|
|
/**
|
|
* Get all devices
|
|
*/
|
|
async getAllDevices(): Promise<DattoRMMDevice[]> {
|
|
const response = await this.makeApiCall<any>(
|
|
'/account/devices',
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
// The API returns devices in a 'devices' field
|
|
return response.devices || [];
|
|
}
|
|
|
|
/**
|
|
* Get devices for a specific site
|
|
*/
|
|
async getDevicesBySite(siteUid: string): Promise<DattoRMMDevice[]> {
|
|
const response = await this.makeApiCall<any>(
|
|
`/site/${siteUid}/devices`,
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
// The API returns devices in a 'devices' field
|
|
return response.devices || [];
|
|
}
|
|
|
|
/**
|
|
* Get devices by site name (matches with company name)
|
|
*/
|
|
async getDevicesByCompanyName(companyName: string): Promise<DattoRMMDevice[]> {
|
|
// First, find the site that matches the company name
|
|
const site = await this.getSiteByName(companyName);
|
|
|
|
if (!site) {
|
|
console.warn(`No RMM site found for company: ${companyName}`);
|
|
return [];
|
|
}
|
|
|
|
// Then get devices for that site using the UID
|
|
return this.getDevicesBySite(site.uid);
|
|
}
|
|
|
|
/**
|
|
* Get device by ID
|
|
*/
|
|
async getDeviceById(deviceId: string): Promise<DattoRMMDevice | null> {
|
|
try {
|
|
const response = await this.makeApiCall<{ item: DattoRMMDevice }>(
|
|
`/devices/${deviceId}`,
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
return response.item || null;
|
|
} catch (error) {
|
|
console.error(`Failed to get device ${deviceId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search devices by various criteria
|
|
*/
|
|
async searchDevices(criteria: {
|
|
hostname?: string;
|
|
serialNumber?: string;
|
|
ipAddress?: string;
|
|
macAddress?: string;
|
|
}): Promise<DattoRMMDevice[]> {
|
|
const allDevices = await this.getAllDevices();
|
|
|
|
return allDevices.filter(device => {
|
|
if (criteria.hostname &&
|
|
!device.hostname.toLowerCase().includes(criteria.hostname.toLowerCase())) {
|
|
return false;
|
|
}
|
|
|
|
if (criteria.serialNumber &&
|
|
device.serialNumber !== criteria.serialNumber) {
|
|
return false;
|
|
}
|
|
|
|
if (criteria.ipAddress &&
|
|
device.intIpAddress !== criteria.ipAddress &&
|
|
device.extIpAddress !== criteria.ipAddress) {
|
|
return false;
|
|
}
|
|
|
|
if (criteria.macAddress &&
|
|
!device.macAddresses.some(mac =>
|
|
mac.toLowerCase() === criteria.macAddress!.toLowerCase()
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get device audit data with detailed hardware information
|
|
*/
|
|
async getDeviceAudit(deviceId: string | number): Promise<any> {
|
|
try {
|
|
const response = await this.makeApiCall<any>(
|
|
`/device/${deviceId}/auditdata`,
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error(`Failed to get audit for device ${deviceId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get device with audit data
|
|
*/
|
|
async getDeviceWithAudit(deviceId: string | number): Promise<DattoRMMDevice | null> {
|
|
try {
|
|
// Get basic device info
|
|
const device = await this.getDeviceById(deviceId.toString());
|
|
if (!device) return null;
|
|
|
|
// Try to get audit data for more details
|
|
const audit = await this.getDeviceAudit(deviceId);
|
|
if (audit) {
|
|
// Merge audit data into device
|
|
if (audit.bios) {
|
|
device.manufacturer = audit.bios.manufacturer || device.manufacturer;
|
|
device.model = audit.bios.model || device.model;
|
|
device.serialNumber = audit.bios.serialNumber || device.serialNumber;
|
|
}
|
|
|
|
if (audit.system) {
|
|
device.manufacturer = audit.system.manufacturer || device.manufacturer;
|
|
device.model = audit.system.model || device.model;
|
|
}
|
|
|
|
if (audit.processors && audit.processors.length > 0) {
|
|
device.cpuName = audit.processors[0].name;
|
|
device.cpuCores = audit.processors[0].cores;
|
|
}
|
|
|
|
if (audit.memory) {
|
|
device.memory = audit.memory.totalPhysicalMemory;
|
|
}
|
|
|
|
if (audit.disks && audit.disks.length > 0) {
|
|
// Sum up all disk sizes
|
|
device.diskSize = audit.disks.reduce((total: number, disk: any) =>
|
|
total + (disk.size || 0), 0
|
|
);
|
|
}
|
|
}
|
|
|
|
return device;
|
|
} catch (error) {
|
|
console.error(`Failed to get device with audit ${deviceId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get device alerts
|
|
*/
|
|
async getDeviceAlerts(deviceId: string): Promise<any[]> {
|
|
try {
|
|
const response = await this.makeApiCall<DattoRMMApiResponse<any>>(
|
|
`/devices/${deviceId}/alerts`,
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
return response.items || [];
|
|
} catch (error) {
|
|
console.error(`Failed to get alerts for device ${deviceId}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
}
|