collabSupport/integrations/mdm/client.js
jmcqueen f06dd2d09a Add bulk store iPad reboot script with WS1 SoftReset support.
Fleet-wide dry-run/execute script for MRD/MR1/CD iPads, plus MDM client helper and filter tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 12:24:57 -04:00

260 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/integrations/mdm/client.js
// VMware Workspace ONE / AirWatch MDM integration
import axios from 'axios';
import { logger } from '../../utils/logger.js';
const MDM_BASE_URL = 'https://as1991.awmdm.com';
const TOKEN_URL = 'https://na.uemauth.workspaceone.com/connect/token';
// Dedicated axios instance
export const mdmAxios = axios.create({
baseURL: MDM_BASE_URL,
timeout: 12000,
headers: {
'Accept': 'application/json',
},
});
// ──────────────────────────────────────────────
// Get OAuth token (client_credentials flow)
// ──────────────────────────────────────────────
export async function getMDMToken() {
logger('mdm:token', 'Requesting new MDM OAuth token', 'debug');
try {
const response = await axios.post(
TOKEN_URL,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.WS1_CLIENT_ID,
client_secret: process.env.WS1_CLIENT_SECRET,
}),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}
);
const accessToken = response.data.access_token;
logger('mdm:token', `MDM token acquired successfully (length: ${accessToken.length})`, 'debug');
return accessToken;
} catch (err) {
const msg = err.response
? `${err.response.status} ${JSON.stringify(err.response.data)}`
: err.message;
logger('mdm:token', `Failed to acquire MDM token: ${msg}`, 'error');
throw new Error(`MDM token fetch failed: ${msg}`);
}
}
// ──────────────────────────────────────────────
// Get MDM devices for a store
// ──────────────────────────────────────────────
export async function getMDMDevices2(storeNum) {
logger('mdm:device', `Fetching devices for store ${storeNum}`, 'debug');
const storeNumPadded = String(storeNum).padStart(6, '0');
try {
const accessToken = await getMDMToken();
logger('mdm:device', `Searching devices for user/store: ${storeNumPadded}`, 'debug');
const response = await axios.get('/api/mdm/devices/search', {
baseURL: MDM_BASE_URL,
params: { user: storeNumPadded },
headers: {
Authorization: `Bearer ${accessToken}`,
'aw-tenant-code': process.env.WS1_TENANT_CODE,
Accept: 'application/json'
}
});
let devices = response.data.Devices || [];
logger('mdm:device', `Raw devices returned from MDM: ${devices.length}`, 'debug');
logger('mdm:client', `Fetched ${response.data.Devices?.length || 0} devices from MDM`);
const result = devices.map(d => ({
friendlyName: d.DeviceFriendlyName || 'Unknown',
serialNumber: d.SerialNumber || '—',
lastSeen: d.LastSeen || d.LastSystemSampleTime || 'Unknown',
locationGroup: d.LocationGroupId?.Name || d.LocationGroupName || 'Unknown',
orgGroupId: parseInt(d.OrganizationalGroupID || d.orgGroupId || d.OrganizationalGroup || d.groupId || 0, 10)
}));
logger('mdm:device', `Returning ${result.length} MDM devices for store ${storeNum}`, 'debug');
return result;
} catch (err) {
logger('mdm:device', `Error fetching devices for store ${storeNum}: ${err.message}`, 'error');
if (err.response) {
logger('mdm:device', `MDM API error status: ${err.response.status}`, 'error');
}
return [];
}
}
export async function getMDMDevices(storeNum) {
logger('mdm:device', `Fetching devices for store ${storeNum}`, 'debug');
const storeNumPadded = String(storeNum).padStart(6, '0');
try {
const accessToken = await getMDMToken();
logger('mdm:device', `Searching devices for user/store: ${storeNumPadded}`, 'debug');
const response = await axios.get('/api/mdm/devices/search', {
baseURL: MDM_BASE_URL,
params: { user: storeNumPadded },
headers: {
Authorization: `Bearer ${accessToken}`,
'aw-tenant-code': process.env.WS1_TENANT_CODE,
Accept: 'application/json'
}
});
let devices = response.data.Devices || [];
logger('mdm:device', `Raw devices returned from MDM: ${devices.length}`, 'debug');
logger('mdm:device', `Returning ${devices.length} MDM devices for store ${storeNum}`, 'debug');
return devices;
} catch (err) {
logger('mdm:device', `Error fetching devices for store ${storeNum}: ${err.message}`, 'error');
if (err.response) {
logger('mdm:device', `MDM API error status: ${err.response.status}`, 'error');
}
return [];
}
}
/**
* Get ALL devices with NO filtering whatsoever
* No platform parameter, no client-side filtering.
*/
export async function getMDMDevicesByPlatform(platformFilter = null, maxDevices = 9999) {
logger('mdm:device', `Fetching ALL devices (broad search, no filters at all)`);
let allDevices = [];
let page = 0;
const pageSize = 500;
try {
while (allDevices.length < maxDevices) {
const accessToken = await getMDMToken();
const response = await axios.get('/api/mdm/devices/search', {
baseURL: 'https://as1991.awmdm.com',
params: {
page: page,
page_size: pageSize,
platform: platformFilter
// NO platform, NO group, NO other filters
},
headers: {
Authorization: `Bearer ${accessToken}`,
'aw-tenant-code': process.env.WS1_TENANT_CODE,
Accept: 'application/json'
}
});
const pageDevices = response.data.Devices || response.data.devices || response.data.results || [];
allDevices = allDevices.concat(pageDevices);
logger('mdm:device', `Page ${page} returned ${pageDevices.length} devices (total so far: ${allDevices.length})`);
if (pageDevices.length < pageSize) break;
page++;
}
logger('mdm:device', `Broad fetch complete ${allDevices.length} total devices returned (NO filtering applied)`);
return allDevices.slice(0, maxDevices);
} catch (err) {
logger('mdm:device', `Broad fetch failed: ${err.message}`, 'error');
return [];
}
}
/**
* Returns ONLY Audio-Visual devices from MDM (VW, MSC, LED in name)
* Used specifically for the AV modal view
*/
export async function getAVMDMDevices(storeNum) {
logger('mdm:av', `Fetching AV devices (VW/MSC/LED) for store ${storeNum}`, 'debug');
const allDevices = await getMDMDevices(storeNum);
if (!allDevices || allDevices.length === 0) return [];
const AV_PATTERN = /(VW|MSC|LED|AppleTV)/i;
const avDevices = allDevices.filter(device => {
const name = (device.UserName || '').toUpperCase();
return AV_PATTERN.test(name);
});
logger('mdm:av', `MDM AV filter: ${allDevices.length} total → ${avDevices.length} AV devices`, 'debug');
return avDevices;
}
/**
* Send a SoftReset (reboot) command via Workspace ONE UEM.
* Mirrors appspace/mdm.js sendMDMRebootCommand — proven in production.
*
* @param {string|number} deviceId WS1 numeric Id (device.Id.Value), NOT serial
* @returns {Promise<{ success: boolean, status?: number, error?: string }>}
*/
export async function sendMDMRebootCommand(deviceId) {
if (!deviceId) {
return { success: false, error: 'No WS1 deviceId supplied' };
}
const url = `${MDM_BASE_URL}/api/mdm/devices/commands`;
const params = { command: 'SoftReset', searchBy: 'DeviceId', id: deviceId };
async function doRequest(token) {
return axios.post(url, null, {
params,
headers: {
Authorization: `Bearer ${token}`,
'aw-tenant-code': process.env.WS1_TENANT_CODE,
Accept: 'application/json',
},
timeout: 20000,
});
}
let token;
try {
token = await getMDMToken();
} catch (err) {
return { success: false, error: `Could not obtain MDM token: ${err.message}` };
}
try {
const resp = await doRequest(token);
return { success: true, status: resp.status };
} catch (err) {
const status = err.response?.status;
if (status === 401 || status === 403) {
logger('mdm:reboot', `Auth failed on SoftReset for ${deviceId} — retrying once`, 'warn');
try {
const retryResp = await doRequest(await getMDMToken());
return { success: true, status: retryResp.status };
} catch (retryErr) {
return {
success: false,
status: retryErr.response?.status,
error: retryErr.response?.data?.message
|| retryErr.response?.data?.errorCode
|| retryErr.message,
};
}
}
return {
success: false,
status,
error: err.response?.data?.message || err.response?.data?.errorCode || err.message,
};
}
}
export default mdmAxios;