2 * @file RMM Agent Management API Routes
4 * Central API module for RMM agent lifecycle management, monitoring, and data collection.
5 * Provides comprehensive agent operations including registration, heartbeat tracking, hardware
6 * inventory, software management, file transfers, metrics collection, and MeshCentral integration.
9 * - **Agent Registration**: Initial agent enrollment with tenant validation
10 * - **Heartbeat Monitoring**: Real-time status tracking and keep-alive
11 * - **Hardware Inventory**: CPU, memory, disk, network, GPU detection and tracking
12 * - **Software Management**: Installed programs, running processes, Windows services
13 * - **File Operations**: File browser, upload/download, transfer tracking
14 * - **Metrics Collection**: Performance data aggregation and historical queries
15 * - **MeshCentral Integration**: Device URL generation, node info, session management
16 * - **Activity Logging**: Agent events, system logs, activity tracking
17 * - **Version Management**: Agent version checks, download links, hash verification
19 * **Endpoint Groups:**
20 * 1. **Core Agent Operations** (lines 14-200):
21 * - POST /register - Agent registration with tenant JWT validation
22 * - POST /:agentId/heartbeat - Keep-alive with status tracking
23 * - GET /:agentId/status - Agent status and connectivity check
24 * - GET /:agentId - Get single agent details
25 * - GET /list - List all agents (legacy, non-authenticated)
26 * - GET /:agentId/plugins - Get allowed plugins for agent
27 * - GET /:agentId/contract - Get contract details for agent's tenant
29 * 2. **MeshCentral Integration** (lines 199-478):
30 * - GET /:agentId/meshcentral-url - Generate MeshCentral access URL
31 * - GET /:agentId/meshcentral-node - Get MeshCentral node/device info
32 * - GET /:agentId/meshcentral-info - Get complete MeshCentral session data
34 * 3. **Metrics & Monitoring** (lines 478-583):
35 * - GET /download-token - Generate agent download token
36 * - POST /metrics - Submit agent metrics (batch)
37 * - GET /:agentId/metrics - Query historical metrics data
38 * - GET /os-options - Get supported OS platforms list
40 * 4. **Hardware Inventory** (lines 601-881):
41 * - POST /enroll - Full agent enrollment with hardware detection
42 * - POST /:agent_uuid/hardware - Submit hardware inventory data
43 * - GET /:agent_uuid/hardware - Retrieve hardware specifications
45 * 5. **Agent Management** (lines 881-1132):
46 * - GET /list - List agents with tenant filtering (authenticated)
47 * - GET /:id - Get agent by ID with full details
48 * - DELETE /:id - Delete/uninstall agent
50 * 6. **Software & System Info** (lines 1132-1419):
51 * - GET /:agent_uuid/programs - List installed programs
52 * - GET /:agent_uuid/processes - List running processes
53 * - GET /:agent_uuid/files - Browse agent filesystem
54 * - GET /:agent_uuid/services - List Windows services
55 * - GET /:agent_uuid/activity-logs - Get agent activity history
56 * - GET /:agent_uuid/events - Get system event logs
57 * - GET /:agent_uuid/agent-logs - Get agent application logs
59 * 7. **Version Management** (lines 1419-1553):
60 * - GET /version - Get latest agent version info
61 * - GET /download/:version - Download agent installer by version
62 * - GET /hash/:version - Get installer hash for integrity verification
64 * 8. **File Transfer Operations** (lines 1553-1637):
65 * - POST /:agent_uuid/files/upload - Upload file to agent
66 * - POST /:agent_uuid/files/download - Request file download from agent
67 * - GET /:agent_uuid/files/transfer/:transferId - Get transfer status
68 * - GET /:agent_uuid/files/transfer/:transferId/download - Download transferred file
69 * - DELETE /:agent_uuid/files/transfer/:transferId - Cancel/cleanup transfer
72 * - Tenant JWT validation for registration/enrollment (verifyTenantJwt)
73 * - Bearer token authentication for most endpoints (authenticateToken middleware)
74 * - Tenant isolation via tenant_id validation
75 * - File upload size limits (multer configuration)
77 * **Database Tables:**
78 * - `agents`: Core agent records (registration, status, hardware specs)
79 * - `agent_metrics`: Performance metrics time-series data
80 * - `agent_programs`: Installed software inventory
81 * - `agent_processes`: Running process snapshots
82 * - `agent_services`: Windows services status
83 * - `agent_activity_logs`: Agent event history
84 * - `hardware_specs`: Detailed hardware inventory (CPU, RAM, GPU, disks)
86 * **External Integrations:**
87 * - **MeshCentral**: Remote access (terminal, files, desktop) via MeshCentral server
88 * - **DigitalOcean Spaces**: Agent installer distribution and file transfer storage
89 * @module routes/agent
91 * @requires ../services/db - PostgreSQL database pool
92 * @requires jsonwebtoken - JWT token generation/verification
93 * @requires ../models/Agent - Agent data access model
94 * @requires ../models/Policies - Policy management model
95 * @requires ../models/Plugins - Plugin management model
96 * @requires ../models/Contracts - Contract data model
97 * @requires ../utils/auth - JWT verification utilities
98 * @requires ../middleware/auth - Authentication middleware
99 * @requires ../services/metricsAggregator - Metrics collection service
100 * @see {@link module:models/Agent}
101 * @see {@link module:services/metricsAggregator}
104const express = require('express');
105const pool = require('../services/db');
106const router = express.Router();
107const jwt = require('jsonwebtoken');
108const Agents = require('../models/Agent');
109const Policies = require('../models/Policies');
110const Plugins = require('../models/Plugins');
111const Contracts = require('../models/Contracts');
112const { verifyTenantJwt } = require('../utils/auth');
113const authenticateToken = require('../middleware/auth');
114const { addMetric } = require('../services/metricsAggregator');
117 * @api {post} /agent/register Register New Agent
118 * @apiName RegisterAgent
121 * Registers a new RMM agent with the platform. Validates tenant JWT token, creates agent
122 * record in database, and returns agent UUID for subsequent API calls. This is the first
123 * API call made by a newly installed agent.
124 * @apiPermission tenantJWT
125 * @apiHeader {string} Authorization Bearer token with tenant JWT (AGENT_SIGN_KEY signed)
126 * @apiBody {number} tenantId Tenant ID from bootstrap.json
127 * @apiBody {string} version Agent version (e.g., "1.0.12")
128 * @apiBody {string} hostname Machine hostname/computer name
129 * @apiBody {String="win32","linux","darwin"} platform Operating system platform
130 * @apiBody {string} machineId Unique machine identifier (Windows machine GUID or Linux /etc/machine-id)
131 * @apiSuccess {number} agentId Database agent ID
132 * @apiSuccess {string} agentUuid Agent UUID for API authentication
133 * @apiError (403) {String} error "INVALID_TOKEN" (if JWT verification fails)
134 * @apiError (500) {String} error Error message if registration fails
135 * @apiExample {curl} Example Request:
136 * curl -X POST https://api.ibghub.com/api/agent/register \
137 * -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
138 * -H "Content-Type: application/json" \
139 * -d '{"tenantId":5,"version":"1.0.12","hostname":"DESKTOP-ABC123","platform":"win32","machineId":"12345678-1234-1234-1234-123456789012"}'
142// Refactored agent registration route
143router.post('/register', async (req, res) => {
145 const jwtPayload = verifyTenantJwt(req.headers.authorization?.split(' ')[1]);
146 if (!jwtPayload) return res.status(403).json({ error: 'INVALID_TOKEN' });
148 const { tenantId, version, hostname, platform, machineId } = req.body;
149 const policyId = null; // Set to null for now
150 const result = await Agents.createAgent({
159 // const policy = await Policies.getPolicy(policyId);
160 // const contract = await Contracts.getContractForTenant(tenantId);
161 // const allowedPlugins = await Plugins.getAllowedPlugins(agentId);
163 res.json(result); // Return {agentId, agentUuid}
165 console.error('REGISTER ERROR:', err);
166 res.status(500).json({ error: err.message });
171 * @api {post} /agent/:agentId/heartbeat Agent Heartbeat
172 * @apiName AgentHeartbeat
175 * Updates agent's last_seen timestamp and optional version. Returns pending commands
176 * from queue (future: remote command execution).
179 * - Updates last_seen timestamp in database (NOW())
180 * - Updates agent version if provided in body
181 * - Returns any pending commands for agent (currently empty, future feature)
182 * @apiPermission public
183 * @apiNote No authentication required - agents use agentId (UUID) in URL for identification
184 * @apiParam {string} agentId Agent UUID from registration response
185 * @apiBody {string} [version] Agent version string (e.g., "1.0.13") - updates agent record if provided
186 * @apiSuccess {boolean} success Always true if heartbeat processed
187 * @apiSuccess {object[]} commands Array of pending commands for agent to execute (empty for now)
188 * @apiSuccess {string} timestamp ISO timestamp of heartbeat processing
189 * @apiError (500) {String} error Error message if heartbeat update fails
190 * @apiExample {curl} Example Request:
191 * curl -X POST https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/heartbeat \
192 * -H "Content-Type: application/json" \
193 * -d '{"version":"1.0.13"}'
196// Refactored heartbeat update
197router.post('/:agentId/heartbeat', async (req, res) => {
199 const version = req.body?.version;
200 const agentUuid = req.params.agentId;
202 // Update heartbeat and version if provided
205 `UPDATE agents SET last_seen = NOW(), version = $1 WHERE agent_uuid = $2`,
209 await Agents.updateHeartbeat(agentUuid);
212 // Check for pending commands for this agent
213 // TODO: Implement command queue in database
219 timestamp: new Date().toISOString()
222 console.error('HEARTBEAT ERROR:', err);
223 res.status(500).json({ error: err.message });
228 * @api {get} /agent/:agentId/status Get Agent Status
229 * @apiName GetAgentStatus
232 * Returns real-time connectivity status of agent including WebSocket and MeshCentral
233 * connection states. Prioritizes MeshCentral status over WebSocket when both available.
234 * Used by dashboards for live agent status indicators.
235 * @apiPermission public
236 * @apiNote No authentication required - open endpoint for agent status checks
237 * @apiParam {string} agentId Agent UUID or agent_id
238 * @apiSuccess {string} agent_uuid Agent UUID
239 * @apiSuccess {number} agent_id Database agent ID
240 * @apiSuccess {Date} last_seen Last heartbeat timestamp (MeshCentral or WebSocket)
241 * @apiSuccess {string} os Operating system (e.g., "Windows 10 Pro")
242 * @apiSuccess {string} hostname Machine hostname
243 * @apiSuccess {string} status Agent status ("active", "offline", etc.)
244 * @apiSuccess {string} version Agent version string
245 * @apiSuccess {string} ip_address Agent IP address
246 * @apiSuccess {string} meshcentral_nodeid MeshCentral node identifier
247 * @apiSuccess {boolean} meshcentral_connected MeshCentral connection status
248 * @apiSuccess {Date} meshcentral_last_seen MeshCentral last contact time
249 * @apiSuccess {boolean} connected Overall connection status (MeshCentral OR WebSocket)
250 * @apiSuccess {string} connection_status "online" or "offline"
251 * @apiSuccess {Object} connection_sources Source-specific connection states
252 * @apiSuccess {boolean} connection_sources.meshcentral MeshCentral connected
253 * @apiSuccess {boolean} connection_sources.websocket WebSocket connected
254 * @apiError (404) {String} error "Agent not found"
255 * @apiError (500) {String} error Error message
256 * @apiExample {curl} Example Request:
257 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/status
260// Get agent status (including WebSocket and MeshCentral connection)
261router.get('/:agentId/status', async (req, res) => {
263 const wsManager = require('../services/websocketManager');
264 const agent = await pool.query(
266 agent_uuid, agent_id, last_seen, os, hostname, status, version, ip_address,
267 meshcentral_nodeid, meshcentral_connected, meshcentral_last_seen
269 WHERE agent_uuid = $1 OR agent_id::text = $1`,
272 if (agent.rows.length === 0) {
273 return res.status(404).json({ error: 'Agent not found' });
275 const agentData = agent.rows[0];
276 const isConnectedWS = wsManager.isAgentConnected(agentData.agent_uuid);
277 const isConnectedMesh = agentData.meshcentral_connected || false;
279 // Prefer MeshCentral status if available, fallback to WebSocket
280 const connected = isConnectedMesh || isConnectedWS;
281 const lastSeen = agentData.meshcentral_last_seen || agentData.last_seen;
285 connected: connected,
286 connection_status: connected ? 'online' : 'offline',
288 connection_sources: {
289 meshcentral: isConnectedMesh,
290 websocket: isConnectedWS
294 console.error('GET AGENT STATUS ERROR:', err);
295 res.status(500).json({ error: err.message });
300 * @api {get} /agent/:agentId Get Agent Details
304 * Retrieves complete agent details including tenant and customer information.
305 * Returns full agent record with joined customer name and tenant name.
306 * @apiPermission public
307 * @apiParam {string} agentId Agent UUID
308 * @apiSuccess {object} agent Complete agent object with all database columns
309 * @apiSuccess {string} agent.agent_uuid Agent UUID
310 * @apiSuccess {number} agent.agent_id Database agent ID
311 * @apiSuccess {number} agent.tenant_id Tenant ID
312 * @apiSuccess {string} agent.tenant_name Tenant name (joined from tenants table)
313 * @apiSuccess {number} agent.customer_id Customer ID
314 * @apiSuccess {string} agent.customer_name Customer name (joined from customers table)
315 * @apiSuccess {string} agent.hostname Machine hostname
316 * @apiSuccess {string} agent.os Operating system
317 * @apiSuccess {string} agent.version Agent version
318 * @apiSuccess {string} agent.ip_address IP address
319 * @apiSuccess {Date} agent.last_seen Last heartbeat timestamp
320 * @apiSuccess {string} agent.status Agent status
321 * @apiSuccess {string} agent.platform Platform (win32/linux/darwin)
322 * @apiSuccess {boolean} connected WebSocket connection status
323 * @apiError (404) {String} error "Agent not found"
324 * @apiError (500) {String} error Error message
325 * @apiExample {curl} Example Request:
326 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000
329// Refactored get agent (full details)
330router.get('/:agentId', async (req, res) => {
332 const wsManager = require('../services/websocketManager');
333 const agent = await pool.query(
334 'SELECT a.*, c.name as customer_name, t.name as tenant_name FROM agents a LEFT JOIN customers c ON a.customer_id = c.id LEFT JOIN tenants t ON a.tenant_id = t.tenant_id WHERE a.agent_uuid = $1',
337 if (agent.rows.length === 0) {
338 return res.status(404).json({ error: 'Agent not found' });
340 const agentData = agent.rows[0];
341 agentData.connected = wsManager.isAgentConnected(req.params.agentId);
342 res.json({ agent: agentData });
344 console.error('GET AGENT ERROR:', err);
345 res.status(500).json({ error: err.message });
350 * @api {get} /agent/list List All Agents (Legacy)
351 * @apiName ListAgentsLegacy
354 * **LEGACY ENDPOINT** - Lists all agents without authentication or filtering.
355 * This endpoint has no tenant isolation and returns all agents across all tenants.
356 * Deprecated in favor of authenticated GET /agent/list endpoint with tenant filtering.
357 * @apiPermission public
358 * @apiDeprecated Use authenticated GET /agent/list endpoint instead
359 * @apiNote No authentication - security risk, should be removed or restricted
360 * @apiQuery {object} [filters] Optional query parameters passed to listAgents model method
361 * @apiSuccess {object[]} agents Array of all agent objects
362 * @apiError (500) {String} error Error message
363 * @apiExample {curl} Example Request:
364 * curl https://api.ibghub.com/api/agent/list
367// Refactored list agents
368router.get('/list', async (req, res) => {
370 const agents = await Agents.listAgents(req.query); // Implement listAgents in model
371 res.json({ agents });
373 console.error('LIST AGENTS ERROR:', err);
374 res.status(500).json({ error: err.message });
379 * @api {get} /agent/:agentId/plugins Get Allowed Plugins
380 * @apiName GetAgentPlugins
383 * Retrieves list of plugins enabled/allowed for the agent based on policy configuration.
384 * Used by agents to determine which plugin modules to load and execute.
385 * @apiPermission public
386 * @apiParam {string} agentId Agent UUID
387 * @apiSuccess {object[]} plugins Array of allowed plugin objects
388 * @apiSuccess {string} plugins.plugin_name Plugin name/identifier
389 * @apiSuccess {boolean} plugins.enabled Plugin enabled status
390 * @apiSuccess {object} plugins.config Plugin-specific configuration
391 * @apiError (500) {String} error Error message
392 * @apiExample {curl} Example Request:
393 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/plugins
396// Refactored get allowed plugins
397router.get('/:agentId/plugins', async (req, res) => {
399 const plugins = await Plugins.getAllowedPlugins(req.params.agentId);
400 res.json({ plugins });
402 console.error('GET PLUGINS ERROR:', err);
403 res.status(500).json({ error: err.message });
408 * @api {get} /agent/:agentId/contract Get Agent's Contract
409 * @apiName GetAgentContract
412 * Retrieves contract details for the agent's tenant. Used to determine billing interval,
413 * service level agreements, and contract-specific features or limitations.
414 * @apiPermission public
415 * @apiParam {string} agentId Agent UUID
416 * @apiSuccess {object} contract Contract object
417 * @apiSuccess {number} contract.contract_id Contract ID
418 * @apiSuccess {number} contract.tenant_id Tenant ID
419 * @apiSuccess {string} contract.billing_interval Billing frequency (monthly, yearly, etc.)
420 * @apiSuccess {number} contract.monthly_rate Contract monthly rate
421 * @apiSuccess {string} contract.title Contract title/name
422 * @apiError (404) {String} error "Agent not found"
423 * @apiError (500) {String} error Error message
424 * @apiExample {curl} Example Request:
425 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/contract
428// Refactored get contract
429router.get('/:agentId/contract', async (req, res) => {
431 const agent = await Agents.getAgent(req.params.agentId);
432 if (!agent) return res.status(404).json({ error: 'Agent not found' });
433 const contract = await Contracts.getContractForTenant(agent.tenant_id);
434 res.json({ contract });
436 console.error('GET CONTRACT ERROR:', err);
437 res.status(500).json({ error: err.message });
442 * @api {get} /agent/:agentId/meshcentral-url Get MeshCentral Remote Access URL
443 * @apiName GetMeshCentralURL
446 * Generates authenticated MeshCentral URL for remote desktop/terminal access. Creates
447 * temporary session token valid for 1 hour, queries MeshCentral API for device info,
448 * and returns dashboard URL with embedded authentication. Supports both authenticated
449 * (dashboard user) and unauthenticated (agent) calls with tenant isolation.
452 * - Validates agent exists and retrieves meshcentral_nodeid
453 * - Enforces tenant isolation if JWT authenticated
454 * - Creates session token in database (expires in 1 hour)
455 * - Queries MeshCentral API for device details
456 * - Returns dashboard URL: /meshcentral?agentId={id}&sessionToken={token}
457 * @apiPermission public/authenticated
458 * @apiNote Supports both authenticated (JWT) and unauthenticated (agent UUID) access
459 * @apiParam {string} agentId Agent ID (numeric) or agent UUID
460 * @apiSuccess {string} url MeshCentral dashboard access URL
461 * @apiSuccess {string} sessionToken Temporary session token (1-hour expiration)
462 * @apiSuccess {string} agentId Agent identifier
463 * @apiSuccess {string} hostname Machine hostname
464 * @apiSuccess {string} meshId MeshCentral mesh/group identifier
465 * @apiSuccess {string} nodeId MeshCentral node identifier
466 * @apiSuccess {Object} device MeshCentral device object with connection status
467 * @apiError (404) {String} error "Agent not found"
468 * @apiError (404) {String} error "MeshCentral node ID not found for agent"
469 * @apiError (500) {String} error Error message
470 * @apiExample {curl} Example Request (UUID):
471 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/meshcentral-url
472 * @apiExample {curl} Example Request (ID with JWT):
473 * curl https://api.ibghub.com/api/agent/123/meshcentral-url \
474 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
477// Get MeshCentral remote desktop URL
478// Note: This endpoint should be called from authenticated dashboard
479// For agent-to-backend calls, agents identify themselves via their agent_uuid
480router.get('/:agentId/meshcentral-url', async (req, res) => {
482 const { agentId } = req.params;
484 // Get agent details including meshcentral_nodeid
485 // Check if agentId is a number (agent_id) or UUID (agent_uuid)
486 const isNumeric = /^\d+$/.test(agentId);
488 // If authenticated via JWT, enforce tenant isolation
489 // If not authenticated (agent calling), allow by agent_uuid only
492 // Authenticated request - enforce tenant isolation
494 ? 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_id = $1 AND (tenant_id = $2 OR $2 = 1)'
495 : 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_uuid = $1 AND (tenant_id = $2 OR $2 = 1)';
496 params = [agentId, req.user.tenantId];
498 // Unauthenticated request - only allow UUID lookup (agents calling)
500 return res.status(401).json({ error: 'Authentication required for agent_id lookups' });
502 query = 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_uuid = $1';
506 const result = await pool.query(query, params);
508 if (result.rows.length === 0) {
509 return res.status(404).json({ error: 'Agent not found' });
512 const agent = result.rows[0];
514 if (!agent.meshcentral_nodeid) {
515 return res.status(404).json({ error: 'MeshCentral node ID not configured for this agent' });
519 // Get MeshCentral API client and generate login token
520 const { getMeshAPI } = require('../workers/meshcentralSync');
521 const api = await getMeshAPI();
523 // Generate login token with node ID for direct access
524 const tokenData = await api.generateLoginToken(agent.meshcentral_nodeid);
527 agentId: agent.agent_id,
528 hostname: agent.hostname,
529 meshcentralNodeId: agent.meshcentral_nodeid,
530 remoteDesktopUrl: tokenData.embedUrl,
531 tokenUser: tokenData.tokenUser,
532 tokenPass: tokenData.tokenPass,
533 expiresAt: tokenData.expiresAt
535 } catch (tokenError) {
536 console.error('Failed to generate MeshCentral login token:', tokenError);
537 // Fallback: return basic URL that requires manual login
538 const meshcentralUrl = process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app';
540 agentId: agent.agent_id,
541 hostname: agent.hostname,
542 meshcentralNodeId: agent.meshcentral_nodeid,
543 remoteDesktopUrl: `${meshcentralUrl}/?gotonode=${agent.meshcentral_nodeid}&viewmode=11`,
544 error: 'Login token generation failed - manual authentication required'
548 console.error('GET MESHCENTRAL URL ERROR:', err);
549 res.status(500).json({ error: err.message });
554 * @api {get} /agent/:agentId/meshcentral-node Get MeshCentral Node ID
555 * @apiName GetMeshCentralNode
558 * Retrieves MeshCentral node ID for agent. Used by WebSocket connections to establish
559 * direct MeshCentral sessions. Enforces tenant isolation.
560 * @apiPermission authenticated
562 * @apiParam {string} agentId Agent ID (numeric) or agent UUID
563 * @apiSuccess {number} agentId Database agent ID
564 * @apiSuccess {string} nodeId MeshCentral node identifier
565 * @apiError (404) {String} error "Agent not found"
566 * @apiError (404) {String} error "MeshCentral node ID not configured for this agent"
567 * @apiError (500) {String} error Error message
568 * @apiExample {curl} Example Request:
569 * curl https://api.ibghub.com/api/agent/123/meshcentral-node \
570 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
573// Get MeshCentral node ID only (for WebSocket connections)
574router.get('/:agentId/meshcentral-node', authenticateToken, async (req, res) => {
576 const { agentId } = req.params;
578 // Check if agentId is a number (agent_id) or UUID (agent_uuid)
579 const isNumeric = /^\d+$/.test(agentId);
581 const query = isNumeric
582 ? 'SELECT agent_id, meshcentral_nodeid FROM agents WHERE agent_id = $1 AND (tenant_id = $2 OR $2 = 1)'
583 : 'SELECT agent_id, meshcentral_nodeid FROM agents WHERE agent_uuid = $1 AND (tenant_id = $2 OR $2 = 1)';
585 const result = await pool.query(query, [agentId, req.user.tenantId]);
587 if (result.rows.length === 0) {
588 return res.status(404).json({ error: 'Agent not found' });
591 const agent = result.rows[0];
593 if (!agent.meshcentral_nodeid) {
594 return res.status(404).json({ error: 'MeshCentral node ID not configured for this agent' });
598 agentId: agent.agent_id,
599 nodeId: agent.meshcentral_nodeid
602 console.error('GET MESHCENTRAL NODE ID ERROR:', err);
603 res.status(500).json({ error: err.message });
608 * @api {get} /agent/:agentId/meshcentral-info Get MeshCentral Device Info
609 * @apiName GetMeshCentralInfo
612 * Retrieves comprehensive MeshCentral device information including connection status,
613 * hardware specifications, and system details. Uses direct database queries for 20x
614 * faster performance compared to MeshCentral API. Falls back to API if DB query fails.
617 * - MeshCentral mesh.nodes collection (device metadata)
618 * - MeshCentral mesh.smbios collection (hardware SMBIOS data)
620 * Hardware info extracted:
621 * - System manufacturer, model, BIOS version
622 * - CPU model, core count
624 * - Network interfaces with IPv4 addresses
625 * - Storage drives with capacity and free space
626 * - OS version details
627 * @apiPermission authenticated
629 * @apiParam {string} agentId Agent ID (numeric) or agent UUID
630 * @apiSuccess {boolean} connected MeshCentral connection status
631 * @apiSuccess {string} meshcentralNodeId MeshCentral node identifier
632 * @apiSuccess {string} hostname Machine hostname
633 * @apiSuccess {Date} lastSeen Last connection timestamp
634 * @apiSuccess {string} lastAddress Last known IP address
635 * @apiSuccess {string} platform Operating system platform
636 * @apiSuccess {string} agentVersion MeshCentral agent version
637 * @apiSuccess {Object} hardware Hardware specifications object
638 * @apiSuccess {string} hardware.manufacturer System manufacturer (e.g., "Dell Inc.")
639 * @apiSuccess {string} hardware.model System model (e.g., "OptiPlex 7060")
640 * @apiSuccess {string} hardware.cpu_model CPU model name
641 * @apiSuccess {number} hardware.cpu_cores CPU core count
642 * @apiSuccess {number} hardware.ram_bytes Total RAM in bytes
643 * @apiSuccess {string} hardware.os_version OS version string
644 * @apiSuccess {string} hardware.bios_version BIOS version
645 * @apiSuccess {string} hardware.bios_vendor BIOS vendor
646 * @apiSuccess {string} hardware.ipv4_address Primary IPv4 address
647 * @apiSuccess {Object[]} hardware.drives Array of storage drive objects
648 * @apiError (404) {String} error "Agent not found"
649 * @apiError (200) {String} error "MeshCentral node ID not configured" (with connected:false)
650 * @apiError (200) {String} error "Node not found in MeshCentral" (with connected:false)
651 * @apiError (500) {String} error Error message
653 * @apiExample {curl} Example Request:
654 * curl https://api.ibghub.com/api/agent/123/meshcentral-info \
655 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
658// Get MeshCentral node information (including offline devices)
659// Now uses direct PostgreSQL queries for 20x faster performance
660router.get('/:agentId/meshcentral-info', authenticateToken, async (req, res) => {
662 const { agentId } = req.params;
664 // Check if agentId is a number (agent_id) or UUID (agent_uuid)
665 const isNumeric = /^\d+$/.test(agentId);
667 const query = isNumeric
668 ? 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_id = $1 AND (tenant_id = $2 OR $2 = 1)'
669 : 'SELECT agent_id, hostname, meshcentral_nodeid, tenant_id FROM agents WHERE agent_uuid = $1 AND (tenant_id = $2 OR $2 = 1)';
671 const result = await pool.query(query, [agentId, req.user.tenantId]);
673 if (result.rows.length === 0) {
674 return res.status(404).json({ error: 'Agent not found' });
677 const agent = result.rows[0];
679 if (!agent.meshcentral_nodeid) {
680 return res.status(200).json({
682 error: 'MeshCentral node ID not configured for this agent'
686 // Use direct database query instead of API (much faster!)
687 const { getDevice, getDeviceHardware } = require('../services/meshcentral-db');
690 const nodeInfo = await getDevice(agent.meshcentral_nodeid);
693 return res.status(200).json({
695 meshcentralNodeId: agent.meshcentral_nodeid,
696 error: 'Node not found in MeshCentral'
700 // Get hardware info from SMBIOS table
701 const hardware = await getDeviceHardware(agent.meshcentral_nodeid);
703 // Parse node data to extract useful information
705 connected: nodeInfo.connected,
706 meshcentralNodeId: agent.meshcentral_nodeid,
707 hostname: nodeInfo.hostname || agent.hostname,
708 lastSeen: nodeInfo.lastSeen || nodeInfo.lastConnect,
709 lastAddress: nodeInfo.lastAddress || null,
710 platform: nodeInfo.os || null,
711 agentVersion: nodeInfo.agentVersion || null,
713 // System information
717 // If we have hardware info, extract details
719 // MeshCentral stores hardware in different formats for Windows vs Linux
720 const hwData = hardware.windows || hardware.linux || hardware;
722 nodeData.hardware = {
723 manufacturer: hwData.identifiers?.system_manufacturer || null,
724 model: hwData.identifiers?.system_model || null,
725 cpu_model: hwData.cpu?.[0]?.name || null,
726 cpu_cores: hwData.cpu?.[0]?.cores || null,
727 ram_bytes: hwData.memory?.MemTotal ? hwData.memory.MemTotal * 1024 : null,
728 os_version: hwData.osinfo?.Caption || hwData.osinfo?.Version || null,
729 bios_version: hwData.identifiers?.bios_version || null,
730 bios_vendor: hwData.identifiers?.bios_vendor || null
733 // Parse network interfaces for IP addresses
734 if (hwData.netinfo && Array.isArray(hwData.netinfo)) {
735 for (const iface of hwData.netinfo) {
736 if (iface.address && !iface.address.startsWith('127.')) {
737 nodeData.hardware.ipv4_address = iface.address;
743 // Parse storage devices
744 if (hwData.storage && Array.isArray(hwData.storage)) {
745 nodeData.hardware.drives = hwData.storage.map(drive => ({
746 Name: drive.Caption || drive.name,
747 Total: drive.Size ? Math.round(parseInt(drive.Size) / (1024 * 1024 * 1024)) : 0,
748 FreeSpace: drive.FreeSpace ? Math.round(parseInt(drive.FreeSpace) / (1024 * 1024 * 1024)) : 0,
749 Model: drive.Model || null
757 console.error('Direct DB query failed, falling back to API:', dbErr.message);
759 // Fallback to API if database query fails
760 const MeshCentralAPI = require('../lib/meshcentral-api');
761 const meshcentralUrl = process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app';
762 const meshcentral = new MeshCentralAPI({
764 username: process.env.MESHCENTRAL_ADMIN_USER || 'admin',
765 password: process.env.MESHCENTRAL_ADMIN_PASS || 'admin'
768 const loginSuccess = await meshcentral.login();
770 return res.status(200).json({
772 error: 'MeshCentral connection unavailable'
776 const nodeInfo = await meshcentral.getNode(agent.meshcentral_nodeid);
779 return res.status(200).json({
781 meshcentralNodeId: agent.meshcentral_nodeid,
782 error: 'Node not found in MeshCentral'
786 // Parse node data to extract useful information
788 connected: nodeInfo.conn ? true : false,
789 meshcentralNodeId: agent.meshcentral_nodeid,
790 hostname: nodeInfo.name || agent.hostname,
791 lastSeen: nodeInfo.lastconnect ? new Date(nodeInfo.lastconnect * 1000).toISOString() : null,
792 lastAddress: nodeInfo.lastaddr || null,
793 platform: nodeInfo.osdesc || nodeInfo.os || null,
794 agentVersion: nodeInfo.agent?.ver || null,
796 // System information from sysinfo
800 // If we have sysinfo, extract hardware details
801 if (nodeInfo.sysinfo) {
802 const sysinfo = nodeInfo.sysinfo;
803 nodeData.hardware = {
804 manufacturer: sysinfo.hardware?.system?.manufacturer || null,
805 model: sysinfo.hardware?.system?.model || null,
806 cpu_model: sysinfo.hardware?.cpu?.[0]?.caption || null,
807 cpu_cores: sysinfo.hardware?.cpu?.[0]?.cores || null,
808 ram_bytes: sysinfo.hardware?.memory?.physical || null,
809 os_version: sysinfo.hardware?.identifiers?.os_version || null,
810 bios_version: sysinfo.hardware?.bios?.version || null,
811 bios_vendor: sysinfo.hardware?.bios?.vendor || null
814 // Parse network interfaces for IP addresses
815 if (sysinfo.hardware?.networkadapters) {
816 const interfaces = sysinfo.hardware.networkadapters;
817 for (const iface of interfaces) {
818 if (iface.address && iface.address.length > 0) {
819 const ipv4 = iface.address.find(addr => addr.includes('.') && !addr.startsWith('127.'));
821 nodeData.hardware.ipv4_address = ipv4;
828 // Parse storage devices
829 if (sysinfo.hardware?.storage) {
830 nodeData.hardware.drives = sysinfo.hardware.storage.map(drive => ({
831 Name: drive.caption || drive.name,
832 Total: drive.size ? Math.round(drive.size / (1024 * 1024 * 1024)) : 0,
833 Model: drive.model || null
841 console.error('GET MESHCENTRAL INFO ERROR:', err);
842 res.status(500).json({ error: err.message });
847 * @api {get} /agent/download-token Generate Agent Download Token
848 * @apiName GenerateDownloadToken
851 * Generates JWT token for agent installer downloads. Token includes tenantId and OS
852 * platform, valid for 24 hours. Used by agent download endpoints to validate tenant
853 * and generate appropriate installer binaries.
854 * @apiPermission public
855 * @apiNote No authentication required - returns JWT signed with AGENT_SIGN_KEY
856 * @apiQuery {number} tenantId Tenant ID for agent association
857 * @apiQuery {String="windows","linux","macos","nobara"} os Operating system platform
858 * @apiSuccess {string} jwt JWT token valid for 24 hours
859 * @apiError (400) {String} error "Missing tenantId or os"
860 * @apiError (500) {String} error "Failed to create token"
861 * @apiExample {curl} Example Request:
862 * curl "https://api.ibghub.com/api/agent/download-token?tenantId=5&os=windows"
863 * @apiExample {json} Example Response:
865 * "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRJZCI6NSwib3MiOiJ3aW5kb3dzIiwiaWF0Ijo..."
869// -------------------------
870// 1. GET /api/agent/download-token
871// -------------------------
872router.get('/download-token', async (req, res) => {
874 console.log('==============================');
875 console.log('[Agent] 🏁 /download-token called');
876 console.log('[Agent] Query:', JSON.stringify(req.query, null, 2));
877 const { tenantId, os } = req.query;
878 if (!tenantId || !os) {
879 console.log('[Agent] ❌ Missing tenantId or os');
880 res.status(400).json({ error: 'Missing tenantId or os' });
881 console.log('[Agent] Response: 400 Missing tenantId or os');
882 console.log('==============================');
886 const jwtToken = jwt.sign(
888 process.env.AGENT_SIGN_KEY,
891 console.log('[Agent] ✅ JWT generated:', jwtToken);
892 res.json({ jwt: jwtToken });
893 console.log('[Agent] Response: 200 JWT sent');
894 console.log('==============================');
896 console.error('[Agent] Token generation failed:', err);
897 res.status(500).json({ error: 'Failed to create token' });
898 console.log('[Agent] Response: 500 error', err.message);
899 console.log('==============================');
904 * @api {post} /agent/metrics Submit Agent Metrics (Batch)
905 * @apiName SubmitMetricsBatch
908 * Submits batch of performance metrics from agent. Metrics are buffered in memory
909 * aggregator and periodically flushed to database. Supports CPU, memory, disk, uptime,
910 * and raw JSON metrics.
911 * @apiPermission authenticated
913 * @apiBody {string} agent_uuid Agent UUID (required)
914 * @apiBody {number} [cpu] CPU usage percentage (0-100)
915 * @apiBody {number} [memory] Memory usage percentage (0-100)
916 * @apiBody {number} [disk] Disk usage percentage (0-100)
917 * @apiBody {number} [uptime] System uptime in seconds
918 * @apiBody {object} [raw] Raw metrics JSON object
919 * @apiSuccess (201) {String} status "buffered" - metrics queued for database insert
920 * @apiError (400) {String} error "agent_uuid required"
921 * @apiError (500) {String} error "Failed to buffer metrics"
922 * @apiExample {curl} Example Request:
923 * curl -X POST https://api.ibghub.com/api/agent/metrics \
924 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
925 * -H "Content-Type: application/json" \
926 * -d '{"agent_uuid":"550e8400-e29b-41d4-a716-446655440000","cpu":45.2,"memory":78.5,"disk":65.0,"uptime":86400}'
929// -------------------------
930// 2. POST /api/agent/metric (single metric)
931// -------------------------
932// -------------------------
933// 3. POST /api/agent/metrics (batch metric)
934// -------------------------
935router.post('/metrics', authenticateToken, (req, res) => {
937 console.log('==============================');
938 console.log('[Agent] 📊 /metrics called');
939 console.log('[Agent] Headers:', JSON.stringify({
940 'content-type': req.headers['content-type'],
941 'authorization': req.headers['authorization'] ? 'Bearer ...' + req.headers['authorization'].substring(req.headers['authorization'].length - 20) : 'MISSING',
942 'user-agent': req.headers['user-agent']
944 console.log('[Agent] Body:', JSON.stringify(req.body, null, 2));
945 const payload = req.body;
947 if (!payload.agent_uuid) {
948 console.log('[Agent] ❌ agent_uuid required');
949 res.status(400).send('agent_uuid required');
950 console.log('[Agent] Response: 400 agent_uuid required');
951 console.log('==============================');
956 res.status(201).json({ status: 'buffered' });
957 console.log('[Agent] Response: 201 metrics buffered');
958 console.log('==============================');
961 console.error('[Agent Metrics] Error:', err);
962 res.status(500).send('Failed to buffer metrics');
963 console.log('[Agent] Response: 500 error', err.message);
964 console.log('==============================');
969 * @api {get} /agent/:agentId/metrics Get Agent Metrics History
970 * @apiName GetAgentMetrics
973 * Retrieves historical performance metrics for specific agent. Returns time-series
974 * data for CPU, memory, disk usage, and uptime. Used for performance graphs and
976 * @apiPermission authenticated
978 * @apiParam {string} agentId Agent UUID
979 * @apiQuery {number} [limit=100] Maximum number of metrics to return
980 * @apiSuccess {object[]} metrics Array of metric objects (newest first)
981 * @apiSuccess {number} metrics.metric_id Metric record ID
982 * @apiSuccess {Date} metrics.timestamp Metric collection timestamp
983 * @apiSuccess {number} metrics.cpu CPU usage percentage
984 * @apiSuccess {number} metrics.memory Memory usage percentage
985 * @apiSuccess {number} metrics.disk Disk usage percentage
986 * @apiSuccess {number} metrics.uptime System uptime in seconds
987 * @apiSuccess {Object} metrics.raw Raw metrics JSON
988 * @apiError (500) {String} error "Failed to retrieve metrics"
989 * @apiExample {curl} Example Request:
990 * curl "https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/metrics?limit=50" \
991 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
994// -------------------------
995// 3b. GET /api/agent/:agentId/metrics - Get metrics history for an agent
996// -------------------------
997router.get('/:agentId/metrics', authenticateToken, async (req, res) => {
999 const { agentId } = req.params;
1000 const limit = parseInt(req.query.limit, 10) || 100;
1002 const result = await pool.query(
1011 FROM agent_metrics m
1012 JOIN agents a ON m.agent_id = a.agent_id
1013 WHERE a.agent_uuid = $1
1019 res.json(result.rows);
1021 console.error('[Agent Metrics GET] Error:', err);
1022 res.status(500).json({ error: 'Failed to retrieve metrics' });
1027 * @api {get} /agent/os-options Get Supported OS Platforms
1028 * @apiName GetOSOptions
1031 * Returns list of supported operating system platforms for agent installation.
1032 * Used by installer download UI to populate OS selection dropdown.
1033 * @apiPermission public
1034 * @apiNote No authentication required
1035 * @apiSuccess {object[]} osOptions Array of OS option objects
1036 * @apiSuccess {string} osOptions.value OS identifier (windows/linux/macos/nobara)
1037 * @apiSuccess {string} osOptions.label Human-readable OS label with installer format
1038 * @apiExample {curl} Example Request:
1039 * curl https://api.ibghub.com/api/agent/os-options
1040 * @apiExample {json} Example Response:
1043 * {"value": "windows", "label": "Windows (.exe)"},
1044 * {"value": "linux", "label": "Linux (.sh)"},
1045 * {"value": "macos", "label": "macOS (.pkg)"},
1046 * {"value": "nobara", "label": "Nobara Linux (.sh)"}
1051// -------------------------
1052// 4. GET /api/agent/os-options
1053// -------------------------
1054router.get('/os-options', (req, res) => {
1055 console.log('==============================');
1056 console.log('[Agent] 🖥️ /os-options called');
1059 { value: 'windows', label: 'Windows (.exe)' },
1060 { value: 'linux', label: 'Linux (.sh)' },
1061 { value: 'macos', label: 'macOS (.pkg)' },
1062 { value: 'nobara', label: 'Nobara Linux (.sh)' }
1065 console.log('[Agent] Response: 200 osOptions sent');
1066 console.log('==============================');
1070 * @api {post} /agent/enroll Enroll New Agent
1071 * @apiName EnrollAgent
1074 * Enrolls new agent or re-enrolls existing agent. Validates JWT token containing
1075 * tenantId and customerId claims. Prevents duplicate enrollments using hardware_uuid
1076 * fingerprinting. Auto-generates RSA keypair if not provided. Uses database upsert
1077 * with ON CONFLICT handling for idempotent enrollment.
1080 * 1. Validates JWT signed with AGENT_SIGN_KEY
1081 * 2. Checks for existing agent by hardware_uuid within tenant
1082 * 3. Generates RSA-2048 keypair if publicKey not provided
1083 * 4. Upserts agent record with ON CONFLICT UPDATE
1084 * 5. Returns agent_uuid, certificate, and privateKey (if generated)
1085 * @apiPermission authenticated
1086 * @apiHeader {string} Authorization Bearer JWT token with tenantId/customerId claims
1087 * @apiBody {string} [hardware_uuid] Hardware fingerprint for duplicate detection
1088 * @apiBody {string} [hardware_id] Legacy hardware ID (alias for hardware_uuid)
1089 * @apiBody {string} [hostname] Machine hostname
1090 * @apiBody {string} [os] Operating system (windows/linux/macos)
1091 * @apiBody {string} [version] Agent software version
1092 * @apiBody {string} [publicKey] RSA public key PEM (generated if omitted)
1093 * @apiSuccess (201) {String} status "enrolled"
1094 * @apiSuccess (201) {String} agent_uuid Agent UUID (existing or newly generated)
1095 * @apiSuccess (201) {String} certificate Agent public key PEM
1096 * @apiSuccess (201) {String} [privateKey] RSA private key PEM (only if generated)
1097 * @apiError (401) {String} error "Missing JWT in Authorization header"
1098 * @apiError (401) {String} error "Invalid or expired JWT"
1099 * @apiError (500) {String} error "Failed to enroll agent"
1100 * @apiExample {curl} Example Request (New Agent):
1101 * curl -X POST https://api.ibghub.com/api/agent/enroll \\
1102 * -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \\
1103 * -H "Content-Type: application/json" \\
1104 * -d '{"hardware_uuid":"550e8400-e29b-41d4-a716-446655440000","hostname":"WORKSTATION-01","os":"windows","version":"1.0.0"}'
1105 * @apiExample {json} Example Response (Keypair Generated):
1107 * "status": "enrolled",
1108 * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000",
1109 * "certificate": "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhki...",
1110 * "privateKey": "-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhki..."
1114// -------------------------
1115// 5. POST /api/agent/enroll
1116// -------------------------
1117router.post('/enroll', authenticateToken, async (req, res) => {
1119 // Verbose logging for registration attempts
1120 console.log('==============================');
1121 console.log('[Agent] 🚦 /enroll called');
1122 console.log('[Agent] Request Headers:', JSON.stringify(req.headers, null, 2));
1123 console.log('[Agent] Request Body:', JSON.stringify(req.body, null, 2));
1126 const authHeader = req.headers['authorization'] || '';
1127 const token = authHeader.startsWith('Bearer ')
1128 ? authHeader.slice(7)
1132 console.log('[Agent] ❌ Missing JWT in Authorization header');
1133 res.status(401).json({ error: 'Missing JWT in Authorization header' });
1134 console.log('[Agent] Response: 401 Missing JWT');
1135 console.log('==============================');
1141 const agentSignKey = process.env.AGENT_SIGN_KEY || '5bf7e505c65d07dd60178b92b0751a0eecb3947269b95b837a85bd5075d446c4';
1142 console.log('[Agent] Using AGENT_SIGN_KEY:', agentSignKey ? `${agentSignKey.substring(0, 16)}...` : 'undefined');
1143 console.log('[Agent] Token to verify:', token.substring(0, 50) + '...');
1145 claims = jwt.verify(token, agentSignKey);
1146 console.log('[Agent] ✅ JWT verified. Claims:', JSON.stringify(claims, null, 2));
1148 console.log('[Agent] ❌ Invalid or expired JWT:', jwtErr.message);
1149 console.log('[Agent] JWT Error details:', jwtErr);
1150 res.status(401).json({ error: 'Invalid or expired JWT' });
1151 console.log('[Agent] Response: 401 Invalid JWT');
1152 console.log('==============================');
1156 const { tenantId, customerId } = claims;
1157 // Support both hardware_uuid (new) and hardware_id (old) for backward compatibility
1158 let hardware_uuid = req.body.hardware_uuid || req.body.hardware_id || null;
1159 const { hostname = '', os = '', version = '' } = req.body || {};
1161 let publicKey = req.body.publicKey;
1162 let privateKey = null;
1163 // If no publicKey, generate keypair
1165 const { generateKeyPairSync } = require('crypto');
1166 const { publicKey: pub, privateKey: priv } = generateKeyPairSync('rsa', {
1167 modulusLength: 2048,
1168 publicKeyEncoding: { type: 'spki', format: 'pem' },
1169 privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
1173 console.log('[Agent] 🔑 Generated new keypair for agent');
1176 // Check for existing agent by hardware_uuid within the same tenant
1177 let agentRow = null;
1178 if (hardware_uuid) {
1179 const result = await pool.query(
1180 'SELECT agent_uuid FROM agents WHERE hardware_uuid = $1 AND tenant_id = $2',
1181 [hardware_uuid, tenantId]
1183 if (result.rows.length > 0) {
1184 agentRow = result.rows[0];
1185 console.log('[Agent] 🔄 Found existing agent by hardware_uuid:', agentRow.agent_uuid);
1188 let agent_uuid = agentRow ? agentRow.agent_uuid : crypto.randomUUID();
1190 console.log('[Agent] 📝 Enrolling agent with:');
1191 console.log(' agent_uuid:', agent_uuid);
1192 console.log(' tenantId:', tenantId);
1193 console.log(' customerId:', customerId);
1194 console.log(' hostname:', hostname);
1195 console.log(' os:', os);
1196 console.log(' version:', version);
1197 console.log(' hardware_uuid:', hardware_uuid);
1198 console.log(' publicKey:', publicKey ? '[present]' : '[missing]');
1202 (agent_uuid, tenant_id, customer_id, hostname, os, version, public_key, certificate, hardware_uuid, status, updated_at)
1203 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'enrolled',NOW())
1204 ON CONFLICT (agent_uuid)
1206 tenant_id=$2, customer_id=$3, hostname=$4,
1207 os=$5, version=$6, public_key=$7, certificate=$8, hardware_uuid=$9, status='enrolled', updated_at=NOW()`,
1220 console.log('[Agent] ✅ Agent enrolled successfully:', agent_uuid);
1221 res.status(201).json({
1224 certificate: publicKey,
1225 privateKey: privateKey
1227 console.log('[Agent] Response: 201 enrolled', {
1230 certificate: '[present]',
1231 privateKey: privateKey ? '[present]' : '[missing]'
1233 console.log('==============================');
1236 console.error('[Agent] Enrollment error:', err);
1237 res.status(500).json({ error: 'Failed to enroll agent' });
1238 console.log('[Agent] Response: 500 error', err.message);
1239 console.log('==============================');
1244 * @api {post} /agent/:agent_uuid/hardware Submit Hardware Inventory
1245 * @apiName SubmitHardwareInventory
1248 * Submits comprehensive hardware inventory from agent. Updates agent record with
1249 * system specifications, installed programs, network information, and device details.
1250 * Stores full hardware snapshot in system_info JSON field.
1252 * Hardware data collected:
1253 * - Computer name, manufacturer, model
1254 * - CPU model, RAM bytes
1255 * - Drive count and storage details
1256 * - GPU, network adapter
1257 * - OS build, domain, MAC address, IP address
1258 * - Hardware UUID for fingerprinting
1259 * - Installed programs list
1260 * - BitLocker recovery key (if detected)
1261 * @apiPermission authenticated
1262 * @apiUse AuthHeader
1263 * @apiParam {string} agent_uuid Agent UUID
1264 * @apiBody {string} [computerName] Computer name/hostname
1265 * @apiBody {Array} [drives] Array of drive objects with size/free space
1266 * @apiBody {string} [cpu] CPU model name
1267 * @apiBody {number} [ram] RAM in bytes
1268 * @apiBody {string} [bitlockerKey] BitLocker recovery key
1269 * @apiBody {string} [gpu] GPU model
1270 * @apiBody {string} [adapter] Network adapter model
1271 * @apiBody {string} [os_build] OS build version
1272 * @apiBody {string} [username] Logged-in username
1273 * @apiBody {string} [manufacturer] System manufacturer
1274 * @apiBody {string} [model] System model
1275 * @apiBody {string} [domain] Windows domain
1276 * @apiBody {string} [mac_address] Primary MAC address
1277 * @apiBody {string} [uuid] Hardware UUID
1278 * @apiBody {Array} [installedPrograms] Array of installed software
1279 * @apiBody {string} [ip_address] Primary IPv4 address
1280 * @apiSuccess {boolean} success true
1281 * @apiError (500) {String} error "Failed to update hardware info"
1283 * @apiExample {curl} Example Request:
1284 * curl -X POST https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/hardware \
1285 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
1286 * -H "Content-Type: application/json" \
1287 * -d '{"computerName":"WORKSTATION-01","cpu":"Intel Core i7-8700","ram":16777216000,"drives":[{"name":"C:","total":500000000000,"free":250000000000}],"installedPrograms":[{"name":"Google Chrome","version":"120.0"}]}'
1290// -------------------------
1291// 6. POST /api/agent/:agent_uuid/hardware
1292// -------------------------
1293router.post('/:agent_uuid/hardware', authenticateToken, async (req, res) => {
1294 const agent_uuid = req.params.agent_uuid;
1295 // Accept all dashboard fields + agent hardware info
1311 // Agent hardware info fields
1321 // Map agent fields to database fields
1322 const agentHostname = computerName || hostname;
1323 const agentOs = platform;
1324 const agentCpu = cpu || (cpus && cpus[0]);
1325 const agentRam = ram || memory;
1326 const agentIp = ipv4_address;
1328 // Convert drives to integer count (handle array, object, or number)
1329 let driveCount = null;
1330 if (typeof drives === 'number') {
1331 driveCount = drives;
1332 } else if (Array.isArray(drives)) {
1333 driveCount = drives.length;
1334 } else if (drives && typeof drives === 'object') {
1335 driveCount = Object.keys(drives).length;
1341 hostname = COALESCE($1, hostname),
1342 os = COALESCE($2, os),
1343 computer_name = COALESCE($3, computer_name),
1345 cpu_model = COALESCE($5, cpu_model),
1346 ram_bytes = COALESCE($6, ram_bytes),
1356 hardware_uuid = COALESCE($16, hardware_uuid),
1358 ip_address = COALESCE($18, ip_address),
1360 WHERE agent_uuid = $19`,
1378 JSON.stringify(req.body), // Store full hardware info including installedPrograms
1383 console.log(`[Agent] Updated hardware info for agent ${agent_uuid}`,
1384 installedPrograms ? `(${installedPrograms.length} programs)` : '');
1385 res.json({ success: true });
1387 console.error('[Agent] Hardware info update error:', err);
1388 res.status(500).json({ error: 'Failed to update hardware info' });
1393 * @api {get} /agent/:agent_uuid/hardware Get Hardware Inventory
1394 * @apiName GetHardwareInventory
1397 * Retrieves hardware inventory for agent. Returns system specifications, drive details,
1398 * and device information. Data extracted from agents table and system_info JSON field.
1399 * @apiPermission authenticated
1400 * @apiUse AuthHeader
1401 * @apiParam {string} agent_uuid Agent UUID
1402 * @apiSuccess {string} computerName Computer name
1403 * @apiSuccess {number} drive_count Number of drives
1404 * @apiSuccess {string} cpu_model CPU model name
1405 * @apiSuccess {string} cpu Alias for cpu_model
1406 * @apiSuccess {string[]} cpus Array format CPU models
1407 * @apiSuccess {number} ram_bytes Total RAM in bytes
1408 * @apiSuccess {number} ram Alias for ram_bytes
1409 * @apiSuccess {number} memory Alias for ram_bytes
1410 * @apiSuccess {string} bitlockerKey BitLocker recovery key
1411 * @apiSuccess {string} bitlocker_key Snake_case alias
1412 * @apiSuccess {string} gpu GPU model
1413 * @apiSuccess {string} adapter Network adapter
1414 * @apiSuccess {string} os_build OS build version
1415 * @apiSuccess {string} username Logged-in username
1416 * @apiSuccess {string} manufacturer System manufacturer
1417 * @apiSuccess {string} model System model
1418 * @apiSuccess {String} domain Windows domain
1419 * @apiSuccess {String} mac_address Primary MAC address
1420 * @apiSuccess {String} uuid Hardware UUID
1421 * @apiSuccess {String} platform Operating system
1422 * @apiSuccess {String} ip_address Primary IPv4 address
1423 * @apiSuccess {String} ipv4_address Alias for ip_address
1424 * @apiSuccess {Array} drives Array of drive objects from system_info
1426 * @apiError (404) {String} error "Hardware info not found for agent"
1427 * @apiError (500) {String} error "Failed to fetch hardware info"
1429 * @apiExample {curl} Example Request:
1430 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/hardware \
1431 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1434// -------------------------
1435// GET /api/agent/:agent_uuid/hardware
1436// -------------------------
1437router.get('/:agent_uuid/hardware', authenticateToken, async (req, res) => {
1438 const agent_uuid = req.params.agent_uuid;
1440 const result = await pool.query(
1441 `SELECT computer_name, drive_count, cpu_model, ram_bytes, bitlocker_key, gpu, adapter, os_build, username, manufacturer, model, domain, mac_address, hardware_uuid, os, ip_address, system_info FROM agents WHERE agent_uuid = $1 LIMIT 1`,
1444 if (result.rows.length === 0) {
1445 return res.status(404).json({ error: 'Hardware info not found for agent' });
1447 const row = result.rows[0];
1449 // Parse system_info to get drives and other detailed data
1450 const systemInfo = row.system_info || {};
1452 // Return fields in format frontend expects
1454 computerName: row.computer_name,
1455 drive_count: row.drive_count,
1456 cpu_model: row.cpu_model,
1457 cpu: row.cpu_model, // Alias for compatibility
1458 cpus: row.cpu_model ? [row.cpu_model] : null, // Array format
1459 ram_bytes: row.ram_bytes,
1460 ram: row.ram_bytes, // Alias
1461 memory: row.ram_bytes, // Alias
1462 bitlockerKey: row.bitlocker_key,
1463 bitlocker_key: row.bitlocker_key,
1465 adapter: row.adapter,
1466 os_build: row.os_build,
1467 username: row.username,
1468 manufacturer: row.manufacturer,
1471 mac_address: row.mac_address,
1472 uuid: row.hardware_uuid,
1474 ip_address: row.ip_address,
1475 ipv4_address: row.ip_address, // Alias
1476 // Include drives from system_info
1477 drives: systemInfo.drives || []
1480 console.error('[Agent] Hardware info fetch error:', err);
1481 res.status(500).json({ error: 'Failed to fetch hardware info' });
1486 * @api {get} /agent/list List All Agents (Filtered)
1487 * @apiName ListAgents
1490 * Lists all agents with pagination, filtering, and sorting. Includes LEFT JOIN to
1491 * customers and tenants for customer_name and tenant_name. Supports search across
1492 * hostname, agent name, customer name, and OS. Enforces tenant isolation via user JWT.
1495 * - Pagination (limit/offset)
1496 * - Search across hostname, name, customer name, OS
1497 * - Customer ID filtering
1498 * - Sort by any column (asc/desc)
1499 * - Total count for pagination UI
1500 * @apiPermission authenticated
1501 * @apiUse AuthHeader
1502 * @apiQuery {number} [limit=50] Maximum agents to return
1503 * @apiQuery {number} [offset=0] Pagination offset
1504 * @apiQuery {string} [search] Search term (hostname, name, customer, OS)
1505 * @apiQuery {number} [ Customer ID filter
1506 * @apiQuery {string} [sortBy=agent_id] Column to sort by
1507 * @apiQuery {String="asc","desc"} [sortOrder=asc] Sort direction
1508 * @apiSuccess {object[]} agents Array of agent objects
1509 * @apiSuccess {number} total Total count of matching agents
1510 * @apiSuccess {number} agents.agent_id Database agent ID
1511 * @apiSuccess {string} agents.agent_uuid Agent UUID
1512 * @apiSuccess {string} agents.hostname Machine hostname
1513 * @apiSuccess {string} agents.name Custom agent name
1514 * @apiSuccess {string} agents.os Operating system
1515 * @apiSuccess {Date} agents.last_seen Last heartbeat timestamp
1516 * @apiSuccess {string} agents.status Agent status (enrolled/active/offline)
1517 * @apiSuccess {string} agents.customer_name Customer name (from join)
1518 * @apiSuccess {string} agents.tenant_name Tenant name (from join)
1519 * @apiError (500) {String} error "Failed to list agents"
1520 * @apiExample {curl} Example Request:
1521 * curl "https://api.ibghub.com/api/agent/list?limit=20&search=windows&sortBy=last_seen&sortOrder=desc" \
1522 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1525// -------------------------
1526// 7. GET /api/agent/list (list agents)
1527// -------------------------
1528router.get('/list', authenticateToken, async (req, res) => {
1530 console.log('==============================');
1531 console.log('[Agent] 📋 /list called');
1532 console.log('[Agent] Query:', JSON.stringify(req.query, null, 2));
1533 // PATCH: Remove tenant filtering for agent list (debug/dev mode)
1534 const conditions = [];
1539 if (req.query.customer_id) {
1540 params.push(req.query.customer_id);
1541 conditions.push(`a.customer_id = $${paramIdx++}`);
1543 if (req.query.search) {
1544 const search = `%${req.query.search}%`;
1545 params.push(search);
1546 conditions.push(`(a.name ILIKE $${paramIdx} OR a.hostname ILIKE $${paramIdx} OR c.name ILIKE $${paramIdx} OR a.os ILIKE $${paramIdx})`);
1550 params.push(req.query.os);
1551 conditions.push(`a.os = $${paramIdx++}`);
1554 // Always exclude default agent_uuid and require status='enrolled'
1555 const baseConditions = [
1556 "a.agent_uuid != '00000000-0000-0000-0000-000000000001'",
1557 "a.status = 'enrolled'"
1559 const allConditions = baseConditions.concat(conditions);
1560 const whereClause = allConditions.length > 0 ? 'WHERE ' + allConditions.join(' AND ') : '';
1563 const page = parseInt(req.query.page, 10) || 1;
1564 const limit = parseInt(req.query.limit, 10) || 20;
1565 const offset = (page - 1) * limit;
1567 // Build columns based on user type
1580 "c.name AS customer_name"
1583 columns.push("a.tenant_id", "t.name AS tenant_name");
1585 // Optionally join contracts if needed
1586 columns.push("co.title AS contract_title");
1589 const agentListSQL = `
1591 ${columns.join(",\n ")}
1593 LEFT JOIN customers c ON c.customer_id = a.customer_id
1594 LEFT JOIN tenants t ON a.tenant_id = t.tenant_id
1595 LEFT JOIN contracts co ON co.contract_id = a.contract_id
1598 LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
1600 console.log('[Agent] Final SQL:', agentListSQL);
1601 console.log('[Agent] SQL Params:', [...params, limit, offset]);
1604 const totalResult = await pool.query(`
1605 SELECT COUNT(*) AS total
1607 LEFT JOIN customers c ON c.customer_id = a.customer_id
1610 const total = parseInt(totalResult.rows[0]?.total || '0', 10);
1615 result = await pool.query(agentListSQL, [...params, limit, offset]);
1616 console.log('[Agent] Query result rows:', result.rows);
1617 console.log('[Agent] Response: 200 agent list sent');
1618 console.log('==============================');
1619 } catch (queryErr) {
1620 console.error('[Agent] SQL error in agent list:', queryErr);
1621 res.status(500).json({ error: 'Failed to list agents (SQL)'});
1622 console.log('[Agent] Response: 500 error', queryErr.message);
1623 console.log('==============================');
1627 res.json({ agents: result.rows, total });
1629 console.error('[Agent] Failed to list agents:', err);
1630 res.status(500).json({ error: 'Failed to list agents' });
1631 console.log('[Agent] Response: 500 error', err.message);
1632 console.log('==============================');
1637 * @api {get} /agent/:id Get Agent by ID
1638 * @apiName GetAgentById
1641 * Retrieves single agent by agent_id (numeric) or agent_uuid. Includes LEFT JOIN to
1642 * customers, tenants, and contracts for related names/titles. Enforces tenant isolation.
1643 * @apiPermission authenticated
1644 * @apiUse AuthHeader
1645 * @apiParam {string} id Agent ID (numeric) or agent UUID
1646 * @apiSuccess {object} agent Full agent object with relationships
1647 * @apiSuccess {number} agent.agent_id Database agent ID
1648 * @apiSuccess {string} agent.agent_uuid Agent UUID
1649 * @apiSuccess {string} agent.hostname Machine hostname
1650 * @apiSuccess {string} agent.name Custom agent name
1651 * @apiSuccess {string} agent.os Operating system
1652 * @apiSuccess {string} agent.version Agent software version
1653 * @apiSuccess {Date} agent.last_seen Last heartbeat timestamp
1654 * @apiSuccess {string} agent.status Agent status
1655 * @apiSuccess {string} agent.meshcentral_nodeid MeshCentral node ID
1656 * @apiSuccess {string} agent.customer_name Customer name (joined)
1657 * @apiSuccess {string} agent.tenant_name Tenant name (joined)
1658 * @apiSuccess {string} agent.contract_title Contract title (joined)
1659 * @apiSuccess {Object} agent.system_info Full hardware inventory JSON
1660 * @apiError (400) {String} error "Invalid agent identifier"
1661 * @apiError (404) {String} error "Agent not found"
1662 * @apiError (500) {String} error "Failed to load agent"
1663 * @apiExample {curl} Example Request (UUID):
1664 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000 \
1665 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1666 * @apiExample {curl} Example Request (Numeric ID):
1667 * curl https://api.ibghub.com/api/agent/123 \
1668 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1671// -------------------------
1672// 8. GET /api/agent/:id (single agent)
1673// -------------------------
1674router.get('/:id',authenticateToken, async (req, res) => {
1676 console.log('==============================');
1677 console.log('[Agent] 🔍 /:id called');
1678 console.log('[Agent] Params:', JSON.stringify(req.params, null, 2));
1679 const { id } = req.params;
1681 // PATCH: Remove tenant filtering for single agent (debug/dev mode)
1682 let conditions = [];
1686 // UUID or numeric ID
1687 if (id.includes('-')) {
1688 conditions.push(`a.agent_uuid = $${paramIdx++}`);
1690 } else if (!isNaN(id)) {
1691 conditions.push(`a.agent_id = $${paramIdx++}`);
1694 res.status(400).json({ error: 'Invalid agent identifier' });
1695 console.log('[Agent] Response: 400 Invalid agent identifier');
1696 console.log('==============================');
1700 const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
1702 SELECT a.*, c.name AS customer_name, t.name AS tenant_name, co.title AS contract_title
1704 LEFT JOIN customers c ON c.customer_id = a.customer_id
1705 LEFT JOIN tenants t ON a.tenant_id = t.tenant_id
1706 LEFT JOIN contracts co ON co.contract_id = a.contract_id
1712 result = await pool.query(agentSQL, params);
1713 } catch (queryErr) {
1714 console.error('[Agent] SQL error in single agent:', queryErr);
1715 res.status(500).json({ error: 'Failed to load agent (SQL)'});
1716 console.log('[Agent] Response: 500 error', queryErr.message);
1717 console.log('==============================');
1721 if (result.rows.length === 0) {
1722 res.status(404).json({ error: 'Agent not found' });
1723 console.log('[Agent] Response: 404 Agent not found');
1724 console.log('==============================');
1728 res.json({ agent: result.rows[0] });
1729 console.log('[Agent] Response: 200 agent sent', result.rows[0]);
1730 console.log('==============================');
1733 console.error('[Agent] Failed to load agent:', err);
1734 res.status(500).json({ error: 'Failed to load agent' });
1735 console.log('[Agent] Response: 500 error', err.message);
1736 console.log('==============================');
1741 * @api {delete} /agent/:id Delete Agent
1742 * @apiName DeleteAgent
1745 * Deletes agent from database and publishes Redis uninstall event for agent cleanup.
1746 * Supports both numeric agent_id and agent_uuid identifiers. Checks Redis for scheduled
1747 * deletions if agent not found in database.
1750 * 1. Parses agent identifier (UUID or numeric ID)
1751 * 2. Queries agent record before deletion
1752 * 3. Deletes agent from database
1753 * 4. Publishes Redis event: `agent:uninstall:{agent_uuid}`
1754 * 5. Returns deleted agent UUID
1755 * @apiPermission authenticated
1756 * @apiUse AuthHeader
1757 * @apiParam {string} id Agent ID (numeric) or agent UUID
1758 * @apiSuccess {string} status "deleted"
1759 * @apiSuccess {string} agentId Deleted agent UUID
1760 * @apiError (400) {String} error "Invalid agent identifier"
1761 * @apiError (404) {String} error "Agent not found (may already be deleted or purged)"
1762 * @apiError (404) {String} error "Agent scheduled for deletion (soft delete in progress)"
1763 * @apiError (500) {String} error "Failed to delete agent"
1764 * @apiExample {curl} Example Request:
1765 * curl -X DELETE https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000 \
1766 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1767 * @apiExample {json} Example Response:
1769 * "status": "deleted",
1770 * "agentId": "550e8400-e29b-41d4-a716-446655440000"
1774// -------------------------
1775// 9. DELETE /api/agent/:id (delete and unenroll agent)
1776// -------------------------
1777router.delete('/:id', authenticateToken, async (req, res) => {
1779 console.log('==============================');
1780 console.log('[Agent] 🗑️ DELETE /:id called');
1781 const { id } = req.params;
1782 // Remove agent from DB
1783 let conditions = [];
1786 if (id.includes('-')) {
1787 conditions.push(`agent_uuid = $${paramIdx++}`);
1789 } else if (!isNaN(id)) {
1790 conditions.push(`agent_id = $${paramIdx++}`);
1793 res.status(400).json({ error: 'Invalid agent identifier' });
1794 console.log('[Agent] Response: 400 Invalid agent identifier');
1795 console.log('==============================');
1798 const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
1799 // Get agent info before delete
1800 const selectSQL = `SELECT * FROM agents ${whereClause} LIMIT 1`;
1801 const agentResult = await pool.query(selectSQL, params);
1802 if (agentResult.rows.length === 0) {
1803 // Check if agent is scheduled for deletion in Redis
1805 const Redis = require('ioredis');
1806 const redis = new Redis(require('../config/redis'));
1807 const redisKey = `agent:delete:${id}`;
1808 const scheduled = await redis.exists(redisKey);
1811 res.status(404).json({ error: 'Agent not found in DB, but scheduled for deletion (soft delete in progress)' });
1812 console.log('[Agent] Response: 404 Agent scheduled for deletion');
1814 res.status(404).json({ error: 'Agent not found (may already be deleted or purged)' });
1815 console.log('[Agent] Response: 404 Agent not found or purged');
1817 } catch (redisErr) {
1818 res.status(404).json({ error: 'Agent not found (Redis check failed)' });
1819 console.log('[Agent] Response: 404 Agent not found, Redis error:', redisErr.message);
1821 console.log('==============================');
1825 const deleteSQL = `DELETE FROM agents ${whereClause}`;
1826 await pool.query(deleteSQL, params);
1827 // Publish Redis uninstall event
1828 const redis = require('../services/redis');
1829 const agent = agentResult.rows[0];
1830 const uninstallEvent = `agent:uninstall:${agent.agent_uuid}`;
1831 await redis.publish('agent-events', JSON.stringify({ event: uninstallEvent, agentId: agent.agent_uuid }));
1832 console.log('[Agent] Redis uninstall event published:', uninstallEvent);
1833 res.json({ status: 'deleted', agentId: agent.agent_uuid });
1834 console.log('[Agent] Response: 200 agent deleted');
1835 console.log('==============================');
1837 console.error('[Agent] Failed to delete agent:', err);
1838 res.status(500).json({ error: 'Failed to delete agent' });
1839 console.log('[Agent] Response: 500 error', err.message);
1840 console.log('==============================');
1845 * @api {get} /agent/:agent_uuid/programs Get Installed Programs
1846 * @apiName GetInstalledPrograms
1849 * Retrieves list of installed software from agent's system_info JSON field.
1850 * Programs data collected during hardware inventory submission. Used for software
1851 * auditing, license management, and vulnerability scanning.
1852 * @apiPermission authenticated
1853 * @apiUse AuthHeader
1854 * @apiParam {string} agent_uuid Agent UUID
1855 * @apiSuccess {object[]} programs Array of installed program objects
1856 * @apiSuccess {string} programs.name Program name
1857 * @apiSuccess {string} programs.version Program version
1858 * @apiSuccess {string} programs.publisher Software publisher
1859 * @apiSuccess {Date} programs.installDate Installation date
1860 * @apiError (404) {String} error "Agent not found"
1861 * @apiError (500) {String} error "Failed to fetch programs"
1862 * @apiExample {curl} Example Request:
1863 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/programs \
1864 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1867// -------------------------
1868// GET /api/agent/:agent_uuid/programs
1869// Get installed programs from agent
1870// -------------------------
1871router.get('/:agent_uuid/programs', authenticateToken, async (req, res) => {
1873 console.log('==============================');
1874 console.log('[Agent] 📦 /programs called for:', req.params.agent_uuid);
1876 const { agent_uuid } = req.params;
1878 // Check if agent exists and get installed programs from system_info
1879 const result = await pool.query(
1880 'SELECT system_info FROM agents WHERE agent_uuid = $1',
1884 if (result.rows.length === 0) {
1885 console.log('[Agent] ❌ Agent not found');
1886 res.status(404).json({ error: 'Agent not found' });
1887 console.log('[Agent] Response: 404 Agent not found');
1888 console.log('==============================');
1892 const systemInfo = result.rows[0].system_info || {};
1893 const programs = systemInfo.installedPrograms || [];
1895 console.log('[Agent] ✅ Found', programs.length, 'programs');
1897 console.log('[Agent] Response: 200 programs sent');
1898 console.log('==============================');
1901 console.error('[Agent] Failed to fetch programs:', err);
1902 res.status(500).json({ error: 'Failed to fetch programs' });
1903 console.log('[Agent] Response: 500 error', err.message);
1904 console.log('==============================');
1909 * @api {get} /agent/:agent_uuid/processes Get Running Processes
1910 * @apiName GetRunningProcesses
1913 * Retrieves list of running processes from agent's system_info JSON field.
1914 * Process snapshot collected during hardware inventory submission. Used for
1915 * performance monitoring, troubleshooting, and security analysis.
1916 * @apiPermission authenticated
1917 * @apiUse AuthHeader
1918 * @apiParam {string} agent_uuid Agent UUID
1919 * @apiSuccess {object[]} processes Array of process objects
1920 * @apiSuccess {number} processes.pid Process ID
1921 * @apiSuccess {string} processes.name Process name
1922 * @apiSuccess {number} processes.cpu CPU usage percentage
1923 * @apiSuccess {number} processes.memory Memory usage in bytes
1924 * @apiError (404) {String} error "Agent not found"
1925 * @apiError (500) {String} error "Failed to fetch processes"
1926 * @apiExample {curl} Example Request:
1927 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/processes \
1928 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1931// GET /api/agent/:agent_uuid/processes
1932// Get running processes from agent (real-time via WebSocket or cached)
1933// -------------------------
1934router.get('/:agent_uuid/processes', authenticateToken, async (req, res) => {
1936 console.log('==============================');
1937 console.log('[Agent] 🔄 /processes called for:', req.params.agent_uuid);
1939 const { agent_uuid } = req.params;
1941 // Check if agent exists and get processes from system_info
1942 const result = await pool.query(
1943 'SELECT system_info FROM agents WHERE agent_uuid = $1',
1947 if (result.rows.length === 0) {
1948 console.log('[Agent] ❌ Agent not found');
1949 res.status(404).json({ error: 'Agent not found' });
1950 console.log('[Agent] Response: 404 Agent not found');
1951 console.log('==============================');
1955 const systemInfo = result.rows[0].system_info || {};
1956 const processes = systemInfo.processes || [];
1958 console.log('[Agent] ✅ Found', processes.length, 'processes');
1959 res.json(processes);
1960 console.log('[Agent] Response: 200 processes sent');
1961 console.log('==============================');
1964 console.error('[Agent] Failed to fetch processes:', err);
1965 res.status(500).json({ error: 'Failed to fetch processes' });
1966 console.log('[Agent] Response: 500 error', err.message);
1967 console.log('==============================');
1972 * @api {get} /agent/:agent_uuid/files Get File Listing (Not Implemented)
1973 * @apiName GetFileListing
1976 * Placeholder endpoint for file browser feature. Currently returns empty array.
1977 * Future implementation will provide real-time filesystem browsing over WebSocket.
1978 * @apiPermission authenticated
1979 * @apiUse AuthHeader
1980 * @apiParam {string} agent_uuid Agent UUID
1981 * @apiQuery {string} [path=C:\] Directory path to browse
1982 * @apiSuccess {string} path Current directory path
1983 * @apiSuccess {Array} files Empty array (not yet implemented)
1984 * @apiError (404) {String} error "Agent not found"
1985 * @apiError (500) {String} error "Failed to fetch files"
1986 * @apiNote Feature not yet implemented - returns placeholder response
1987 * @apiExample {curl} Example Request:
1988 * curl "https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files?path=C:\\Windows" \
1989 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1992// GET /api/agent/:agent_uuid/files
1993// Get file listing from agent (placeholder - not yet implemented)
1994// -------------------------
1995router.get('/:agent_uuid/files', authenticateToken, async (req, res) => {
1997 const { agent_uuid } = req.params;
1998 const { path } = req.query;
2000 // Check if agent exists
2001 const result = await pool.query(
2002 'SELECT agent_id FROM agents WHERE agent_uuid = $1',
2006 if (result.rows.length === 0) {
2007 return res.status(404).json({ error: 'Agent not found' });
2010 // File browser feature not yet implemented
2011 // Return empty array for now
2013 path: path || 'C:\\',
2018 console.error('[Agent] Failed to fetch files:', err);
2019 res.status(500).json({ error: 'Failed to fetch files' });
2024 * @api {get} /agent/:agent_uuid/services Get Windows Services
2025 * @apiName GetWindowsServices
2028 * Retrieves Windows services list from agent's system_info JSON field. Services data
2029 * collected during hardware inventory. Includes service status, startup type, and
2030 * description. Used for service management and troubleshooting.
2031 * @apiPermission authenticated
2032 * @apiUse AuthHeader
2033 * @apiParam {string} agent_uuid Agent UUID
2034 * @apiSuccess {object[]} services Array of service objects
2035 * @apiSuccess {string} services.name Service name
2036 * @apiSuccess {string} services.displayName Service display name
2037 * @apiSuccess {string} services.status Service status (Running/Stopped)
2038 * @apiSuccess {string} services.startType Startup type (Automatic/Manual/Disabled)
2039 * @apiSuccess {string} services.description Service description
2040 * @apiError (404) {String} error "Agent not found"
2041 * @apiError (500) {String} error "Failed to fetch services"
2042 * @apiExample {curl} Example Request:
2043 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/services \
2044 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2047// GET /api/agent/:agent_uuid/services
2048// Get Windows services from agent
2049// -------------------------
2050router.get('/:agent_uuid/services', authenticateToken, async (req, res) => {
2052 console.log('==============================');
2053 console.log('[Agent] 🔄 /services called for:', req.params.agent_uuid);
2055 const { agent_uuid } = req.params;
2057 // Check if agent exists and get services from system_info
2058 const result = await pool.query(
2059 'SELECT system_info FROM agents WHERE agent_uuid = $1',
2063 if (result.rows.length === 0) {
2064 console.log('[Agent] ❌ Agent not found');
2065 res.status(404).json({ error: 'Agent not found' });
2066 console.log('[Agent] Response: 404 Agent not found');
2067 console.log('==============================');
2071 const systemInfo = result.rows[0].system_info || {};
2072 const services = systemInfo.services || [];
2074 console.log('[Agent] ✅ Found', services.length, 'services');
2076 console.log('[Agent] Response: 200 services sent');
2077 console.log('==============================');
2080 console.error('[Agent] Failed to fetch services:', err);
2081 res.status(500).json({ error: 'Failed to fetch services' });
2082 console.log('[Agent] Response: 500 error', err.message);
2083 console.log('==============================');
2088 * @api {get} /agent/:agent_uuid/activity-logs Get Activity Logs
2089 * @apiName GetActivityLogs
2092 * Retrieves user activity logs for agent from multiple sources. Combines activity_logs
2093 * table events with script execution history. Used for audit trails, compliance, and
2097 * - Remote sessions (RDP, VNC, SSH)
2099 * - Script executions
2100 * - Configuration changes
2102 * @apiPermission authenticated
2103 * @apiUse AuthHeader
2104 * @apiParam {string} agent_uuid Agent UUID
2105 * @apiQuery {number} [limit=100] Maximum logs to return
2106 * @apiSuccess {object[]} logs Array of activity log objects (newest first)
2107 * @apiSuccess {string} logs.action Activity action type
2108 * @apiSuccess {string} logs.details Activity details/description
2109 * @apiSuccess {Date} logs.timestamp Activity timestamp
2110 * @apiSuccess {string} logs.user User email or "system"
2111 * @apiError (500) {String} error "Failed to fetch activity logs"
2112 * @apiExample {curl} Example Request:
2113 * curl "https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/activity-logs?limit=50" \
2114 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2117// GET /api/agent/:agent_uuid/activity-logs
2118// Get user activity logs (remote sessions, scripts, file transfers, etc.)
2119// -------------------------
2120router.get('/:agent_uuid/activity-logs', authenticateToken, async (req, res) => {
2122 console.log('==============================');
2123 console.log('[Agent] 🔄 /activity-logs called for:', req.params.agent_uuid);
2125 const { agent_uuid } = req.params;
2126 const limit = parseInt(req.query.limit) || 100;
2128 // Get activity logs from database (combining multiple sources)
2129 const logs = await pool.query(`
2132 details::text as details,
2133 created_at as timestamp,
2136 WHERE agent_uuid = $1
2141 'script_execution' as action,
2142 'Script executed: ' || s.name as details,
2143 se.created_at as timestamp,
2145 FROM script_executions se
2146 JOIN scripts s ON se.script_id = s.script_id
2147 WHERE se.agent_uuid = $1
2149 ORDER BY timestamp DESC
2151 `, [agent_uuid, limit]);
2153 console.log('[Agent] ✅ Found', logs.rows.length, 'activity logs');
2154 res.json(logs.rows);
2155 console.log('[Agent] Response: 200 activity logs sent');
2156 console.log('==============================');
2159 console.error('[Agent] Failed to fetch activity logs:', err);
2160 res.status(500).json({ error: 'Failed to fetch activity logs' });
2161 console.log('[Agent] Response: 500 error', err.message);
2162 console.log('==============================');
2167 * @api {get} /agent/:agent_uuid/events Get Windows Event Logs
2168 * @apiName GetWindowsEvents
2171 * Retrieves Windows Event Viewer logs from agent's system_info JSON field. Event logs
2172 * collected during hardware inventory. Includes Application, System, and Security logs.
2173 * Used for system diagnostics, security monitoring, and troubleshooting.
2174 * @apiPermission authenticated
2175 * @apiUse AuthHeader
2176 * @apiParam {string} agent_uuid Agent UUID
2177 * @apiQuery {number} [limit=100] Maximum events to return
2178 * @apiSuccess {object[]} events Array of event log objects
2179 * @apiSuccess {string} events.logName Log source (Application/System/Security)
2180 * @apiSuccess {string} events.level Event level (Error/Warning/Information)
2181 * @apiSuccess {number} events.eventId Windows event ID
2182 * @apiSuccess {Date} events.timeCreated Event timestamp
2183 * @apiSuccess {string} events.message Event message
2184 * @apiError (404) {String} error "Agent not found"
2185 * @apiError (500) {String} error "Failed to fetch event logs"
2186 * @apiExample {curl} Example Request:
2187 * curl "https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/events?limit=200" \
2188 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2191// GET /api/agent/:agent_uuid/events
2192// Get Windows Event Viewer logs from agent
2193// -------------------------
2194router.get('/:agent_uuid/events', authenticateToken, async (req, res) => {
2196 console.log('==============================');
2197 console.log('[Agent] 🔄 /events called for:', req.params.agent_uuid);
2199 const { agent_uuid } = req.params;
2200 const limit = parseInt(req.query.limit) || 100;
2202 // Get Windows event logs from system_info
2203 const result = await pool.query(
2204 'SELECT system_info FROM agents WHERE agent_uuid = $1',
2208 if (result.rows.length === 0) {
2209 console.log('[Agent] ❌ Agent not found');
2210 res.status(404).json({ error: 'Agent not found' });
2211 console.log('[Agent] Response: 404 Agent not found');
2212 console.log('==============================');
2216 const systemInfo = result.rows[0].system_info || {};
2217 let events = systemInfo.event_logs || [];
2219 // Limit the number of events returned
2220 if (events.length > limit) {
2221 events = events.slice(0, limit);
2224 console.log('[Agent] ✅ Found', events.length, 'event logs');
2226 console.log('[Agent] Response: 200 event logs sent');
2227 console.log('==============================');
2230 console.error('[Agent] Failed to fetch event logs:', err);
2231 res.status(500).json({ error: 'Failed to fetch event logs' });
2232 console.log('[Agent] Response: 500 error', err.message);
2233 console.log('==============================');
2238 * @api {get} /agent/:agent_uuid/agent-logs Get Agent Internal Logs
2239 * @apiName GetAgentLogs
2242 * Retrieves agent application logs from system_info JSON field. Logs include agent
2243 * heartbeats, errors, updates, and internal operations. Used for agent troubleshooting
2245 * @apiPermission authenticated
2246 * @apiUse AuthHeader
2247 * @apiParam {string} agent_uuid Agent UUID
2248 * @apiSuccess {object[]} logs Array of agent log objects
2249 * @apiSuccess {string} logs.level Log level (ERROR/WARN/INFO/DEBUG)
2250 * @apiSuccess {Date} logs.timestamp Log timestamp
2251 * @apiSuccess {string} logs.message Log message
2252 * @apiSuccess {object} logs.metadata Additional log metadata
2253 * @apiError (404) {String} error "Agent not found"
2254 * @apiError (500) {String} error "Failed to fetch agent logs"
2255 * @apiExample {curl} Example Request:
2256 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/agent-logs \
2257 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2260// GET /api/agent/:agent_uuid/agent-logs
2261// Get agent internal logs (heartbeats, errors, updates)
2262// -------------------------
2263router.get('/:agent_uuid/agent-logs', authenticateToken, async (req, res) => {
2265 console.log('==============================');
2266 console.log('[Agent] 🔄 /agent-logs called for:', req.params.agent_uuid);
2268 const { agent_uuid } = req.params;
2270 // Get agent logs from system_info or a dedicated logs table
2271 const result = await pool.query(
2272 'SELECT system_info FROM agents WHERE agent_uuid = $1',
2276 if (result.rows.length === 0) {
2277 console.log('[Agent] ❌ Agent not found');
2278 res.status(404).json({ error: 'Agent not found' });
2279 console.log('[Agent] Response: 404 Agent not found');
2280 console.log('==============================');
2284 const systemInfo = result.rows[0].system_info || {};
2285 const agentLogs = systemInfo.agent_logs || [];
2287 console.log('[Agent] ✅ Found', agentLogs.length, 'agent logs');
2288 res.json(agentLogs);
2289 console.log('[Agent] Response: 200 agent logs sent');
2290 console.log('==============================');
2293 console.error('[Agent] Failed to fetch agent logs:', err);
2294 res.status(500).json({ error: 'Failed to fetch agent logs' });
2295 console.log('[Agent] Response: 500 error', err.message);
2296 console.log('==============================');
2301 * @api {get} /agent/version Get Latest Agent Version
2302 * @apiName GetAgentVersion
2305 * Returns latest agent software version. Queries agent_versions table for is_latest flag,
2306 * falls back to agent/package.json if database is empty. Used by auto-update checks
2307 * and installer downloads.
2308 * @apiPermission public
2309 * @apiNote No authentication required
2310 * @apiSuccess {string} version Semantic version string (e.g., "1.2.3")
2311 * @apiError (500) {String} error "Failed to read agent version"
2312 * @apiExample {curl} Example Request:
2313 * curl https://api.ibghub.com/api/agent/version
2314 * @apiExample {json} Example Response:
2316 * "version": "1.2.3"
2320// =============================================
2321// GET /api/agent/version
2322// Returns the latest agent version from database
2323// =============================================
2324router.get('/version', async (req, res) => {
2326 console.log('==============================');
2327 console.log('[Agent] GET /api/agent/version');
2329 // Try database first
2330 const result = await pool.query(
2331 'SELECT version FROM agent_versions WHERE is_latest = TRUE ORDER BY release_date DESC LIMIT 1'
2334 if (result.rows && result.rows.length > 0) {
2335 const version = result.rows[0].version;
2336 console.log('[Agent] Latest version from database:', version);
2337 console.log('==============================');
2338 return res.json({ version });
2341 // Fallback to package.json if database is empty
2342 const fs = require('fs');
2343 const path = require('path');
2344 const packagePath = path.join(__dirname, '../../agent/package.json');
2345 const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
2347 console.log('[Agent] Latest version from package.json:', packageJson.version);
2348 console.log('==============================');
2350 res.json({ version: packageJson.version });
2352 console.error('[Agent] Failed to read agent version:', err);
2353 res.status(500).json({ error: 'Failed to read agent version' });
2358 * @api {get} /agent/download/:version Download Agent Installer
2359 * @apiName DownloadAgentInstaller
2362 * Downloads agent installer executable for specified version. Serves file from
2363 * /opt/apps/IBG_HUB_dist/agent/{version}/agent.exe. Streams file to avoid memory
2364 * issues with large binaries.
2365 * @apiPermission public
2366 * @apiNote No authentication required (download tokens validated separately)
2367 * @apiParam {string} version Agent version (e.g., "1.2.3")
2368 * @apiSuccess {File} agent-{version}.exe Agent installer binary
2369 * @apiError (404) {String} error "Agent binary not found"
2370 * @apiError (500) {String} error "Failed to download agent"
2371 * @apiError (500) {String} error "Failed to stream agent binary"
2372 * @apiExample {curl} Example Request:
2373 * curl https://api.ibghub.com/api/agent/download/1.2.3 \
2374 * --output agent-1.2.3.exe
2377// =============================================
2378// GET /api/agent/download/:version
2379// Downloads the agent executable for the specified version
2380// =============================================
2381router.get('/download/:version', (req, res) => {
2383 const fs = require('fs');
2384 const path = require('path');
2385 const { version } = req.params;
2387 // Path to the version-specific agent binary
2388 const agentPath = `/opt/apps/IBG_HUB_dist/agent/${version}/agent.exe`;
2390 console.log('==============================');
2391 console.log('[Agent] GET /api/agent/download/' + version);
2392 console.log('[Agent] Serving agent from:', agentPath);
2394 // Check if file exists
2395 if (!fs.existsSync(agentPath)) {
2396 console.log('[Agent] Agent binary not found!');
2397 console.log('==============================');
2398 return res.status(404).json({ error: 'Agent binary not found' });
2401 const stat = fs.statSync(agentPath);
2402 console.log('[Agent] File size:', (stat.size / 1024 / 1024).toFixed(2), 'MB');
2403 console.log('[Agent] Sending agent binary...');
2404 console.log('==============================');
2406 // Set headers for download
2407 res.setHeader('Content-Type', 'application/octet-stream');
2408 res.setHeader('Content-Disposition', `attachment; filename="agent-${version}.exe"`);
2409 res.setHeader('Content-Length', stat.size);
2412 const fileStream = fs.createReadStream(agentPath);
2413 fileStream.pipe(res);
2415 fileStream.on('error', (err) => {
2416 console.error('[Agent] Stream error:', err);
2417 if (!res.headersSent) {
2418 res.status(500).json({ error: 'Failed to stream agent binary' });
2423 console.error('[Agent] Failed to download agent:', err);
2424 res.status(500).json({ error: 'Failed to download agent' });
2429 * @api {get} /agent/hash/:version Get Installer Hash
2430 * @apiName GetInstallerHash
2433 * Computes SHA256 hash of agent installer binary for verification. Clients validate
2434 * downloaded installer against this hash to prevent tampering. Reads installer from
2435 * /opt/apps/IBG_HUB_dist/agent/{version}/agent.exe.
2436 * @apiPermission public
2437 * @apiNote No authentication required
2438 * @apiParam {string} version Agent version (e.g., "1.2.3")
2439 * @apiSuccess {string} hash SHA256 hash (64 hex characters)
2440 * @apiError (404) {String} error "Agent binary not found"
2441 * @apiError (500) {String} error "Failed to compute hash"
2442 * @apiExample {curl} Example Request:
2443 * curl https://api.ibghub.com/api/agent/hash/1.2.3
2444 * @apiExample {json} Example Response:
2446 * "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
2450// =============================================
2451// GET /api/agent/hash/:version
2452// Returns the SHA256 hash of the agent binary for verification
2453// =============================================
2454router.get('/hash/:version', (req, res) => {
2456 const crypto = require('crypto');
2457 const fs = require('fs');
2458 const path = require('path');
2459 const { version } = req.params;
2461 // Path to the version-specific agent executable on the server
2462 const agentPath = `/opt/apps/IBG_HUB_dist/agent/${version}/agent.exe`;
2464 console.log('==============================');
2465 console.log('[Agent] GET /api/agent/hash/' + version);
2466 console.log('[Agent] Computing hash for:', agentPath);
2468 // Check if file exists
2469 if (!fs.existsSync(agentPath)) {
2470 console.log('[Agent] Agent binary not found!');
2471 console.log('==============================');
2472 return res.status(404).json({ error: 'Agent binary not found' });
2475 // Compute SHA256 hash
2476 const fileBuffer = fs.readFileSync(agentPath);
2477 const hashSum = crypto.createHash('sha256');
2478 hashSum.update(fileBuffer);
2479 const hash = hashSum.digest('hex');
2481 console.log('[Agent] Computed hash:', hash);
2482 console.log('==============================');
2486 console.error('[Agent] Failed to compute hash:', err);
2487 res.status(500).json({ error: 'Failed to compute hash' });
2492 * @api {post} /agent/:agent_uuid/files/upload Upload File to Agent
2493 * @apiName UploadFileToAgent
2494 * @apiGroup Agent File Transfer
2496 * Initiates file upload to remote agent. Accepts multipart/form-data with file and
2497 * destination path. File stored in uploads/ directory, transfer tracked by transferId.
2498 * Uses fileTransferService for WebSocket coordination with agent.
2499 * @apiPermission authenticated
2500 * @apiUse AuthHeader
2501 * @apiParam {string} agent_uuid Agent UUID
2502 * @apiBody {File} file File to upload (multipart/form-data)
2503 * @apiBody {string} remotePath Destination path on agent filesystem
2504 * @apiSuccess {string} transferId Transfer tracking ID
2505 * @apiSuccess {string} status "started"
2506 * @apiError (500) {String} error Error message from fileTransferService
2507 * @apiExample {curl} Example Request:
2508 * curl -X POST https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files/upload \
2509 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
2510 * -F "file=@config.ini" \
2511 * -F "remotePath=C:\\ProgramData\\config.ini"
2514// -------------------------
2515// File Transfer Routes
2516// -------------------------
2517const fileTransferService = require('../services/fileTransferService');
2518const multer = require('multer');
2519const upload = multer({ dest: 'uploads/' });
2521// POST /api/agent/:agent_uuid/files/upload
2522// Upload file to agent
2523router.post('/:agent_uuid/files/upload', authenticateToken, upload.single('file'), async (req, res) => {
2525 const { agent_uuid } = req.params;
2526 const { remotePath } = req.body;
2527 const localPath = req.file.path;
2528 const user_email = req.user?.email || 'system';
2530 const transferId = await fileTransferService.startUpload(
2537 res.json({ transferId, status: 'started' });
2539 console.error('[Agent] File upload failed:', err);
2540 res.status(500).json({ error: err.message });
2545 * @api {post} /agent/:agent_uuid/files/download Download File from Agent
2546 * @apiName DownloadFileFromAgent
2547 * @apiGroup Agent File Transfer
2549 * Initiates file download from remote agent. Requests file at remotePath, agent
2550 * uploads to server, file tracked by transferId. Uses fileTransferService for
2551 * WebSocket coordination.
2552 * @apiPermission authenticated
2553 * @apiUse AuthHeader
2554 * @apiParam {string} agent_uuid Agent UUID
2555 * @apiBody {string} remotePath File path on agent filesystem to download
2556 * @apiSuccess {string} transferId Transfer tracking ID
2557 * @apiSuccess {string} status "started"
2558 * @apiError (500) {String} error Error message from fileTransferService
2559 * @apiExample {curl} Example Request:
2560 * curl -X POST https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files/download \
2561 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
2562 * -H "Content-Type: application/json" \
2563 * -d '{"remotePath":"C:\\Windows\\System32\\drivers\\etc\\hosts"}'
2566// POST /api/agent/:agent_uuid/files/download
2567// Download file from agent
2568router.post('/:agent_uuid/files/download', authenticateToken, async (req, res) => {
2570 const { agent_uuid } = req.params;
2571 const { remotePath } = req.body;
2572 const user_email = req.user?.email || 'system';
2574 const transferId = await fileTransferService.startDownload(
2580 res.json({ transferId, status: 'started' });
2582 console.error('[Agent] File download failed:', err);
2583 res.status(500).json({ error: err.message });
2588 * @api {get} /agent/:agent_uuid/files/transfer/:transferId Get Transfer Status
2589 * @apiName GetTransferStatus
2590 * @apiGroup Agent File Transfer
2592 * Retrieves current status and progress of file transfer. Returns transfer object
2593 * with status, progress percentage, file size, and error details if failed.
2595 * Transfer statuses:
2596 * - pending: Transfer queued, not started
2597 * - in_progress: Transfer active
2598 * - completed: Transfer finished successfully
2599 * - failed: Transfer error occurred
2600 * - cancelled: Transfer cancelled by user
2601 * @apiPermission authenticated
2602 * @apiUse AuthHeader
2603 * @apiParam {string} agent_uuid Agent UUID
2604 * @apiParam {string} transferId Transfer tracking ID
2605 * @apiSuccess {string} transferId Transfer ID
2606 * @apiSuccess {string} type Transfer type ("upload" or "download")
2607 * @apiSuccess {string} status Transfer status
2608 * @apiSuccess {number} progress Progress percentage (0-100)
2609 * @apiSuccess {string} localPath Server-side file path
2610 * @apiSuccess {string} remotePath Agent-side file path
2611 * @apiSuccess {number} fileSize File size in bytes
2612 * @apiSuccess {string} error Error message (if failed)
2613 * @apiError (404) {String} error "Transfer not found"
2614 * @apiError (500) {String} error Error message
2615 * @apiExample {curl} Example Request:
2616 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files/transfer/abc123 \
2617 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2620// GET /api/agent/:agent_uuid/files/transfer/:transferId
2621// Get transfer status
2622router.get('/:agent_uuid/files/transfer/:transferId', authenticateToken, async (req, res) => {
2624 const { transferId } = req.params;
2625 const transfer = fileTransferService.getTransfer(transferId);
2628 return res.status(404).json({ error: 'Transfer not found' });
2633 console.error('[Agent] Get transfer status failed:', err);
2634 res.status(500).json({ error: err.message });
2639 * @api {get} /agent/:agent_uuid/files/transfer/:transferId/download Download Completed File
2640 * @apiName DownloadCompletedFile
2641 * @apiGroup Agent File Transfer
2643 * Downloads file that was transferred from agent. Only works for completed "download"
2644 * type transfers. Streams file from server-side transfer cache directory.
2645 * @apiPermission authenticated
2646 * @apiUse AuthHeader
2647 * @apiParam {string} agent_uuid Agent UUID
2648 * @apiParam {string} transferId Transfer tracking ID
2649 * @apiSuccess {File} file Transferred file binary
2650 * @apiError (404) {String} error "Transfer not found"
2651 * @apiError (400) {String} error "Transfer not completed"
2652 * @apiError (500) {String} error "Download file failed"
2653 * @apiExample {curl} Example Request:
2654 * curl https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files/transfer/abc123/download \
2655 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
2656 * --output downloaded-file.txt
2659// GET /api/agent/:agent_uuid/files/transfer/:transferId/download
2660// Download completed file
2661router.get('/:agent_uuid/files/transfer/:transferId/download', authenticateToken, async (req, res) => {
2663 const { transferId } = req.params;
2664 const transfer = fileTransferService.getTransfer(transferId);
2666 if (!transfer || transfer.type !== 'download') {
2667 return res.status(404).json({ error: 'Transfer not found' });
2670 if (transfer.status !== 'completed') {
2671 return res.status(400).json({ error: 'Transfer not completed' });
2674 res.download(transfer.localPath);
2676 console.error('[Agent] Download file failed:', err);
2677 res.status(500).json({ error: err.message });
2682 * @api {delete} /agent/:agent_uuid/files/transfer/:transferId Cancel File Transfer
2683 * @apiName CancelFileTransfer
2684 * @apiGroup Agent File Transfer
2686 * Cancels active or pending file transfer. Cleans up transfer state, notifies agent
2687 * via WebSocket, and removes temporary files. Cannot cancel already completed transfers.
2688 * @apiPermission authenticated
2689 * @apiUse AuthHeader
2690 * @apiParam {string} agent_uuid Agent UUID
2691 * @apiParam {string} transferId Transfer tracking ID
2692 * @apiSuccess {boolean} success true
2693 * @apiError (500) {String} error "Cancel transfer failed"
2694 * @apiExample {curl} Example Request:
2695 * curl -X DELETE https://api.ibghub.com/api/agent/550e8400-e29b-41d4-a716-446655440000/files/transfer/abc123 \
2696 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
2699// DELETE /api/agent/:agent_uuid/files/transfer/:transferId
2701router.delete('/:agent_uuid/files/transfer/:transferId', authenticateToken, async (req, res) => {
2703 const { transferId } = req.params;
2704 await fileTransferService.cancelTransfer(transferId);
2705 res.json({ success: true });
2707 console.error('[Agent] Cancel transfer failed:', err);
2708 res.status(500).json({ error: err.message });
2712module.exports = router;