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>
260 lines
8.7 KiB
JavaScript
260 lines
8.7 KiB
JavaScript
// 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;
|