mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Phase 2.2e: kiosk.js scoped to workspace_id; import kiosk INSERT bundled
This commit is contained in:
parent
efce13e05d
commit
806c931e43
|
|
@ -3,6 +3,8 @@ const router = express.Router();
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { db } = require('../db/database');
|
const { db } = require('../db/database');
|
||||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
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
|
// Escape HTML to prevent XSS
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
|
|
@ -23,28 +25,52 @@ function safeNumber(val, fallback) {
|
||||||
return isFinite(n) ? n : fallback;
|
return isFinite(n) ? n : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List kiosk pages
|
// 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) => {
|
router.get('/', (req, res) => {
|
||||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
if (!req.workspaceId) return res.json([]);
|
||||||
const pages = db.prepare(
|
const pages = db.prepare(
|
||||||
`SELECT * FROM kiosk_pages ${isAdmin ? '' : 'WHERE user_id = ?'} ORDER BY created_at DESC`
|
'SELECT * FROM kiosk_pages WHERE (workspace_id = ? OR workspace_id IS NULL) ORDER BY created_at DESC'
|
||||||
).all(...(isAdmin ? [] : [req.user.id]));
|
).all(req.workspaceId);
|
||||||
res.json(pages);
|
res.json(pages);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper: check kiosk ownership
|
// Phase 2.2e: workspace-aware access. Mirrors widgets/content helpers.
|
||||||
function checkKioskAccess(req, res) {
|
// 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);
|
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) { res.status(404).json({ error: 'Page not found' }); return null; }
|
||||||
if (req.user && !ELEVATED_ROLES.includes(req.user.role) && page.user_id !== req.user.id) {
|
if (!page.workspace_id) return page;
|
||||||
res.status(403).json({ error: 'Access denied' }); return null;
|
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;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get kiosk page
|
// Get kiosk page
|
||||||
router.get('/:id', (req, res) => {
|
router.get('/:id', (req, res) => {
|
||||||
const page = checkKioskAccess(req, res);
|
const page = checkKioskRead(req, res);
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
res.json(page);
|
res.json(page);
|
||||||
});
|
});
|
||||||
|
|
@ -158,21 +184,22 @@ router.get('/:id/render', (req, res) => {
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create kiosk page
|
// Create kiosk page in the caller's current workspace.
|
||||||
router.post('/', (req, res) => {
|
router.post('/', (req, res) => {
|
||||||
|
if (!req.workspaceId) return res.status(403).json({ error: 'No workspace context. Switch to a workspace before creating kiosk pages.' });
|
||||||
const { name, config: pageConfig } = req.body;
|
const { name, config: pageConfig } = req.body;
|
||||||
if (!name) return res.status(400).json({ error: 'name required' });
|
if (!name) return res.status(400).json({ error: 'name required' });
|
||||||
|
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
db.prepare('INSERT INTO kiosk_pages (id, user_id, name, config) VALUES (?, ?, ?, ?)')
|
db.prepare('INSERT INTO kiosk_pages (id, user_id, workspace_id, name, config) VALUES (?, ?, ?, ?, ?)')
|
||||||
.run(id, req.user.id, name, JSON.stringify(pageConfig || getDefaultKioskConfig()));
|
.run(id, req.user.id, req.workspaceId, name, JSON.stringify(pageConfig || getDefaultKioskConfig()));
|
||||||
|
|
||||||
res.status(201).json(db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(id));
|
res.status(201).json(db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update kiosk page
|
// Update kiosk page
|
||||||
router.put('/:id', (req, res) => {
|
router.put('/:id', (req, res) => {
|
||||||
const page = checkKioskAccess(req, res);
|
const page = checkKioskWrite(req, res);
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
|
|
||||||
const { name, config: pageConfig } = req.body;
|
const { name, config: pageConfig } = req.body;
|
||||||
|
|
@ -185,7 +212,7 @@ router.put('/:id', (req, res) => {
|
||||||
|
|
||||||
// Delete kiosk page
|
// Delete kiosk page
|
||||||
router.delete('/:id', (req, res) => {
|
router.delete('/:id', (req, res) => {
|
||||||
const page = checkKioskAccess(req, res);
|
const page = checkKioskWrite(req, res);
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
db.prepare('DELETE FROM kiosk_pages WHERE id = ?').run(req.params.id);
|
db.prepare('DELETE FROM kiosk_pages WHERE id = ?').run(req.params.id);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,7 @@ router.post('/import', importUpload.single('file'), async (req, res) => {
|
||||||
const newId = uuid.v4();
|
const newId = uuid.v4();
|
||||||
idMap.kiosk[k.id] = newId;
|
idMap.kiosk[k.id] = newId;
|
||||||
const config = typeof k.config === 'string' ? k.config : JSON.stringify(k.config || {});
|
const config = typeof k.config === 'string' ? k.config : JSON.stringify(k.config || {});
|
||||||
db.prepare(`INSERT INTO kiosk_pages (id, user_id, name, config, created_at) VALUES (?, ?, ?, ?, ?)`).run(newId, userId, k.name, config, k.created_at || Math.floor(Date.now() / 1000));
|
db.prepare(`INSERT INTO kiosk_pages (id, user_id, workspace_id, name, config, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(newId, userId, workspaceId, k.name, config, k.created_at || Math.floor(Date.now() / 1000));
|
||||||
stats.kiosk_pages++;
|
stats.kiosk_pages++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue