const express = require('express'); const router = express.Router(); const { v4: uuidv4 } = require('uuid'); const { db } = require('../db/database'); // Escape HTML to prevent XSS function escapeHtml(str) { if (typeof str !== 'string') return str; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } // Validate timezone format (e.g. America/New_York, UTC, Etc/GMT+5) function safeTimezone(tz) { if (!tz) return 'UTC'; return /^[A-Za-z_\-\/+0-9]+$/.test(tz) ? tz : 'UTC'; } // Validate ISO date string format function safeDateString(d) { if (!d) return ''; return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?/.test(d) ? d : ''; } // Validate URL is http/https function safeUrl(url) { if (!url) return 'about:blank'; try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol) ? url : 'about:blank'; } catch { return 'about:blank'; } } // List widgets router.get('/', (req, res) => { const isAdmin = req.user.role === 'superadmin'; const widgets = db.prepare( `SELECT * FROM widgets ${isAdmin ? '' : 'WHERE user_id = ? OR user_id IS NULL'} ORDER BY created_at DESC` ).all(...(isAdmin ? [] : [req.user.id])); res.json(widgets); }); // Create widget router.post('/', (req, res) => { const { widget_type, name, config } = req.body; if (!widget_type || !name) return res.status(400).json({ error: 'widget_type and name required' }); const id = uuidv4(); db.prepare('INSERT INTO widgets (id, user_id, widget_type, name, config) VALUES (?, ?, ?, ?, ?)') .run(id, req.user.id, widget_type, name, JSON.stringify(config || {})); res.status(201).json(db.prepare('SELECT * FROM widgets WHERE id = ?').get(id)); }); // Helper: check widget ownership function checkWidgetAccess(req, res) { const widget = db.prepare('SELECT * FROM widgets WHERE id = ?').get(req.params.id); if (!widget) { res.status(404).json({ error: 'Widget not found' }); return null; } // Allow access if: admin, owner, no owner (public), or render route (no req.user) if (req.user && !['admin','superadmin'].includes(req.user.role) && widget.user_id && widget.user_id !== req.user.id) { res.status(403).json({ error: 'Access denied' }); return null; } return widget; } // Get widget router.get('/:id', (req, res) => { const widget = checkWidgetAccess(req, res); if (!widget) return; res.json(widget); }); // Update widget router.put('/:id', (req, res) => { const widget = checkWidgetAccess(req, res); if (!widget) return; const { name, config } = req.body; if (name) db.prepare('UPDATE widgets SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id); if (config) db.prepare('UPDATE widgets SET config = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(JSON.stringify(config), req.params.id); res.json(db.prepare('SELECT * FROM widgets WHERE id = ?').get(req.params.id)); }); // Delete widget router.delete('/:id', (req, res) => { const widget = checkWidgetAccess(req, res); if (!widget) return; db.prepare('DELETE FROM widgets WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // Render widget as HTML page router.get('/:id/render', (req, res) => { const widget = db.prepare('SELECT * FROM widgets WHERE id = ?').get(req.params.id); if (!widget) return res.status(404).send('Widget not found'); const config = JSON.parse(widget.config || '{}'); let html = ''; switch (widget.widget_type) { case 'clock': html = renderClock(config); break; case 'weather': html = renderWeather(config); break; case 'rss': html = renderRSS(config); break; case 'text': html = renderText(config); break; case 'webpage': html = renderWebpage(config); break; case 'social': html = renderSocial(config); break; default: html = '

Unknown widget

'; } res.setHeader('Content-Type', 'text/html'); res.send(html); }); function renderClock(c) { return `
${c.show_date !== false ? '
' : ''} `; } function renderWeather(c) { return `
--
${escapeHtml(c.location) || 'Unknown'}
`; } function renderRSS(c) { return `
Loading feed...
`; } function renderText(c) { // Designer preview uses fontSize/10 vw, but older published HTML used fontSize*10.8 px. // Convert any px-based font sizes to vw so they scale to any viewport: px / 108 = vw let html = c.html || '

Empty text widget

'; html = html.replace(/font-size:\s*([\d.]+)px/g, (match, px) => { return `font-size:${(parseFloat(px) / 108).toFixed(2)}vw`; }); return `${html}`; // NOTE: c.html is intentionally rendered as raw HTML - this is user-authored content for the text widget } function renderWebpage(c) { const zoom = (c.zoom || 100) / 100; const invZoom = 100 / (c.zoom || 100) * 100; return ` ${c.refresh_interval > 0 ? `` : ''} `; } function renderSocial(c) { return `

Social Feed

${escapeHtml(c.platform) || 'twitter'}: ${escapeHtml(c.query) || ''}

Configure API key in widget settings

`; } module.exports = router;