diff --git a/frontend/js/views/dashboard.js b/frontend/js/views/dashboard.js index 7f3b9b3..f5e94ee 100644 --- a/frontend/js/views/dashboard.js +++ b/frontend/js/views/dashboard.js @@ -2,6 +2,11 @@ import { api } from '../api.js'; import { on, off, requestScreenshot } from '../socket.js'; import { showToast } from '../components/toast.js'; +function esc(str) { + if (!str) return ''; + return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); +} + const DESTRUCTIVE_COMMANDS = ['reboot', 'shutdown']; const GROUP_COMMANDS = [ { type: 'screen_on', label: 'Screen On' }, @@ -61,12 +66,12 @@ function renderDeviceCard(device) { ` : ''}
-
${device.name}
+
${esc(device.name)}
${device.owner_name || device.owner_email ? `
- ${device.owner_name || device.owner_email} + ${esc(device.owner_name || device.owner_email)}
` : ''}
@@ -108,14 +113,14 @@ function renderGroupSection(group, devices) { const onlineCount = devices.filter(d => d.status === 'online').length; return `
-
+
- ${group.name} + ${esc(group.name)} ${devices.length} device${devices.length !== 1 ? 's' : ''} · ${onlineCount} online
${devices.length > 0 ? ` - ${GROUP_COMMANDS.map(c => ``).join('')} @@ -412,7 +417,7 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000'; modal.innerHTML = `
-

${group.name}

+

${esc(group.name)}

Check devices to add them to this group

${allDevices.filter(d => d.status !== 'provisioning').map(d => { @@ -421,8 +426,8 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { `; }).join('')} diff --git a/server/routes/device-groups.js b/server/routes/device-groups.js index 654e844..95a6cb2 100644 --- a/server/routes/device-groups.js +++ b/server/routes/device-groups.js @@ -3,6 +3,17 @@ const router = express.Router(); const { v4: uuidv4 } = require('uuid'); const { db } = require('../db/database'); +const VALID_COLOR = /^#[0-9A-Fa-f]{6}$/; +const ALLOWED_COMMANDS = ['screen_on', 'screen_off', 'launch', 'update', 'reboot', 'shutdown']; + +// Verify group belongs to the authenticated user +function requireGroupOwnership(req, res, next) { + const group = db.prepare('SELECT * FROM device_groups WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id); + if (!group) return res.status(404).json({ error: 'group not found' }); + req.group = group; + next(); +} + // List groups router.get('/', (req, res) => { const groups = db.prepare(` @@ -20,6 +31,7 @@ router.get('/', (req, res) => { router.post('/', (req, res) => { const { name, color } = req.body; if (!name) return res.status(400).json({ error: 'name required' }); + if (color && !VALID_COLOR.test(color)) return res.status(400).json({ error: 'invalid color format, use #RRGGBB' }); const id = uuidv4(); db.prepare('INSERT INTO device_groups (id, user_id, name, color) VALUES (?, ?, ?, ?)') .run(id, req.user.id, name, color || '#3B82F6'); @@ -27,21 +39,22 @@ router.post('/', (req, res) => { }); // Update group -router.put('/:id', (req, res) => { +router.put('/:id', requireGroupOwnership, (req, res) => { const { name, color } = req.body; - if (name) db.prepare('UPDATE device_groups SET name = ? WHERE id = ? AND user_id = ?').run(name, req.params.id, req.user.id); - if (color) db.prepare('UPDATE device_groups SET color = ? WHERE id = ? AND user_id = ?').run(color, req.params.id, req.user.id); + if (color && !VALID_COLOR.test(color)) return res.status(400).json({ error: 'invalid color format, use #RRGGBB' }); + if (name) db.prepare('UPDATE device_groups SET name = ? WHERE id = ?').run(name, req.params.id); + if (color) db.prepare('UPDATE device_groups SET color = ? WHERE id = ?').run(color, req.params.id); res.json(db.prepare('SELECT * FROM device_groups WHERE id = ?').get(req.params.id)); }); // Delete group -router.delete('/:id', (req, res) => { - db.prepare('DELETE FROM device_groups WHERE id = ? AND user_id = ?').run(req.params.id, req.user.id); +router.delete('/:id', requireGroupOwnership, (req, res) => { + db.prepare('DELETE FROM device_groups WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // Get devices in a group -router.get('/:id/devices', (req, res) => { +router.get('/:id/devices', requireGroupOwnership, (req, res) => { const devices = db.prepare(` SELECT d.* FROM devices d JOIN device_group_members dgm ON d.id = dgm.device_id @@ -52,7 +65,7 @@ router.get('/:id/devices', (req, res) => { }); // Add device to group -router.post('/:id/devices', (req, res) => { +router.post('/:id/devices', requireGroupOwnership, (req, res) => { const { device_id } = req.body; if (!device_id) return res.status(400).json({ error: 'device_id required' }); try { @@ -64,13 +77,13 @@ router.post('/:id/devices', (req, res) => { }); // Remove device from group -router.delete('/:id/devices/:deviceId', (req, res) => { +router.delete('/:id/devices/:deviceId', requireGroupOwnership, (req, res) => { db.prepare('DELETE FROM device_group_members WHERE device_id = ? AND group_id = ?').run(req.params.deviceId, req.params.id); res.json({ success: true }); }); // Bulk assign content to all devices in a group -router.post('/:id/assign-content', (req, res) => { +router.post('/:id/assign-content', requireGroupOwnership, (req, res) => { const { content_id, duration_sec } = req.body; if (!content_id) return res.status(400).json({ error: 'content_id required' }); @@ -86,13 +99,10 @@ router.post('/:id/assign-content', (req, res) => { }); // Send command to all devices in a group -router.post('/:id/command', (req, res) => { +router.post('/:id/command', requireGroupOwnership, (req, res) => { const { type, payload } = req.body; if (!type) return res.status(400).json({ error: 'command type required' }); - - // Verify group belongs to user - const group = db.prepare('SELECT * FROM device_groups WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id); - if (!group) return res.status(404).json({ error: 'group not found' }); + if (!ALLOWED_COMMANDS.includes(type)) return res.status(400).json({ error: 'invalid command type' }); const devices = db.prepare(` SELECT d.id, d.name, d.status FROM devices d @@ -115,7 +125,7 @@ router.post('/:id/command', (req, res) => { const sent = results.filter(r => r.status === 'sent').length; const offline = results.filter(r => r.status === 'offline').length; - console.log(`Group command '${type}' sent to group '${group.name}': ${sent} sent, ${offline} offline`); + console.log(`Group command '${type}' sent to group '${req.group.name}': ${sent} sent, ${offline} offline`); res.json({ success: true, sent, offline, total: devices.length, results }); }); diff --git a/server/server.js b/server/server.js index e11f689..2577400 100644 --- a/server/server.js +++ b/server/server.js @@ -13,7 +13,7 @@ const config = require('./config'); }); const app = express(); -app.set('trust proxy', true); +app.set('trust proxy', 1); // Determine if SSL certs are available const hasSsl = fs.existsSync(config.sslCert) && fs.existsSync(config.sslKey);