Files
UNITRONIX c2aedb10fe Enhance security and input validation across CDAP and organization routes
- Implemented input validation for `orgId` and `deviceId` in CDAP and organization detail routes using `assertSafeApiId`, returning a 400 error for invalid inputs.
- Added HTML escaping for `deviceId` and `orgId` in views to prevent XSS vulnerabilities.
- Hardened `patch-role-scope-i18n.js` against prototype pollution with a guard for unsafe nested keys.
- Updated CodeQL configuration to include new exclusions and ensure documented exclusions are applied.
2026-07-09 21:16:55 +02:00

226 lines
7.1 KiB
JavaScript

/**
* BetterDesk Console - CDAP Routes
* Routes for CDAP (Custom Device Automation Protocol) device management
* and widget rendering in the admin panel.
*/
const express = require('express');
const router = express.Router();
const { assertSafeApiId } = require('../lib/goApiPath');
const { requireAuth, requirePermission } = require('../middleware/auth');
const betterdeskApi = require('../services/betterdeskApi');
// ── Page Routes ──────────────────────────────────────────────────────────
/**
* CDAP devices list page
* GET /cdap
*/
router.get('/cdap', requireAuth, (req, res) => {
res.render('cdap-devices', {
title: req.t('cdap.devices_title'),
activePage: 'cdap',
currentPage: 'cdap'
});
});
/**
* CDAP devices list page (alternative path, used by desktop embed)
* GET /cdap/devices
*/
router.get('/cdap/devices', requireAuth, (req, res) => {
res.render('cdap-devices', {
title: req.t('cdap.devices_title'),
activePage: 'cdap',
currentPage: 'cdap'
});
});
/**
* CDAP device detail page with widget panel
* GET /cdap/devices/:id
*/
router.get('/cdap/devices/:id', requireAuth, (req, res) => {
try {
const deviceId = assertSafeApiId(req.params.id, 'deviceId');
res.render('cdap-device', {
title: req.t('cdap.device_detail'),
activePage: 'devices',
deviceId,
});
} catch (err) {
if (err.message && /^Invalid /.test(err.message)) {
return res.status(400).render('errors/404', {
title: 'Bad Request',
activePage: 'error',
});
}
throw err;
}
});
// ── API Routes ───────────────────────────────────────────────────────────
// Unwrap the { success, data } envelope returned by betterdeskApi helpers
// so that the CDAP frontend receives the flat Go server response it expects.
function unwrap(result) {
return (result && result.success && result.data) ? result.data : result;
}
/**
* GET /api/cdap/status
* Returns CDAP gateway status (enabled, connections, port)
*/
router.get('/api/cdap/status', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPStatus();
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ enabled: false, error: 'Failed to get CDAP status' });
}
});
/**
* GET /api/cdap/devices
* Returns all connected CDAP devices
*/
router.get('/api/cdap/devices', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPDevices();
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ devices: [], error: 'Failed to list CDAP devices' });
}
});
/**
* GET /api/cdap/devices/:id
* Returns full CDAP device info (manifest + state + connection)
*/
router.get('/api/cdap/devices/:id', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPDeviceInfo(req.params.id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to get CDAP device info' });
}
});
/**
* GET /api/cdap/devices/:id/manifest
* Returns device manifest (capabilities, widgets, alerts)
*/
router.get('/api/cdap/devices/:id/manifest', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPDeviceManifest(req.params.id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to get CDAP device manifest' });
}
});
/**
* GET /api/cdap/devices/:id/state
* Returns current widget values for connected device
*/
router.get('/api/cdap/devices/:id/state', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPDeviceState(req.params.id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to get CDAP device state' });
}
});
/**
* POST /api/cdap/devices/:id/command
* Sends a command to a connected CDAP device
* Body: { widget_id, action, value, reason? }
*/
router.post('/api/cdap/devices/:id/command', requireAuth, requirePermission('cdap.command'), async (req, res) => {
try {
const { widget_id, action, value, reason } = req.body;
if (!widget_id || !action) {
return res.status(400).json({ error: 'widget_id and action are required' });
}
const result = await betterdeskApi.sendCDAPCommand(
req.params.id,
widget_id,
action,
value,
reason
);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to send command' });
}
});
/**
* POST /api/cdap/toggle
* Enable or disable CDAP gateway (saves config; requires server restart)
* Body: { enabled: true|false }
*/
router.post('/api/cdap/toggle', requireAuth, requirePermission('server.config'), async (req, res) => {
try {
const { enabled } = req.body;
if (typeof enabled !== 'boolean') {
return res.status(400).json({ success: false, error: 'enabled must be a boolean' });
}
await betterdeskApi.setConfig('cdap_enabled', enabled ? 'Y' : 'N');
res.json({ success: true, enabled, restart_required: true });
} catch (err) {
console.error('[CDAP] Toggle error:', err.message);
res.status(500).json({ success: false, error: 'Failed to toggle CDAP' });
}
});
/**
* GET /api/cdap/alerts
* Returns all currently firing CDAP alerts
* Query: ?device_id=optional (filter by device)
*/
router.get('/api/cdap/alerts', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getCDAPAlerts(req.query.device_id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ alerts: [], total: 0, error: 'Failed to get CDAP alerts' });
}
});
/**
* GET /api/cdap/devices/:id/linked
* Returns all peers linked to this CDAP device
*/
router.get('/api/cdap/devices/:id/linked', requireAuth, async (req, res) => {
try {
const result = await betterdeskApi.getLinkedPeers(req.params.id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to get linked devices' });
}
});
/**
* POST /api/cdap/devices/:id/link
* Link or unlink a peer to this CDAP device
* Body: { linked_peer_id: "PEERID" } or { linked_peer_id: "" } to unlink
*/
router.post('/api/cdap/devices/:id/link', requireAuth, requirePermission('cdap.command'), async (req, res) => {
try {
const { linked_peer_id } = req.body;
if (linked_peer_id === undefined) {
return res.status(400).json({ error: 'linked_peer_id is required' });
}
const result = await betterdeskApi.linkDevice(req.params.id, linked_peer_id);
res.json(unwrap(result));
} catch (err) {
res.status(500).json({ error: 'Failed to link device' });
}
});
module.exports = router;