EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
monitoring.js
Go to the documentation of this file.
1/**
2 * @file monitoring.js
3 * @module MonitoringRoutes
4 * @description Agent monitoring and device assignment routes. Retrieves paginated lists of agents with connection status, search, and tenant filtering. Supports device assignment to tenants and customers, with optional MeshCentral mesh group relocation. Database is the source of truth, kept in sync by auto-sync worker.
5 * @see {@link ../lib/meshcentral-api} for MeshCentral API integration
6 * @see {@link ../services/db} for database connection
7 * @see {@link ../middleware/auth} for authentication middleware
8 * @see {@link ../middleware/tenant} for tenant context utilities
9 * @apiDefine MonitoringGroup Monitoring
10 * @apiGroup Monitoring
11 * @apiHeader {string} Authorization Bearer token required.
12 * @apiHeader {string} [X-Tenant-ID] Tenant context header (optional for root/MSP users).
13 * @apiError (Error 401) Unauthorized Missing or invalid token.
14 * @apiError (Error 403) Forbidden Only administrators can assign devices.
15 * @apiError (Error 404) NotFound Device or tenant not found.
16 * @apiError (Error 500) ServerError Internal server error.
17 */
18
19const express = require('express');
20const router = express.Router();
21const authenticateToken = require('../middleware/auth');
22
23const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
24const MeshCentralAPI = require('../lib/meshcentral-api');
25
26/**
27 * @api {get} /monitoring Get monitoring data (agent list with stats)
28 * @apiName GetMonitoring
29 * @apiGroup Monitoring
30 * @apiDescription Retrieve paginated list of agents with connection status, tenant/customer info, and global stats (total, online, offline). Database is the source of truth (kept in sync by auto-sync worker). Supports search filtering and MSP tenant impersonation.
31 * @apiParam {number} [page=1] Page number for pagination.
32 * @apiParam {number} [limit=20] Number of agents per page.
33 * @apiParam {string} [search] Search term for hostname, name, or MeshCentral node ID.
34 * @apiParam {string} [tenantId] Tenant ID for MSP impersonation (root/MSP users only).
35 * @apiSuccess {object[]} agents List of agents.
36 * @apiSuccess {string} agents.agent_id Agent ID.
37 * @apiSuccess {string} agents.agent_uuid Agent UUID.
38 * @apiSuccess {string} agents.hostname Agent hostname.
39 * @apiSuccess {string} agents.platform Platform (windows, linux, darwin).
40 * @apiSuccess {string} agents.os Operating system.
41 * @apiSuccess {string} agents.os_version OS version.
42 * @apiSuccess {string} agents.tenant_id Tenant ID.
43 * @apiSuccess {string} agents.tenant_name Tenant name.
44 * @apiSuccess {string} agents.customer_id Customer ID.
45 * @apiSuccess {string} agents.customer_name Customer name.
46 * @apiSuccess {boolean} agents.meshcentral_connected MeshCentral connection status.
47 * @apiSuccess {string} agents.status Status (online/offline).
48 * @apiSuccess {Date} agents.last_seen Last seen timestamp.
49 * @apiSuccess {boolean} agents.is_untagged Whether agent is untagged.
50 * @apiSuccess {Number} total Total agent count.
51 * @apiSuccess {Object} overview Global stats.
52 * @apiSuccess {Number} overview.totalAgents Total agents.
53 * @apiSuccess {Number} overview.agentsOnline Online agents.
54 * @apiSuccess {Number} overview.agentsOffline Offline agents.
55 * @apiSuccess {Number} page Current page.
56 * @apiSuccess {Number} perPage Agents per page.
57 *
58 * @apiExample {curl} Example usage:
59 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/monitoring?page=1&limit=20&search=server
60 */
61router.get('/', authenticateToken, setTenantContext, async (req, res) => {
62 const pool = require('../services/db');
63 try {
64 const page = parseInt(req.query.page, 10) || 1;
65 const limit = parseInt(req.query.limit, 10) || 20;
66 const offset = (page - 1) * limit;
67 const search = req.query.search || '';
68
69 console.log('[Monitoring] Request received:', {
70 user: req.user,
71 tenant: req.tenant,
72 query: req.query
73 });
74
75 // For root/MSP users, allow filtering by selected tenant (impersonation)
76 let effectiveTenant = req.tenant;
77 if (req.tenant && req.tenant.isMsp) {
78 const selectedTenantId = req.query.tenantId || req.query.tenant_id;
79 if (selectedTenantId) {
80 effectiveTenant = { ...req.tenant, id: selectedTenantId, isMsp: false };
81 }
82 }
83
84 const isRootOrMsp = req.tenant.isMsp || effectiveTenant.id === '00000000-0000-0000-0000-000000000001';
85
86 console.log('[Monitoring] Query context:', {
87 tenantId: effectiveTenant.id,
88 isRootOrMsp,
89 search
90 });
91
92 // Build tenant filter for query
93 let whereClause = '';
94 let params = [];
95 let paramIndex = 1;
96
97 if (!isRootOrMsp) {
98 whereClause = `WHERE a.tenant_id = $${paramIndex}`;
99 params.push(effectiveTenant.id);
100 paramIndex++;
101 } else {
102 // Root/MSP users see all agents
103 whereClause = 'WHERE 1=1';
104 }
105
106 // Add search filter
107 if (search) {
108 whereClause += ` AND (
109 LOWER(a.hostname) LIKE LOWER($${paramIndex}) OR
110 LOWER(a.name) LIKE LOWER($${paramIndex}) OR
111 LOWER(a.meshcentral_nodeid) LIKE LOWER($${paramIndex})
112 )`;
113 params.push(`%${search}%`);
114 paramIndex++;
115 }
116
117 // Get total count
118 const countResult = await pool.query(
119 `SELECT COUNT(*) as total FROM agents a ${whereClause}`,
120 params
121 );
122 const total = parseInt(countResult.rows[0].total, 10);
123
124 // Get paginated agents from our database
125 const agentsResult = await pool.query(
126 `SELECT
127 a.agent_id,
128 a.agent_uuid,
129 a.tenant_id,
130 a.customer_id,
131 a.hostname,
132 a.name,
133 a.platform,
134 a.os,
135 a.os_version,
136 a.meshcentral_nodeid,
137 a.meshcentral_connected,
138 a.meshcentral_last_seen,
139 a.status,
140 a.last_seen,
141 a.created_at,
142 t.name as tenant_name,
143 t.meshcentral_group_id,
144 c.name as customer_name
145 FROM agents a
146 LEFT JOIN tenants t ON a.tenant_id = t.tenant_id
147 LEFT JOIN customers c ON a.customer_id = c.customer_id
148 ${whereClause}
149 ORDER BY a.meshcentral_connected DESC, a.last_seen DESC NULLS LAST
150 LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
151 [...params, limit, offset]
152 );
153
154 const agents = agentsResult.rows;
155
156 // Calculate stats (from ALL agents, not just paginated)
157 const statsResult = await pool.query(
158 `SELECT
159 COUNT(*) as total,
160 SUM(CASE WHEN meshcentral_connected = true THEN 1 ELSE 0 END) as online,
161 SUM(CASE WHEN meshcentral_connected = false OR meshcentral_connected IS NULL THEN 1 ELSE 0 END) as offline
162 FROM agents a
163 ${whereClause}`,
164 params
165 );
166
167 const stats = statsResult.rows[0];
168 const agentsOnline = parseInt(stats.online, 10) || 0;
169 const agentsOffline = parseInt(stats.offline, 10) || 0;
170 const totalAgents = parseInt(stats.total, 10) || 0;
171
172 console.log('[Monitoring] Stats:', {
173 total: totalAgents,
174 online: agentsOnline,
175 offline: agentsOffline,
176 pageSize: agents.length
177 });
178
179 // Map agents to frontend format
180 const mappedAgents = agents.map(a => ({
181 agent_id: a.agent_id,
182 agent_uuid: a.agent_uuid,
183 hostname: a.hostname || a.name || 'Unknown',
184 name: a.name || a.hostname,
185 platform: a.platform,
186 os: a.os,
187 os_version: a.os_version,
188 tenant_id: a.tenant_id,
189 tenant_name: a.tenant_name,
190 customer_id: a.customer_id,
191 customer_name: a.customer_name,
192 meshcentral_nodeid: a.meshcentral_nodeid,
193 meshcentral_connected: a.meshcentral_connected || false,
194 mesh_id: a.meshcentral_group_id,
195 status: a.meshcentral_connected ? 'online' : 'offline',
196 last_seen: a.meshcentral_last_seen || a.last_seen,
197 created_at: a.created_at,
198 is_untagged: !a.tenant_id || !a.meshcentral_nodeid
199 }));
200
201 res.json({
202 agents: mappedAgents,
203 total: totalAgents,
204 overview: {
205 totalAgents: totalAgents,
206 agentsOnline: agentsOnline,
207 agentsOffline: agentsOffline
208 },
209 page,
210 perPage: limit
211 });
212 } catch (err) {
213 console.error('[Monitoring] Error fetching monitoring data:', err);
214 res.status(500).json({ error: 'Server error', message: err.message });
215 }
216});
217
218/**
219 * @api {post} /monitoring/assign-device Assign untagged device to tenant
220 * @apiName AssignDevice
221 * @apiGroup Monitoring
222 * @apiDescription Assign an untagged MeshCentral device to a tenant and optionally to a customer. Updates device tags in MeshCentral and creates/updates agent in the database. Optionally moves device to tenant's MeshCentral mesh group. Only root/MSP users can assign devices.
223 * @apiParam {string} nodeId MeshCentral node ID.
224 * @apiParam {string} tenantId Tenant ID to assign to.
225 * @apiParam {string} [customerId] Customer ID to assign to (optional).
226 * @apiParam {boolean} [moveMesh=false] Whether to move device to tenant's mesh group.
227 * @apiSuccess {boolean} success Operation success status.
228 * @apiSuccess {object} device Assigned device details.
229 * @apiSuccess {string} device.nodeId MeshCentral node ID.
230 * @apiSuccess {string} device.hostname Device hostname.
231 * @apiSuccess {string} device.tenantId Assigned tenant ID.
232 * @apiSuccess {string} device.tenantName Assigned tenant name.
233 * @apiSuccess {string} device.customerId Assigned customer ID.
234 * @apiSuccess {boolean} device.moved Whether device was moved to tenant's mesh group.
235 * @apiError (Error 400) BadRequest nodeId and tenantId are required.
236 * @apiError (Error 403) Forbidden Only administrators can assign devices.
237 * @apiError (Error 404) NotFound Device or tenant not found.
238 * @apiExample {curl} Example usage:
239 * curl -X POST -H "Authorization: Bearer <token>" -d '{"nodeId":"node//abc123","tenantId":"tenant-id","customerId":"customer-id","moveMesh":true}' https://api.example.com/monitoring/assign-device
240 */
241router.post('/assign-device', authenticateToken, setTenantContext, async (req, res) => {
242 const pool = require('../services/db');
243 try {
244 const { nodeId, tenantId, customerId, moveMesh = false } = req.body;
245
246 // Only root/MSP users can assign devices
247 if (!req.tenant.isMsp) {
248 return res.status(403).json({ error: 'Only administrators can assign devices' });
249 }
250
251 if (!nodeId || !tenantId) {
252 return res.status(400).json({ error: 'nodeId and tenantId are required' });
253 }
254
255 // Get tenant info
256 const tenantResult = await pool.query(
257 'SELECT tenant_id, name, meshcentral_group_id FROM tenants WHERE tenant_id = $1',
258 [tenantId]
259 );
260
261 if (tenantResult.rows.length === 0) {
262 return res.status(404).json({ error: 'Tenant not found' });
263 }
264
265 const tenant = tenantResult.rows[0];
266
267 // Connect to MeshCentral
268 const meshcentral = new MeshCentralAPI({
269 url: process.env.MESHCENTRAL_URL,
270 username: process.env.MESHCENTRAL_ADMIN_USER || process.env.MESHCENTRAL_USERNAME,
271 password: process.env.MESHCENTRAL_ADMIN_PASS || process.env.MESHCENTRAL_PASSWORD
272 });
273
274 await meshcentral.login();
275 const node = await meshcentral.getNode(nodeId);
276
277 if (!node) {
278 return res.status(404).json({ error: 'Device not found in MeshCentral' });
279 }
280
281 const parsed = MeshCentralAPI.parseNodeData(node);
282 const tagData = MeshCentralAPI.parseDeviceTags(node.tags);
283
284 // Update device tags in MeshCentral
285 await meshcentral.updateDeviceMetadata(nodeId, {
286 tenantId: tenantId,
287 customerId: customerId,
288 tags: tagData.customTags
289 });
290
291 console.log(`✅ Tagged device ${parsed.hostname} with tenant ${tenantId}`);
292
293 // Create or update agent in database
294 const agentCheck = await pool.query(
295 'SELECT agent_id FROM agents WHERE meshcentral_nodeid = $1',
296 [nodeId]
297 );
298
299 if (agentCheck.rows.length > 0) {
300 // Update existing agent
301 await pool.query(
302 `UPDATE agents
303 SET tenant_id = $1, customer_id = $2, meshcentral_connected = $3, last_seen = $4
304 WHERE meshcentral_nodeid = $5`,
305 [tenantId, customerId, parsed.connected, parsed.lastSeen, nodeId]
306 );
307 console.log(`✅ Updated agent ${agentCheck.rows[0].agent_id} in database`);
308 } else {
309 // Create new agent
310 const { v4: uuidv4 } = require('uuid');
311 await pool.query(
312 `INSERT INTO agents (
313 tenant_id, customer_id, hostname, platform, os, os_version,
314 agent_uuid, meshcentral_nodeid, meshcentral_connected,
315 last_seen, status, created_at
316 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())`,
317 [
318 tenantId,
319 customerId,
320 parsed.hostname,
321 parsed.platform,
322 parsed.os,
323 parsed.osVersion,
324 uuidv4(),
325 nodeId,
326 parsed.connected,
327 parsed.lastSeen,
328 parsed.connected ? 'online' : 'offline'
329 ]
330 );
331 console.log(`✅ Created new agent for ${parsed.hostname} in database`);
332 }
333
334 // Optionally move device to tenant's mesh group
335 let moved = false;
336 if (moveMesh && tenant.meshcentral_group_id && node.meshid !== tenant.meshcentral_group_id) {
337 try {
338 await meshcentral._sendWebSocketMessage({
339 action: 'changeDeviceMesh',
340 nodeids: [nodeId],
341 meshid: tenant.meshcentral_group_id
342 });
343 moved = true;
344 console.log(`✅ Moved device to ${tenant.name} mesh group`);
345 } catch (moveErr) {
346 console.error('Failed to move device to tenant mesh:', moveErr);
347 }
348 }
349
350 res.json({
351 success: true,
352 device: {
353 nodeId: nodeId,
354 hostname: parsed.hostname,
355 tenantId: tenantId,
356 tenantName: tenant.name,
357 customerId: customerId,
358 moved: moved
359 }
360 });
361 } catch (err) {
362 console.error('Error assigning device:', err);
363 res.status(500).json({ error: 'Failed to assign device', message: err.message });
364 }
365});
366
367module.exports = router;