${buttons.map(btn => `
${btn.icon ? `
`).join('')}
${escapeHtml(btn.icon)}
` : ''}
${escapeHtml(btn.label) || 'Button'}
${btn.sublabel ? `${escapeHtml(btn.sublabel)}
` : ''}
const express = require('express'); const router = express.Router(); const { v4: uuidv4 } = require('uuid'); const { db } = require('../db/database'); const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth'); // Phase 2.2e: workspace-aware access. Same pattern as content/widgets/folders. const { accessContext } = require('../lib/tenancy'); // 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 CSS color values to prevent style injection function safeColor(val, fallback) { if (!val) return fallback; if (/^#[0-9a-fA-F]{3,8}$/.test(val) || /^[a-zA-Z]+$/.test(val)) return val; return fallback; } // Validate CSS numeric values function safeNumber(val, fallback) { const n = Number(val); return isFinite(n) ? n : fallback; } // List kiosk pages in the caller's current workspace plus any platform-template // rows (workspace_id IS NULL) shared with all workspaces. // Phase 2.2e: workspace-scoped. Cross-workspace visibility comes from // switch-workspace, not a special list branch. router.get('/', (req, res) => { if (!req.workspaceId) return res.json([]); const pages = db.prepare( 'SELECT * FROM kiosk_pages WHERE (workspace_id = ? OR workspace_id IS NULL) ORDER BY created_at DESC' ).all(req.workspaceId); res.json(pages); }); // Phase 2.2e: workspace-aware access. Mirrors widgets/content helpers. // Platform-template kiosks (workspace_id IS NULL) are readable by anyone // authenticated and writable only by platform_admin. function checkKioskRead(req, res) { const page = db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(req.params.id); if (!page) { res.status(404).json({ error: 'Page not found' }); return null; } if (!page.workspace_id) return page; const ws = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(page.workspace_id); const ctx = ws && accessContext(req.user.id, req.user.role, ws); if (!ctx) { res.status(403).json({ error: 'Access denied' }); return null; } return page; } function checkKioskWrite(req, res) { const page = db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(req.params.id); if (!page) { res.status(404).json({ error: 'Page not found' }); return null; } if (!page.workspace_id) { if (!PLATFORM_ROLES.includes(req.user.role)) { res.status(403).json({ error: 'Platform admin required to modify shared kiosk pages' }); return null; } return page; } const ws = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(page.workspace_id); const ctx = ws && accessContext(req.user.id, req.user.role, ws); if (!ctx) { res.status(403).json({ error: 'Access denied' }); return null; } if (!ctx.actingAs && ctx.workspaceRole === 'workspace_viewer') { res.status(403).json({ error: 'Read-only access' }); return null; } return page; } // Get kiosk page router.get('/:id', (req, res) => { const page = checkKioskRead(req, res); if (!page) return; res.json(page); }); // Render kiosk page (public - accessed by devices) router.get('/:id/render', (req, res) => { const page = db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(req.params.id); if (!page) return res.status(404).send('Page not found'); const config = JSON.parse(page.config || '{}'); const buttons = config.buttons || []; const style = config.style || {}; const html = `
${escapeHtml(config.subtitle)}
` : ''}