3 * @description MeshCentral integration routes for web-based remote desktop access with multi-tenant isolation.
4 * Each tenant has dedicated user account and device group. Supports session generation, auto-linking,
5 * device sync, hardware data population, and installer distribution. Includes both manual and automated
6 * workflows for connecting agents to MeshCentral nodes.
7 * @module routes/meshcentral
11 * @apiDefine MeshCentralGroup MeshCentral Remote Desktop Integration
12 * Web-based remote desktop via MeshCentral with multi-tenant isolation
16 * MeshCentral Integration Routes
17 * Provides web-based remote desktop access with true multi-tenant isolation
20const express = require('express');
21const router = express.Router();
22const crypto = require('crypto');
23const authenticateToken = require('../middleware/auth');
24const db = require('../db');
25const MeshCentralAPI = require('../lib/meshcentral-api');
26const { encrypt, decrypt } = require('../lib/encryption');
28const MESHCENTRAL_URL = process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app';
29const MESHCENTRAL_ADMIN_USERNAME = process.env.MESHCENTRAL_USERNAME || 'admin';
30const MESHCENTRAL_ADMIN_PASSWORD = process.env.MESHCENTRAL_PASSWORD;
32// Cache for tenant-specific MeshCentral API clients
33const tenantAPIClients = new Map();
36 * Get MeshCentral API client for a specific tenant
37 * Each tenant has their own user account and device group in MeshCentral
40async function getTenantMeshAPI(tenantId) {
41 // Root tenant (MSP) uses admin account
42 if (tenantId === 1 || tenantId === '00000000-0000-0000-0000-000000000001') {
43 return getAdminMeshAPI();
47 const cacheKey = `tenant_${tenantId}`;
48 if (tenantAPIClients.has(cacheKey)) {
49 return tenantAPIClients.get(cacheKey);
52 // Get tenant's MeshCentral credentials from database
53 const tenantResult = await db.query(
54 'SELECT meshcentral_username, meshcentral_password_encrypted, meshcentral_setup_complete FROM tenants WHERE tenant_id = $1',
58 if (tenantResult.rows.length === 0) {
59 throw new Error('Tenant not found');
62 const tenant = tenantResult.rows[0];
64 // If tenant doesn't have MeshCentral setup yet, create it
65 if (!tenant.meshcentral_setup_complete) {
66 console.log(`Setting up MeshCentral for tenant ${tenantId}...`);
67 await setupTenantMeshCentral(tenantId);
69 // Fetch updated credentials
70 const updatedResult = await db.query(
71 'SELECT meshcentral_username, meshcentral_password_encrypted FROM tenants WHERE tenant_id = $1',
74 tenant.meshcentral_username = updatedResult.rows[0].meshcentral_username;
75 tenant.meshcentral_password_encrypted = updatedResult.rows[0].meshcentral_password_encrypted;
79 const password = decrypt(tenant.meshcentral_password_encrypted);
82 throw new Error('Failed to decrypt MeshCentral password');
85 // Create API client with tenant credentials
86 const api = new MeshCentralAPI(MESHCENTRAL_URL, tenant.meshcentral_username, password);
89 // Cache for future requests
90 tenantAPIClients.set(cacheKey, api);
96 * Get admin MeshCentral API client (for root tenant/MSP)
98let adminMeshAPI = null;
99let adminLoginPromise = null;
104async function getAdminMeshAPI() {
106 adminMeshAPI = new MeshCentralAPI(MESHCENTRAL_URL, MESHCENTRAL_ADMIN_USERNAME, MESHCENTRAL_ADMIN_PASSWORD);
109 // Only login once, even if multiple requests come in
110 if (!adminLoginPromise) {
111 adminLoginPromise = adminMeshAPI.login().then(success => {
112 adminLoginPromise = null;
117 await adminLoginPromise;
122 * Setup MeshCentral for a new tenant (create user + device group)
125async function setupTenantMeshCentral(tenantId) {
127 // Get tenant details
128 const tenantResult = await db.query(
129 'SELECT tenant_id, subdomain, status FROM tenants WHERE tenant_id = $1',
133 if (tenantResult.rows.length === 0) {
134 throw new Error('Tenant not found');
137 const tenant = tenantResult.rows[0];
139 // Use admin API to create tenant user and mesh
140 const adminAPI = await getAdminMeshAPI();
142 const setup = await adminAPI.setupTenant({
143 tenantId: tenant.tenant_id,
144 tenantName: tenant.subdomain || `Tenant ${tenantId}`,
145 subdomain: tenant.subdomain
148 // Encrypt password before storing
149 const encryptedPassword = encrypt(setup.password);
151 // Store credentials in database
154 SET meshcentral_username = $1,
155 meshcentral_password_encrypted = $2,
156 meshcentral_group_id = $3,
157 meshcentral_group_name = $4,
158 meshcentral_setup_complete = true
159 WHERE tenant_id = $5`,
160 [setup.username, encryptedPassword, setup.meshId, setup.meshName, tenantId]
163 console.log(`✅ MeshCentral setup complete for tenant ${tenantId}`);
167 console.error(`Failed to setup MeshCentral for tenant ${tenantId}:`, error.message);
173 * @api {post} /api/meshcentral/session/:agentId Generate MeshCentral Session
174 * @apiName CreateMeshCentralSession
175 * @apiGroup MeshCentralGroup
176 * @apiDescription Generates secure MeshCentral session with JWT token authentication for iframe embedding.
177 * Returns authenticated URL with embedded token, session tracking, and 4-hour expiration. Supports
178 * multiple view modes (General, Desktop, Terminal). Enforces tenant isolation.
179 * @apiParam {string} agentId Agent ID or UUID
180 * @apiParam {number} [viewMode=11] View mode (10=General, 11=Desktop, 12=Terminal, etc.)
181 * @apiSuccess {boolean} success Session created successfully
182 * @apiSuccess {string} sessionToken Session tracking token
183 * @apiSuccess {string} expiresAt Session expiration timestamp (4 hours)
184 * @apiSuccess {object} agent Agent details
185 * @apiSuccess {string} agent.id Agent database ID
186 * @apiSuccess {string} agent.uuid Agent UUID
187 * @apiSuccess {string} agent.hostname Agent hostname
188 * @apiSuccess {string} agent.platform Agent platform/OS
189 * @apiSuccess {string} agent.nodeId MeshCentral node ID
190 * @apiSuccess {string} iframeUrl Complete iframe URL with embedded JWT token
191 * @apiSuccess {number} viewMode View mode used
192 * @apiError 404 Agent not found
193 * @apiError 400 Agent not connected to MeshCentral (no node ID)
194 * @apiError 401 No JWT token provided
195 * @apiError 500 Failed to create session
196 * @apiExample {curl} Example:
197 * curl -X POST https://api.example.com/api/meshcentral/session/abc-123 \
198 * -H "Authorization: Bearer YOUR_TOKEN" \
199 * -H "Content-Type: application/json" \
200 * -d '{"viewMode":11}'
201 * @apiExample {json} Success-Response (200):
204 * "sessionToken": "abc123...",
205 * "expiresAt": "2026-03-12T18:00:00Z",
209 * "hostname": "DESKTOP-01",
210 * "platform": "Windows 10",
211 * "nodeId": "node//abc123..."
213 * "iframeUrl": "https://meshcentral.example.com/?token=eyJhbGc...&gotonode=node//abc123&viewmode=11&hide=31",
218 * Generate secure MeshCentral session for dashboard
219 * Returns an authenticated iframe URL with embedded session cookie
221router.post('/session/:agentId', authenticateToken, async (req, res) => {
223 const { agentId } = req.params;
224 const { viewMode = 11 } = req.body; // 10=General, 11=Desktop, 12=Terminal, etc.
226 // Extract userId from JWT (could be user_id, userId, or id)
227 const userId = req.user.user_id || req.user.userId || req.user.id;
229 console.log(`🔍 Session request: agentId=${agentId}, userId=${userId}, tenantId=${req.user.tenantId}, user=`, req.user);
231 // Get agent details with tenant isolation
232 // Note: tenant_id is UUID in agents table, tenantId from JWT is also UUID
233 // Root tenant UUID is '00000000-0000-0000-0000-000000000001'
234 const agentResult = await db.query(
235 'SELECT agent_id, agent_uuid, hostname, meshcentral_nodeid, platform, tenant_id FROM agents WHERE (agent_id::text = $1 OR agent_uuid::text = $1) AND (tenant_id::text = $2 OR $2 = \'00000000-0000-0000-0000-000000000001\')',
236 [agentId, req.user.tenantId]
239 console.log(`🔍 Agent query result: ${agentResult.rows.length} rows`);
241 if (agentResult.rows.length === 0) {
242 return res.status(404).json({ error: 'Agent not found' });
245 const agent = agentResult.rows[0];
247 if (!agent.meshcentral_nodeid) {
248 return res.status(400).json({
249 error: 'Agent not connected to MeshCentral',
250 message: 'Please install MeshCentral agent on this device first'
254 // Get JWT token from request authorization header
255 const authHeader = req.headers.authorization;
256 if (!authHeader || !authHeader.startsWith('Bearer ')) {
257 return res.status(401).json({ error: 'No JWT token provided' });
260 const jwtToken = authHeader.substring(7); // Remove 'Bearer ' prefix
261 console.log(`🔍 Using JWT token for authentication: ${jwtToken.substring(0, 20)}...`);
263 // Generate a secure session token for tracking
264 const sessionToken = crypto.randomBytes(32).toString('hex');
265 const expiresAt = new Date(Date.now() + 4 * 60 * 60 * 1000); // 4 hours
267 // Build authenticated MeshCentral URL with JWT token
268 // The forked MeshCentral supports ?token=JWT_TOKEN for authentication
269 let meshcentralUrl = `${MESHCENTRAL_URL}/?token=${encodeURIComponent(jwtToken)}`;
271 // Add node and view mode parameters
272 meshcentralUrl += `&gotonode=${agent.meshcentral_nodeid}`;
273 meshcentralUrl += `&viewmode=${viewMode}`;
274 meshcentralUrl += `&hide=31`; // Hide UI elements (header=1, tabs=2, footer=4, title=8, toolbar=16)
276 console.log(`🔍 Generated MeshCentral URL: ${MESHCENTRAL_URL}/?token=***&gotonode=${agent.meshcentral_nodeid}&viewmode=${viewMode}&hide=31`);
278 // Store session in database for audit trail
279 console.log(`🔍 Storing session: token=${sessionToken}, userId=${userId}, agentId=${agent.agent_id}, nodeId=${agent.meshcentral_nodeid}`);
282 INSERT INTO meshcentral_sessions (
283 session_token, user_id, agent_id, node_id, view_mode,
284 meshcentral_cookie, expires_at, created_at
285 ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
286 ON CONFLICT (session_token) DO UPDATE
287 SET expires_at = $7, last_used_at = NOW()
288 `, [sessionToken, userId, agent.agent_id, agent.meshcentral_nodeid, viewMode, jwtToken, expiresAt]);
290 console.log(`✅ MeshCentral session created: user=${userId}, agent=${agent.agent_id}, node=${agent.meshcentral_nodeid}`);
298 uuid: agent.agent_uuid,
299 hostname: agent.hostname,
300 platform: agent.platform,
301 nodeId: agent.meshcentral_nodeid
303 iframeUrl: meshcentralUrl,
308 console.error('❌ Error creating MeshCentral session:', error);
309 console.error('Stack trace:', error.stack);
310 res.status(500).json({ error: 'Failed to create session', message: error.message });
315 * Proxy MeshCentral requests through backend (keeps credentials secure)
316 * NOTE: This is a stub - full HTTP/WebSocket proxy needs implementation
318router.use('/proxy/:sessionToken', async (req, res, next) => {
320 const { sessionToken } = req.params;
321 const proxyPath = req.path || '';
323 // Validate session and ensure tenant isolation
324 const sessionResult = await db.query(`
325 SELECT s.*, a.meshcentral_nodeid, a.tenant_id
326 FROM meshcentral_sessions s
327 JOIN agents a ON s.agent_id = a.agent_id
328 JOIN users u ON s.user_id = u.user_id
329 WHERE s.session_token = $1
330 AND s.expires_at > NOW()
331 AND (a.tenant_id = u.tenant_id OR u.tenant_id = 1)
334 if (sessionResult.rows.length === 0) {
335 return res.status(401).json({ error: 'Invalid or expired session' });
338 const session = sessionResult.rows[0];
340 // Get MeshCentral API client
341 const api = await getMeshAPI();
343 // Proxy the request to MeshCentral
344 const meshUrl = `${MESHCENTRAL_URL}/${proxyPath}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`;
346 // For now, return the proxy setup info
347 // In production, you'd actually proxy the HTTP request
351 nodeId: session.meshcentral_nodeid,
352 viewMode: session.view_mode,
353 message: 'Proxy endpoint - implement full HTTP proxying here'
358 console.error('Error proxying MeshCentral request:', error);
359 res.status(500).json({ error: 'Proxy error', message: error.message });
364 * @api {post} /api/meshcentral/connect/:agentId Connect to Agent (Legacy)
365 * @apiName ConnectToAgent
366 * @apiGroup MeshCentralGroup
367 * @apiDescription Legacy endpoint for getting MeshCentral iframe URL with embedded authentication.
368 * Less secure than /session endpoint. Generates login token and stores in database.
369 * @apiParam {string} agentId Agent database ID
370 * @apiSuccess {boolean} success Connection created successfully
371 * @apiSuccess {object} agent Agent details
372 * @apiSuccess {number} agent.id Agent database ID
373 * @apiSuccess {string} agent.hostname Agent hostname
374 * @apiSuccess {string} agent.platform Agent platform/OS
375 * @apiSuccess {string} agent.nodeId MeshCentral node ID
376 * @apiSuccess {string} embedUrl MeshCentral embed URL with token
377 * @apiSuccess {string} token Login token
378 * @apiSuccess {string} expiresAt Token expiration timestamp
379 * @apiError 404 Agent not found
380 * @apiError 400 Agent not connected to MeshCentral
381 * @apiError 500 Failed to create connection
382 * @apiExample {curl} Example:
383 * curl -X POST https://api.example.com/api/meshcentral/connect/123 \
384 * -H "Authorization: Bearer YOUR_TOKEN"
387 * Get MeshCentral iframe URL with embedded auth (legacy, less secure)
389router.post('/connect/:agentId', authenticateToken, async (req, res) => {
391 const { agentId } = req.params;
394 const agentResult = await db.query(
395 'SELECT agent_id, hostname, meshcentral_nodeid, platform FROM agents WHERE agent_id = $1 AND tenant_id = $2',
396 [agentId, req.user.tenantId]
399 if (agentResult.rows.length === 0) {
400 return res.status(404).json({ error: 'Agent not found' });
403 const agent = agentResult.rows[0];
405 if (!agent.meshcentral_nodeid) {
406 return res.status(400).json({
407 error: 'Agent not connected to MeshCentral',
408 message: 'Please install MeshCentral agent on this device first'
412 // Get MeshCentral API client
413 const api = await getMeshAPI();
415 // Generate login token and embed URL
416 const tokenData = await api.generateLoginToken(agent.meshcentral_nodeid);
418 // Store token in our database
420 INSERT INTO meshcentral_tokens (token, agent_id, user_id, node_id, expires_at)
421 VALUES ($1, $2, $3, $4, $5)
422 `, [tokenData.token, agentId, req.user.userId, agent.meshcentral_nodeid, tokenData.expiresAt]);
428 hostname: agent.hostname,
429 platform: agent.platform,
430 nodeId: agent.meshcentral_nodeid
432 embedUrl: tokenData.embedUrl,
433 token: tokenData.token,
434 expiresAt: tokenData.expiresAt
438 console.error('Error creating MeshCentral connection:', error);
439 res.status(500).json({ error: 'Failed to create connection', message: error.message });
444 * @api {get} /api/meshcentral/status/:agentId Get Agent MeshCentral Status
445 * @apiName GetAgentMeshCentralStatus
446 * @apiGroup MeshCentralGroup
447 * @apiDescription Retrieves MeshCentral connection status for agent. Includes both database cache and live
448 * status from MeshCentral server if available. Enforces tenant isolation.
449 * @apiParam {string} agentId Agent database ID
450 * @apiSuccess {boolean} success Status retrieved successfully
451 * @apiSuccess {boolean} installed MeshCentral agent installed (has node ID)
452 * @apiSuccess {boolean} connected Currently connected to MeshCentral
453 * @apiSuccess {string} lastSeen Last connection timestamp
454 * @apiSuccess {string} nodeId MeshCentral node ID
455 * @apiSuccess {object} [liveStatus] Live status from MeshCentral server (if available)
456 * @apiError 404 Agent not found
457 * @apiError 500 Failed to check status
458 * @apiExample {curl} Example:
459 * curl https://api.example.com/api/meshcentral/status/123 \
460 * -H "Authorization: Bearer YOUR_TOKEN"
461 * @apiExample {json} Success-Response (200):
466 * "lastSeen": "2026-03-12T14:30:00Z",
467 * "nodeId": "node//abc123...",
468 * "liveStatus": {"connected": true, "lastSeen": "2026-03-12T14:30:00Z"}
472 * Get agent MeshCentral status
474router.get('/status/:agentId', authenticateToken, async (req, res) => {
476 const { agentId } = req.params;
478 const result = await db.query(
479 'SELECT meshcentral_nodeid, meshcentral_connected, meshcentral_last_seen FROM agents WHERE agent_id = $1 AND tenant_id = $2',
480 [agentId, req.user.tenantId]
483 if (result.rows.length === 0) {
484 return res.status(404).json({ error: 'Agent not found' });
487 const agent = result.rows[0];
489 // If we have a node ID, try to get live status from MeshCentral
490 let liveStatus = null;
491 if (agent.meshcentral_nodeid) {
493 const api = await getMeshAPI();
494 const node = await api.getNode(agent.meshcentral_nodeid);
496 liveStatus = MeshCentralAPI.parseNodeData(node);
499 console.warn('Failed to get live MeshCentral status:', error.message);
505 installed: !!agent.meshcentral_nodeid,
506 connected: liveStatus?.connected || agent.meshcentral_connected || false,
507 lastSeen: liveStatus?.lastSeen || agent.meshcentral_last_seen,
508 nodeId: agent.meshcentral_nodeid,
509 liveStatus: liveStatus
513 console.error('Error checking MeshCentral status:', error);
514 res.status(500).json({ error: 'Failed to check status' });
519 * @api {get} /api/meshcentral/installer Get Agent Installer URL
520 * @apiName GetMeshCentralInstaller
521 * @apiGroup MeshCentralGroup
522 * @apiDescription Returns MeshCentral agent installer download URL for specified platform and mesh ID.
523 * Requires mesh ID query parameter.
524 * @apiParam (Query) {string} meshId MeshCentral mesh/group ID (required)
525 * @apiParam (Query) {string} [platform=windows] Target platform (windows, linux, macos)
526 * @apiSuccess {boolean} success Installer URL retrieved successfully
527 * @apiSuccess {string} platform Target platform
528 * @apiSuccess {string} installerUrl Download URL for installer
529 * @apiSuccess {string} instructions Installation instructions
530 * @apiError 400 Missing meshId
531 * @apiError 500 Failed to get installer URL
532 * @apiExample {curl} Example:
533 * curl "https://api.example.com/api/meshcentral/installer?meshId=mesh123&platform=windows" \
534 * -H "Authorization: Bearer YOUR_TOKEN"
535 * @apiExample {json} Success-Response (200):
538 * "platform": "windows",
539 * "installerUrl": "https://meshcentral.example.com/meshagents?id=mesh123...",
540 * "instructions": "Download and run this installer on the target windows machine"
544 * Get MeshCentral agent installer
546router.get('/installer', authenticateToken, async (req, res) => {
548 const { platform = 'windows', meshId } = req.query;
551 return res.status(400).json({
552 error: 'meshId required',
553 message: 'Please provide the MeshCentral mesh ID'
557 const api = await getMeshAPI();
558 const installerUrl = api.getAgentInstallerUrl(meshId, platform);
564 instructions: `Download and run this installer on the target ${platform} machine`
568 console.error('Error getting MeshCentral installer:', error);
569 res.status(500).json({ error: 'Failed to get installer URL' });
574 * @api {get} /api/meshcentral/devices Get All MeshCentral Devices
575 * @apiName GetMeshCentralDevices
576 * @apiGroup MeshCentralGroup
577 * @apiDescription Retrieves all MeshCentral devices with tenant filtering. Root tenant (tenant_id=1) sees all
578 * devices, other tenants see only their linked agents. Returns parsed device data.
579 * @apiSuccess {boolean} success Devices retrieved successfully
580 * @apiSuccess {number} count Number of devices returned
581 * @apiSuccess {object[]} devices Array of device objects
582 * @apiSuccess {string} devices.nodeId MeshCentral node ID
583 * @apiSuccess {string} devices.hostname Device hostname
584 * @apiSuccess {string} devices.platform Device platform/OS
585 * @apiSuccess {boolean} devices.connected Connection status
586 * @apiSuccess {string} devices.lastSeen Last connection timestamp
587 * @apiSuccess {string} tenantId Current tenant ID
588 * @apiSuccess {boolean} isRootTenant Whether current tenant is root/MSP
589 * @apiError 500 Failed to get devices
590 * @apiExample {curl} Example:
591 * curl https://api.example.com/api/meshcentral/devices \
592 * -H "Authorization: Bearer YOUR_TOKEN"
593 * @apiExample {json} Success-Response (200):
597 * "devices": [{"nodeId": "node//abc", "hostname": "DESKTOP-01", "connected": true}],
598 * "tenantId": "00000000-0000-0000-0000-000000000001",
599 * "isRootTenant": true
603 * Get all MeshCentral devices (for sync)
604 * Root tenant (tenant_id=1) sees all devices, others see only their own
606router.get('/devices', authenticateToken, async (req, res) => {
608 const api = await getMeshAPI();
609 const nodes = await api.getNodes();
612 const parsedNodes = nodes.map(node => MeshCentralAPI.parseNodeData(node));
614 // For non-root tenants, filter to only their agents
615 let filteredNodes = parsedNodes;
616 if (req.user.tenantId !== 1) {
617 // Get list of node IDs for this tenant's agents
618 const tenantAgents = await db.query(
619 'SELECT meshcentral_nodeid FROM agents WHERE tenant_id = $1 AND meshcentral_nodeid IS NOT NULL',
622 const tenantNodeIds = new Set(tenantAgents.rows.map(a => a.meshcentral_nodeid));
623 filteredNodes = parsedNodes.filter(node => tenantNodeIds.has(node.nodeId));
628 count: filteredNodes.length,
629 devices: filteredNodes,
630 tenantId: req.user.tenantId,
631 isRootTenant: req.user.tenantId === 1
635 console.error('Error getting MeshCentral devices:', error);
636 res.status(500).json({ error: 'Failed to get devices', message: error.message });
641 * @api {get} /api/meshcentral/meshes Get All Device Groups
642 * @apiName GetMeshCentralMeshes
643 * @apiGroup MeshCentralGroup
644 * @apiDescription Retrieves all MeshCentral device groups (meshes) from server. No tenant filtering.
645 * @apiSuccess {boolean} success Meshes retrieved successfully
646 * @apiSuccess {number} count Number of device groups
647 * @apiSuccess {object[]} meshes Array of mesh/group objects
648 * @apiError 500 Failed to get meshes
649 * @apiExample {curl} Example:
650 * curl https://api.example.com/api/meshcentral/meshes \
651 * -H "Authorization: Bearer YOUR_TOKEN"
652 * @apiExample {json} Success-Response (200):
656 * "meshes": [{"_id": "mesh//abc", "name": "Tenant 1 Devices"}]
660 * Get all MeshCentral device groups (meshes)
662router.get('/meshes', authenticateToken, async (req, res) => {
664 const api = await getMeshAPI();
665 const meshes = await api.getMeshes();
669 count: meshes.length,
674 console.error('Error getting MeshCentral meshes:', error);
675 res.status(500).json({ error: 'Failed to get meshes', message: error.message });
680 * @api {post} /api/meshcentral/sync-with-hardware Sync Devices with Hardware Data
681 * @apiName SyncMeshCentralDevicesWithHardware
682 * @apiGroup MeshCentralGroup
683 * @apiDescription Enhanced device sync that retrieves and stores detailed hardware information (CPU, memory, network).
684 * Auto-links devices by hostname or MAC address, populates hardware_data column. Creates new agents
685 * for unmatched devices. Tenant-filtered (non-root tenants sync only their linked agents).
686 * @apiSuccess {boolean} success Sync completed successfully
687 * @apiSuccess {number} synced Number of devices updated
688 * @apiSuccess {number} new Number of new agents created
689 * @apiSuccess {number} errors Number of errors encountered
690 * @apiSuccess {number} total Total devices processed
691 * @apiSuccess {object[]} matchDetails Details of matching methods used
692 * @apiSuccess {string} matchDetails.hostname Device hostname
693 * @apiSuccess {string} matchDetails.method Match method (existing, hostname, mac_address, new)
694 * @apiSuccess {number} [matchDetails.agentId] Agent database ID (if matched)
695 * @apiError 500 Failed to sync devices
696 * @apiExample {curl} Example:
697 * curl -X POST https://api.example.com/api/meshcentral/sync-with-hardware \
698 * -H "Authorization: Bearer YOUR_TOKEN"
699 * @apiExample {json} Success-Response (200):
707 * {"hostname": "DESKTOP-01", "method": "hostname", "agentId": 123}
712 * Enhanced sync with hardware data population
713 * POST /api/meshcentral/sync-with-hardware
715router.post('/sync-with-hardware', authenticateToken, async (req, res) => {
717 const api = await getMeshAPI();
718 let nodes = await api.getNodes();
720 // For non-root tenants, filter to only their existing agents
721 if (req.user.tenantId !== 1) {
722 const tenantAgents = await db.query(
723 'SELECT meshcentral_nodeid FROM agents WHERE tenant_id = $1 AND meshcentral_nodeid IS NOT NULL',
726 const tenantNodeIds = new Set(tenantAgents.rows.map(a => a.meshcentral_nodeid));
727 nodes = nodes.filter(node => {
728 const parsed = MeshCentralAPI.parseNodeData(node);
729 return tenantNodeIds.has(parsed.nodeId);
736 const matchDetails = [];
738 for (const node of nodes) {
740 const parsed = MeshCentralAPI.parseNodeData(node);
742 // Get detailed system info if available
745 sysinfo = await api.getNodeSystemInfo(parsed.nodeId);
747 console.log(`⚠️ Could not get sysinfo for ${parsed.hostname}:`, err.message);
750 const hardwareData = sysinfo ? MeshCentralAPI.parseSystemInfo(sysinfo) : null;
752 // Check if we already have an agent with this node ID
753 let existingAgent = await db.query(
754 'SELECT agent_id, hostname FROM agents WHERE meshcentral_nodeid = $1 AND (tenant_id = $2 OR $2 = 1)',
755 [parsed.nodeId, req.user.tenantId]
758 if (existingAgent.rows.length > 0) {
759 // Update existing agent with hardware data
762 SET meshcentral_connected = $1,
763 meshcentral_last_seen = $2,
764 hostname = COALESCE(hostname, $3),
765 platform = COALESCE(platform, $4),
766 last_seen = GREATEST(last_seen, $2),
767 hardware_data = COALESCE($5, hardware_data)
768 WHERE meshcentral_nodeid = $6
774 hardwareData ? JSON.stringify(hardwareData) : null,
778 matchDetails.push({ hostname: parsed.hostname, method: 'existing', agentId: existingAgent.rows[0].agent_id });
780 // Try to match by multiple identifiers
781 let matchedAgent = null;
782 let matchMethod = null;
784 // Try hostname match first
785 const hostnameMatch = await db.query(
786 'SELECT agent_id, hostname FROM agents WHERE LOWER(hostname) = LOWER($1) AND meshcentral_nodeid IS NULL AND (tenant_id = $2 OR $2 = 1) LIMIT 1',
787 [parsed.hostname, req.user.tenantId]
790 if (hostnameMatch.rows.length > 0) {
791 matchedAgent = hostnameMatch.rows[0];
792 matchMethod = 'hostname';
793 } else if (parsed.hardware.macAddresses.length > 0 && hardwareData) {
794 // Try MAC address match
795 // Note: This requires the hardware_data column to be populated with previous data
796 const macMatch = await db.query(`
797 SELECT agent_id, hostname, hardware_data
799 WHERE meshcentral_nodeid IS NULL
800 AND (tenant_id = $1 OR $1 = 1)
801 AND hardware_data IS NOT NULL
802 AND hardware_data::jsonb->'network' @> $2::jsonb
806 JSON.stringify([{ mac: parsed.hardware.macAddresses[0] }])
809 if (macMatch.rows.length > 0) {
810 matchedAgent = macMatch.rows[0];
811 matchMethod = 'mac_address';
816 // Link existing agent
819 SET meshcentral_nodeid = $1,
820 meshcentral_connected = $2,
821 meshcentral_last_seen = $3,
822 hardware_data = COALESCE($4, hardware_data)
828 hardwareData ? JSON.stringify(hardwareData) : null,
829 matchedAgent.agent_id
832 matchDetails.push({ hostname: parsed.hostname, method: matchMethod, agentId: matchedAgent.agent_id });
833 console.log(`✅ Auto-linked agent ${matchedAgent.agent_id} to MeshCentral node ${parsed.nodeId} (method: ${matchMethod})`);
835 // Create new agent entry
838 tenant_id, hostname, platform, agent_uuid,
839 meshcentral_nodeid, meshcentral_connected, meshcentral_last_seen,
840 last_seen, created_at, hardware_data
841 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), $9)
844 parsed.hostname || parsed.name,
845 parsed.platform || 'unknown',
846 parsed.nodeId, // Use node ID as agent UUID temporarily
850 parsed.lastSeen || new Date(),
851 hardwareData ? JSON.stringify(hardwareData) : null
854 matchDetails.push({ hostname: parsed.hostname, method: 'new', agentId: null });
858 console.error('Error syncing node:', node._id, err);
869 matchDetails: matchDetails
873 console.error('Error syncing MeshCentral devices with hardware:', error);
874 res.status(500).json({ error: 'Failed to sync devices', message: error.message });
879 * @api {post} /api/meshcentral/sync Sync MeshCentral Devices (Deprecated)
880 * @apiName SyncMeshCentralDevices
881 * @apiGroup MeshCentralGroup
882 * @apiDescription Basic device synchronization without hardware data. Auto-links devices by hostname.
883 * **DEPRECATED**: Use auto-sync worker (meshcentral-device-sync.js) or /api/sync/meshcentral instead.
884 * This endpoint will be removed in future version. Tenant-filtered.
885 * @apiSuccess {boolean} success Sync completed successfully
886 * @apiSuccess {number} synced Number of devices updated or linked
887 * @apiSuccess {number} new Number of new agents created
888 * @apiSuccess {number} errors Number of errors encountered
889 * @apiSuccess {number} total Total devices processed
890 * @apiError 500 Failed to sync devices
891 * @apiExample {curl} Example (deprecated):
892 * curl -X POST https://api.example.com/api/meshcentral/sync \
893 * -H "Authorization: Bearer YOUR_TOKEN"
896 * Sync MeshCentral devices to our agents table
897 * Root tenant (tenant_id=1) syncs all devices, others sync only their own
898 * @deprecated This endpoint is deprecated. Use the auto-sync worker instead:
899 * - Primary: meshcentral-device-sync.js (runs every 5 minutes automatically)
900 * - Manual: POST /api/sync/meshcentral (triggers immediate sync)
901 * - Backup: meshcentralSync.js BullMQ worker (runs daily at 3 AM)
903 * This endpoint will be removed in a future version. It still works but is
904 * no longer the recommended way to sync devices.
906router.post('/sync', authenticateToken, async (req, res) => {
908 const api = await getMeshAPI();
909 let nodes = await api.getNodes();
911 // For non-root tenants, filter to only their existing agents
912 if (req.user.tenantId !== 1) {
913 const tenantAgents = await db.query(
914 'SELECT meshcentral_nodeid FROM agents WHERE tenant_id = $1 AND meshcentral_nodeid IS NOT NULL',
917 const tenantNodeIds = new Set(tenantAgents.rows.map(a => a.meshcentral_nodeid));
918 nodes = nodes.filter(node => {
919 const parsed = MeshCentralAPI.parseNodeData(node);
920 return tenantNodeIds.has(parsed.nodeId);
928 for (const node of nodes) {
930 const parsed = MeshCentralAPI.parseNodeData(node);
932 // Check if we already have an agent with this node ID (within tenant scope)
933 const existingAgent = await db.query(
934 'SELECT agent_id FROM agents WHERE meshcentral_nodeid = $1 AND (tenant_id = $2 OR $2 = 1)',
935 [parsed.nodeId, req.user.tenantId]
938 if (existingAgent.rows.length > 0) {
939 // Update existing agent
942 SET meshcentral_connected = $1,
943 meshcentral_last_seen = $2,
944 hostname = COALESCE(hostname, $3),
945 platform = COALESCE(platform, $4),
946 last_seen = GREATEST(last_seen, $2)
947 WHERE meshcentral_nodeid = $5
957 // Try to match by hostname to link existing agents (within tenant scope)
958 const hostnameMatch = await db.query(
959 'SELECT agent_id FROM agents WHERE LOWER(hostname) = LOWER($1) AND meshcentral_nodeid IS NULL AND (tenant_id = $2 OR $2 = 1) LIMIT 1',
960 [parsed.hostname, req.user.tenantId]
963 if (hostnameMatch.rows.length > 0) {
964 // Link existing agent
966 'UPDATE agents SET meshcentral_nodeid = $1, meshcentral_connected = $2, meshcentral_last_seen = $3 WHERE agent_id = $4',
967 [parsed.nodeId, parsed.connected, parsed.lastSeen, hostnameMatch.rows[0].agent_id]
970 console.log(`✅ Auto-linked agent ${hostnameMatch.rows[0].agent_id} to MeshCentral node ${parsed.nodeId} (hostname: ${parsed.hostname})`);
972 // Create new agent entry
975 tenant_id, hostname, platform, agent_uuid,
976 meshcentral_nodeid, meshcentral_connected, meshcentral_last_seen,
977 last_seen, created_at
978 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
981 parsed.hostname || parsed.name,
982 parsed.platform || 'unknown',
983 parsed.nodeId, // Use node ID as agent UUID temporarily
987 parsed.lastSeen || new Date()
993 console.error('Error syncing node:', node._id, err);
1000 synced: syncedCount,
1007 console.error('Error syncing MeshCentral devices:', error);
1008 res.status(500).json({ error: 'Failed to sync devices', message: error.message });
1013 * @api {post} /api/meshcentral/auto-link Auto-Link Agent to MeshCentral Device
1014 * @apiName AutoLinkAgent
1015 * @apiGroup MeshCentralGroup
1016 * @apiDescription Automatically links RMM agent to MeshCentral device by hostname matching. Searches all
1017 * MeshCentral nodes for hostname match (case-insensitive). No authentication required for agent calls,
1018 * optional authentication for dashboard usage. Returns available nodes if no match found.
1019 * @apiParam {string} agentUuid RMM agent UUID (required)
1020 * @apiParam {string} [hardwareId] Hardware ID (unused)
1021 * @apiParam {string} [hostname] Override hostname for matching (uses agent's hostname if not provided)
1022 * @apiSuccess {boolean} success Link operation completed
1023 * @apiSuccess {boolean} [alreadyLinked] Agent already linked (true if already has node ID)
1024 * @apiSuccess {number} agentId Agent database ID
1025 * @apiSuccess {string} agentUuid Agent UUID
1026 * @apiSuccess {string} meshcentralNodeId MeshCentral node ID
1027 * @apiSuccess {string} matchMethod Matching method used ("hostname")
1028 * @apiSuccess {string} hostname Matched hostname
1029 * @apiSuccess {string} message Status message
1030 * @apiError 400 Missing agentUuid
1031 * @apiError 404 Agent not found or no matching MeshCentral node found
1032 * @apiError 500 Failed to auto-link
1033 * @apiExample {curl} Example:
1034 * curl -X POST https://api.example.com/api/meshcentral/auto-link \
1035 * -H "Content-Type: application/json" \
1036 * -d '{"agentUuid":"abc-123", "hostname":"DESKTOP-01"}'
1037 * @apiExample {json} Success-Response (200):
1041 * "agentUuid": "abc-123",
1042 * "meshcentralNodeId": "node//abc123...",
1043 * "matchMethod": "hostname",
1044 * "hostname": "DESKTOP-01",
1045 * "message": "Agents linked successfully"
1047 * @apiExample {json} Error-Response (404) - No Match:
1049 * "error": "No matching MeshCentral node found",
1050 * "message": "Please ensure MeshAgent is installed and connected. Looking for hostname: DESKTOP-01",
1051 * "availableNodes": [{"hostname": "DESKTOP-02", "nodeId": "node//abc", "connected": true}]
1055 * Auto-link MeshAgent with our agent by hostname or hardware ID
1057router.post('/auto-link', async (req, res) => {
1059 const { agentUuid, hardwareId, hostname } = req.body;
1062 return res.status(400).json({ error: 'agentUuid required' });
1065 // Get our agent (with tenant check if authenticated)
1066 const tenantFilter = req.user ? ' AND (tenant_id = $2 OR $2 = 1)' : '';
1067 const params = req.user ? [agentUuid, req.user.tenantId] : [agentUuid];
1069 const agentResult = await db.query(
1070 `SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_uuid = $1${tenantFilter}`,
1074 if (agentResult.rows.length === 0) {
1075 return res.status(404).json({ error: 'Agent not found' });
1078 const agent = agentResult.rows[0];
1080 // Check if already linked
1081 if (agent.meshcentral_nodeid) {
1084 alreadyLinked: true,
1085 agentId: agent.agent_id,
1086 meshcentralNodeId: agent.meshcentral_nodeid,
1087 message: 'Agent already linked to MeshCentral'
1091 // Get all MeshCentral nodes
1092 const api = await getMeshAPI();
1093 const nodes = await api.getNodes();
1095 // Find matching node by hostname (primary method)
1096 let matchedNode = null;
1097 const searchHostname = hostname || agent.hostname;
1099 for (const node of nodes) {
1100 const parsed = MeshCentralAPI.parseNodeData(node);
1102 // Match by hostname (case-insensitive)
1103 if (parsed.hostname && searchHostname &&
1104 parsed.hostname.toLowerCase() === searchHostname.toLowerCase()) {
1105 matchedNode = parsed;
1111 return res.status(404).json({
1112 error: 'No matching MeshCentral node found',
1113 message: `Please ensure MeshAgent is installed and connected. Looking for hostname: ${searchHostname}`,
1114 availableNodes: nodes.map(n => {
1115 const p = MeshCentralAPI.parseNodeData(n);
1116 return { hostname: p.hostname, nodeId: p.nodeId, connected: p.connected };
1121 // Link the two agents (ensure tenant ownership is preserved)
1122 const updateParams = req.user
1123 ? [matchedNode.nodeId, matchedNode.connected, matchedNode.lastSeen, agentUuid, req.user.tenantId]
1124 : [matchedNode.nodeId, matchedNode.connected, matchedNode.lastSeen, agentUuid];
1126 const updateQuery = req.user
1127 ? 'UPDATE agents SET meshcentral_nodeid = $1, meshcentral_connected = $2, meshcentral_last_seen = $3 WHERE agent_uuid = $4 AND (tenant_id = $5 OR $5 = 1)'
1128 : 'UPDATE agents SET meshcentral_nodeid = $1, meshcentral_connected = $2, meshcentral_last_seen = $3 WHERE agent_uuid = $4';
1130 await db.query(updateQuery, updateParams);
1134 agentId: agent.agent_id,
1135 agentUuid: agentUuid,
1136 meshcentralNodeId: matchedNode.nodeId,
1137 matchMethod: 'hostname',
1138 hostname: matchedNode.hostname,
1139 message: 'Agents linked successfully'
1143 console.error('Error auto-linking agents:', error);
1144 res.status(500).json({ error: 'Failed to auto-link', message: error.message });
1149 * @api {post} /api/meshcentral/setup-tenant/:tenantId Setup Tenant MeshCentral
1150 * @apiName SetupTenantMeshCentral
1151 * @apiGroup MeshCentralGroup
1152 * @apiDescription Creates isolated MeshCentral user account and device group for tenant. Only root tenant (MSP)
1153 * can setup other tenants. Generates credentials, creates mesh, and stores encrypted password in database.
1154 * Idempotent (returns existing setup if already complete).
1155 * @apiParam {string} tenantId Tenant UUID to setup
1156 * @apiSuccess {boolean} success Setup completed successfully
1157 * @apiSuccess {string} message Status message
1158 * @apiSuccess {string} username MeshCentral username for tenant
1159 * @apiSuccess {string} meshId MeshCentral mesh/group ID
1160 * @apiSuccess {string} meshName Mesh/group name
1161 * @apiError 403 Only MSP can setup tenants (non-root tenant attempted)
1162 * @apiError 404 Tenant not found
1163 * @apiError 500 Failed to setup tenant
1164 * @apiExample {curl} Example:
1165 * curl -X POST https://api.example.com/api/meshcentral/setup-tenant/tenant-uuid-123 \
1166 * -H "Authorization: Bearer YOUR_TOKEN"
1167 * @apiExample {json} Success-Response (200):
1170 * "message": "Tenant MeshCentral setup complete",
1171 * "username": "tenant_tenant-uuid-123",
1172 * "meshId": "mesh//abc123...",
1173 * "meshName": "Tenant 1 Devices"
1177 * Setup MeshCentral for a tenant
1178 * Creates isolated user account and device group
1180 * POST /api/meshcentral/setup-tenant/:tenantId
1182router.post('/setup-tenant/:tenantId', authenticateToken, async (req, res) => {
1184 // Only root tenant (MSP) can setup other tenants
1185 if (req.user.tenantId !== 1 && req.user.tenantId !== '00000000-0000-0000-0000-000000000001') {
1186 return res.status(403).json({ error: 'Only MSP can setup tenants' });
1189 const { tenantId } = req.params;
1191 // Check if already setup
1192 const checkResult = await db.query(
1193 'SELECT meshcentral_setup_complete, meshcentral_username FROM tenants WHERE tenant_id = $1',
1197 if (checkResult.rows.length === 0) {
1198 return res.status(404).json({ error: 'Tenant not found' });
1201 if (checkResult.rows[0].meshcentral_setup_complete) {
1204 message: 'Tenant already setup',
1205 username: checkResult.rows[0].meshcentral_username
1210 const setup = await setupTenantMeshCentral(tenantId);
1214 message: 'Tenant MeshCentral setup complete',
1215 username: setup.username,
1216 meshId: setup.meshId,
1217 meshName: setup.meshName
1221 console.error('Error setting up tenant:', error);
1222 res.status(500).json({ error: 'Failed to setup tenant', message: error.message });
1227 * @api {get} /api/meshcentral/installer/:tenantId Get Tenant Agent Installer URL
1228 * @apiName GetTenantInstallerURL
1229 * @apiGroup MeshCentralGroup
1230 * @apiDescription Returns tenant-specific MeshAgent installer download URL configured for tenant's mesh group.
1231 * Includes platform-specific installation instructions. Verifies tenant access (tenant can get own
1232 * installer, MSP can get any).
1233 * @apiParam {string} tenantId Tenant UUID
1234 * @apiParam (Query) {string} [platform=win32] Target platform (win32, linux, darwin)
1235 * @apiSuccess {boolean} success Installer URL retrieved successfully
1236 * @apiSuccess {string} installerUrl Direct download URL for agent installer
1237 * @apiSuccess {string} meshId Tenant's MeshCentral mesh/group ID
1238 * @apiSuccess {string} platform Target platform
1239 * @apiSuccess {object} instructions Platform-specific installation commands
1240 * @apiSuccess {string} instructions.windows Windows installation command
1241 * @apiSuccess {string} instructions.linux Linux installation command
1242 * @apiSuccess {string} instructions.macos macOS installation command
1243 * @apiError 403 Access denied (tenant accessing another tenant's installer)
1244 * @apiError 404 Tenant not found
1245 * @apiError 400 Tenant MeshCentral not setup (run setup-tenant first)
1246 * @apiError 500 Failed to get installer URL
1247 * @apiExample {curl} Example:
1248 * curl "https://api.example.com/api/meshcentral/installer/tenant-uuid-123?platform=win32" \
1249 * -H "Authorization: Bearer YOUR_TOKEN"
1250 * @apiExample {json} Success-Response (200):
1253 * "installerUrl": "https://meshcentral.example.com/meshagents?id=mesh123&installflags=0&meshinstall=meshagent.exe",
1254 * "meshId": "mesh//abc123...",
1255 * "platform": "win32",
1257 * "windows": "Download and run: https://...",
1258 * "linux": "wget https://... && chmod +x meshagent && sudo ./meshagent -install",
1259 * "macos": "curl -O https://... && chmod +x meshagent && sudo ./meshagent -install"
1264 * Get tenant's MeshAgent installer URL
1265 * Returns download link for agent installer configured for tenant's mesh
1267 * GET /api/meshcentral/installer/:tenantId
1269router.get('/installer/:tenantId', authenticateToken, async (req, res) => {
1271 const { tenantId } = req.params;
1272 const { platform = 'win32' } = req.query;
1274 // Verify access (tenant can get their own, MSP can get any)
1275 if (req.user.tenantId !== tenantId &&
1276 req.user.tenantId !== 1 &&
1277 req.user.tenantId !== '00000000-0000-0000-0000-000000000001') {
1278 return res.status(403).json({ error: 'Access denied' });
1281 // Get tenant's mesh ID
1282 const tenantResult = await db.query(
1283 'SELECT meshcentral_group_id, meshcentral_setup_complete FROM tenants WHERE tenant_id = $1',
1287 if (tenantResult.rows.length === 0) {
1288 return res.status(404).json({ error: 'Tenant not found' });
1291 if (!tenantResult.rows[0].meshcentral_setup_complete) {
1292 return res.status(400).json({
1293 error: 'Tenant MeshCentral not setup',
1294 message: 'Run setup-tenant first'
1298 const meshId = tenantResult.rows[0].meshcentral_group_id;
1300 // Build installer URL
1301 const platformMap = {
1302 'win32': 'meshagent.exe',
1303 'linux': 'meshagent',
1304 'darwin': 'meshagent'
1307 const filename = platformMap[platform] || 'meshagent.exe';
1308 const installerUrl = `${MESHCENTRAL_URL}/meshagents?id=${meshId}&installflags=0&meshinstall=${filename}`;
1312 installerUrl: installerUrl,
1316 windows: `Download and run: ${installerUrl}`,
1317 linux: `wget ${installerUrl} && chmod +x meshagent && sudo ./meshagent -install`,
1318 macos: `curl -O ${installerUrl} && chmod +x meshagent && sudo ./meshagent -install`
1323 console.error('Error getting installer URL:', error);
1324 res.status(500).json({ error: 'Failed to get installer URL', message: error.message });
1329 * @api {post} /api/meshcentral/manual-link Manually Link Agent to Device
1330 * @apiName ManualLinkAgent
1331 * @apiGroup MeshCentralGroup
1332 * @apiDescription Manually links RMM agent to specific MeshCentral device by node ID. Overrides any existing
1333 * link. Enforces tenant isolation. Useful when auto-link fails or for correcting incorrect links.
1334 * @apiParam {string} agentUuid RMM agent UUID (required)
1335 * @apiParam {string} meshcentralNodeId MeshCentral device node ID (required)
1336 * @apiSuccess {boolean} success Link operation completed
1337 * @apiSuccess {number} agentId Agent database ID
1338 * @apiSuccess {string} agentUuid Agent UUID
1339 * @apiSuccess {string} hostname Agent hostname
1340 * @apiSuccess {string} oldNodeId Previous MeshCentral node ID (null if not linked before)
1341 * @apiSuccess {string} newNodeId New MeshCentral node ID
1342 * @apiSuccess {string} message Success message
1343 * @apiError 400 Missing agentUuid or meshcentralNodeId
1344 * @apiError 404 Agent not found
1345 * @apiError 500 Failed to link agent
1346 * @apiExample {curl} Example:
1347 * curl -X POST https://api.example.com/api/meshcentral/manual-link \
1348 * -H "Authorization: Bearer YOUR_TOKEN" \
1349 * -H "Content-Type: application/json" \
1350 * -d '{"agentUuid":"abc-123", "meshcentralNodeId":"node//abc123..."}'
1351 * @apiExample {json} Success-Response (200):
1355 * "agentUuid": "abc-123",
1356 * "hostname": "DESKTOP-01",
1357 * "oldNodeId": null,
1358 * "newNodeId": "node//abc123...",
1359 * "message": "Agent linked to MeshCentral device successfully"
1363 * Manually link an agent to a MeshCentral device
1364 * POST /api/meshcentral/manual-link
1365 * Body: { agentUuid, meshcentralNodeId }
1367router.post('/manual-link', authenticateToken, async (req, res) => {
1369 const { agentUuid, meshcentralNodeId } = req.body;
1371 if (!agentUuid || !meshcentralNodeId) {
1372 return res.status(400).json({ error: 'agentUuid and meshcentralNodeId required' });
1375 // Get our agent (with tenant check)
1376 const agentResult = await db.query(
1377 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_uuid = $1 AND (tenant_id = $2 OR $2 = 1)',
1378 [agentUuid, req.user.tenantId]
1381 if (agentResult.rows.length === 0) {
1382 return res.status(404).json({ error: 'Agent not found' });
1385 const agent = agentResult.rows[0];
1386 const oldNodeId = agent.meshcentral_nodeid;
1388 // Update the agent with new node ID
1391 SET meshcentral_nodeid = $1,
1392 meshcentral_connected = true,
1393 meshcentral_last_seen = NOW()
1394 WHERE agent_uuid = $2 AND (tenant_id = $3 OR $3 = 1)`,
1395 [meshcentralNodeId, agentUuid, req.user.tenantId]
1400 agentId: agent.agent_id,
1401 agentUuid: agentUuid,
1402 hostname: agent.hostname,
1403 oldNodeId: oldNodeId || null,
1404 newNodeId: meshcentralNodeId,
1405 message: 'Agent linked to MeshCentral device successfully'
1409 console.error('Error manually linking agent:', error);
1410 res.status(500).json({ error: 'Failed to link agent', message: error.message });
1415 * @api {post} /api/meshcentral/trigger-sync Trigger Manual Sync Job
1416 * @apiName TriggerMeshCentralSync
1417 * @apiGroup MeshCentralGroup
1418 * @apiDescription Manually triggers MeshCentral device sync job via BullMQ worker. Returns job ID for status tracking.
1419 * Sync runs asynchronously in background.
1420 * @apiSuccess {boolean} success Sync job queued successfully
1421 * @apiSuccess {string} message Status message
1422 * @apiSuccess {string} jobId BullMQ job ID for tracking
1423 * @apiError 500 Failed to trigger sync
1424 * @apiExample {curl} Example:
1425 * curl -X POST https://api.example.com/api/meshcentral/trigger-sync \
1426 * -H "Authorization: Bearer YOUR_TOKEN"
1427 * @apiExample {json} Success-Response (200):
1430 * "message": "MeshCentral sync job queued",
1435 * Trigger manual MeshCentral sync
1436 * POST /api/meshcentral/trigger-sync
1438router.post('/trigger-sync', authenticateToken, async (req, res) => {
1440 // Import the worker module
1441 const { triggerManualSync } = require('../workers/meshcentralSync');
1443 const jobId = await triggerManualSync();
1447 message: 'MeshCentral sync job queued',
1451 console.error('Error triggering sync:', error);
1452 res.status(500).json({ error: 'Failed to trigger sync', message: error.message });
1457 * @api {get} /api/meshcentral/sync-status Get Sync Job Status
1458 * @apiName GetMeshCentralSyncStatus
1459 * @apiGroup MeshCentralGroup
1460 * @apiDescription Retrieves current status and history of MeshCentral sync jobs from BullMQ. Shows waiting,
1461 * active, completed, and failed jobs along with cron schedule.
1462 * @apiSuccess {boolean} success Status retrieved successfully
1463 * @apiSuccess {object} status Current job counts
1464 * @apiSuccess {number} status.waiting Number of waiting jobs
1465 * @apiSuccess {number} status.active Number of active jobs
1466 * @apiSuccess {number} status.recentCompleted Number of recent completed jobs (last 10)
1467 * @apiSuccess {number} status.recentFailed Number of recent failed jobs (last 10)
1468 * @apiSuccess {object[]} schedule Cron schedule for automatic sync
1469 * @apiSuccess {string} schedule.pattern Cron pattern
1470 * @apiSuccess {string} schedule.nextRun Next scheduled run timestamp
1471 * @apiSuccess {Object} recentJobs Recent job details
1472 * @apiSuccess {Object[]} recentJobs.completed Recent completed jobs
1473 * @apiSuccess {Object[]} recentJobs.failed Recent failed jobs
1474 * @apiError 500 Failed to get sync status
1475 * @apiExample {curl} Example:
1476 * curl https://api.example.com/api/meshcentral/sync-status \
1477 * -H "Authorization: Bearer YOUR_TOKEN"
1478 * @apiExample {json} Success-Response (200):
1481 * "status": {"waiting": 0, "active": 1, "recentCompleted": 5, "recentFailed": 0},
1482 * "schedule": [{"pattern": "0 3 * * *", "nextRun": "2026-03-13T03:00:00Z"}],
1484 * "completed": [{"id": "123", "timestamp": 1678886400000, "result": {"synced": 10}}],
1490 * Get sync status and history
1491 * GET /api/meshcentral/sync-status
1493router.get('/sync-status', authenticateToken, async (req, res) => {
1495 const { meshSyncQueue } = require('../workers/meshcentralSync');
1497 const [waiting, active, completed, failed] = await Promise.all([
1498 meshSyncQueue.getWaiting(),
1499 meshSyncQueue.getActive(),
1500 meshSyncQueue.getCompleted(0, 9),
1501 meshSyncQueue.getFailed(0, 9)
1504 const repeatableJobs = await meshSyncQueue.getRepeatableJobs();
1509 waiting: waiting.length,
1510 active: active.length,
1511 recentCompleted: completed.length,
1512 recentFailed: failed.length
1514 schedule: repeatableJobs.map(job => ({
1515 pattern: job.pattern,
1516 nextRun: new Date(job.next).toISOString()
1519 completed: completed.map(job => ({
1521 timestamp: job.timestamp,
1522 result: job.returnvalue
1524 failed: failed.map(job => ({
1526 timestamp: job.timestamp,
1527 error: job.failedReason
1532 console.error('Error getting sync status:', error);
1533 res.status(500).json({ error: 'Failed to get sync status', message: error.message });
1538 * @api {post} /api/meshcentral/tag-devices Tag Devices with Tenant Metadata
1539 * @apiName TagMeshCentralDevices
1540 * @apiGroup MeshCentralGroup
1541 * @apiDescription Tags all devices in tenant's MeshCentral mesh with tenant UUID and customer ID metadata.
1542 * Updates MeshCentral device metadata for filtering and organization. Retrieves customer ID from
1543 * database agent records.
1544 * @apiSuccess {boolean} success Tagging completed successfully
1545 * @apiSuccess {string} tenant Tenant name
1546 * @apiSuccess {string} meshId Tenant's mesh/group ID
1547 * @apiSuccess {number} devicesTagged Number of devices tagged
1548 * @apiSuccess {object[]} devices Array of tagged device details
1549 * @apiSuccess {string} devices.hostname Device hostname
1550 * @apiSuccess {string} devices.nodeId MeshCentral node ID
1551 * @apiSuccess {string} devices.tenantId Tenant UUID applied
1552 * @apiSuccess {string} [devices.customerId] Customer UUID applied (if available)
1553 * @apiError 404 Tenant mesh not found
1554 * @apiError 500 Failed to tag devices
1555 * @apiExample {curl} Example:
1556 * curl -X POST https://api.example.com/api/meshcentral/tag-devices \
1557 * -H "Authorization: Bearer YOUR_TOKEN"
1558 * @apiExample {json} Success-Response (200):
1561 * "tenant": "Tenant 1",
1562 * "meshId": "mesh//abc123...",
1563 * "devicesTagged": 5,
1565 * {"hostname": "DESKTOP-01", "nodeId": "node//abc", "tenantId": "tenant-uuid", "customerId": "cust-uuid"}
1570 * Tag all devices in tenant's mesh with tenant UUID
1571 * POST /api/meshcentral/tag-devices
1573router.post('/tag-devices', authenticateToken, async (req, res) => {
1575 const tenantId = req.user.tenantId;
1577 // Get tenant's mesh ID
1578 const tenantResult = await db.query(
1579 'SELECT meshcentral_group_id, name FROM tenants WHERE tenant_id = $1',
1583 if (tenantResult.rows.length === 0 || !tenantResult.rows[0].meshcentral_group_id) {
1584 return res.status(404).json({ error: 'Tenant mesh not found' });
1587 const { meshcentral_group_id: meshId, name: tenantName } = tenantResult.rows[0];
1589 // Connect to MeshCentral
1590 const api = await getAdminMeshAPI();
1591 const nodes = await api.getNodes();
1592 const meshNodes = nodes.filter(n => n.meshid === meshId);
1594 console.log(`📋 Found ${meshNodes.length} devices in ${tenantName} mesh`);
1596 let taggedCount = 0;
1599 for (const node of meshNodes) {
1600 const parsed = MeshCentralAPI.parseNodeData(node);
1601 const tagData = MeshCentralAPI.parseDeviceTags(node.tags);
1603 // Get database agent for customer ID
1604 const agentResult = await db.query(
1605 'SELECT customer_id FROM agents WHERE meshcentral_nodeid = $1',
1609 const customerId = agentResult.rows[0]?.customer_id;
1611 // Update device tags
1612 await api.updateDeviceMetadata(parsed.nodeId, {
1614 customerId: customerId,
1615 tags: tagData.customTags
1620 hostname: parsed.hostname,
1621 nodeId: parsed.nodeId,
1623 customerId: customerId
1626 console.log(` ✅ Tagged ${parsed.hostname} (tenant:${tenantId}, customer:${customerId || 'none'})`);
1633 devicesTagged: taggedCount,
1637 console.error('Error tagging devices:', error);
1638 res.status(500).json({ error: 'Failed to tag devices', message: error.message });
1642module.exports = router;