mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
fix(api): expose zone_id + layout_id on the public write paths
- playlists: accept zone_id on item create + update, validated against a template or a layout in the playlist's workspace (no cross-tenant zone reference). - devices: accept layout_id on PUT /api/devices/:id (symmetry with the layouts route), validated the same way; null clears it. Both are already returned in the GET SELECTs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fab4ae909a
commit
dce0d22763
|
|
@ -146,7 +146,7 @@ router.put('/:id', (req, res) => {
|
|||
const device = checkDeviceOwnership(req, res);
|
||||
if (!device) return;
|
||||
|
||||
const { name, notes, timezone, orientation, default_content_id } = req.body;
|
||||
const { name, notes, timezone, orientation, default_content_id, layout_id } = req.body;
|
||||
// Whitelist allowed fields to prevent SQL injection via field names
|
||||
const ALLOWED_FIELDS = ['name', 'notes', 'timezone', 'orientation', 'default_content_id'];
|
||||
const updates = [];
|
||||
|
|
@ -157,6 +157,16 @@ router.put('/:id', (req, res) => {
|
|||
values.push(val);
|
||||
}
|
||||
});
|
||||
// #public-api: allow setting the device's layout here too (symmetry with
|
||||
// PUT /api/layouts/device/:id). Validate it's a template or in the device's
|
||||
// workspace; null clears it (fullscreen).
|
||||
if (layout_id !== undefined) {
|
||||
if (layout_id !== null) {
|
||||
const layout = db.prepare('SELECT id FROM layouts WHERE id = ? AND (is_template = 1 OR workspace_id = ?)').get(layout_id, device.workspace_id);
|
||||
if (!layout) return res.status(400).json({ error: 'layout_id not found in this workspace' });
|
||||
}
|
||||
updates.push('layout_id = ?'); values.push(layout_id || null);
|
||||
}
|
||||
if (updates.length > 0) {
|
||||
values.push(req.params.id);
|
||||
db.prepare(`UPDATE devices SET ${updates.join(', ')}, updated_at = strftime('%s','now') WHERE id = ?`).run(...values);
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ router.put('/:id/items/:itemId/schedules', requirePlaylistWrite, (req, res) => {
|
|||
// playlist's workspace (or be a platform-template).
|
||||
router.post('/:id/items', requirePlaylistWrite, async (req, res) => {
|
||||
try {
|
||||
const { content_id, widget_id, sort_order } = req.body;
|
||||
const { content_id, widget_id, sort_order, zone_id } = req.body;
|
||||
let { duration_sec } = req.body;
|
||||
|
||||
if (!content_id && !widget_id) return res.status(400).json({ error: 'content_id or widget_id required' });
|
||||
|
|
@ -380,6 +380,13 @@ router.post('/:id/items', requirePlaylistWrite, async (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
// #public-api: optional multi-zone placement. Validate the zone belongs to a
|
||||
// template or a layout in this playlist's workspace (the agency portal needs this).
|
||||
if (zone_id) {
|
||||
const zone = db.prepare('SELECT lz.id FROM layout_zones lz JOIN layouts l ON l.id = lz.layout_id WHERE lz.id = ? AND (l.is_template = 1 OR l.workspace_id = ?)').get(zone_id, req.playlist.workspace_id);
|
||||
if (!zone) return res.status(400).json({ error: 'zone_id not found in this workspace' });
|
||||
}
|
||||
|
||||
// Auto-increment sort_order if not specified
|
||||
let order = sort_order;
|
||||
if (order === undefined || order === null) {
|
||||
|
|
@ -389,9 +396,9 @@ router.post('/:id/items', requirePlaylistWrite, async (req, res) => {
|
|||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO playlist_items (playlist_id, content_id, widget_id, sort_order, duration_sec)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(req.params.id, content_id || null, widget_id || null, order, duration_sec);
|
||||
INSERT INTO playlist_items (playlist_id, content_id, widget_id, zone_id, sort_order, duration_sec)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(req.params.id, content_id || null, widget_id || null, zone_id || null, order, duration_sec);
|
||||
|
||||
// Mark as draft (items changed since last publish)
|
||||
markDraft(req.params.id);
|
||||
|
|
@ -421,11 +428,19 @@ router.put('/:id/items/:itemId', requirePlaylistWrite, (req, res) => {
|
|||
.get(req.params.itemId, req.params.id);
|
||||
if (!item) return res.status(404).json({ error: 'item not found' });
|
||||
|
||||
const { sort_order, duration_sec } = req.body;
|
||||
const { sort_order, duration_sec, zone_id } = req.body;
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (sort_order !== undefined) { updates.push('sort_order = ?'); values.push(sort_order); }
|
||||
// #public-api: multi-zone placement (zone_id null clears it). Undefined = no change.
|
||||
if (zone_id !== undefined) {
|
||||
if (zone_id !== null) {
|
||||
const zone = db.prepare('SELECT lz.id FROM layout_zones lz JOIN layouts l ON l.id = lz.layout_id WHERE lz.id = ? AND (l.is_template = 1 OR l.workspace_id = ?)').get(zone_id, req.playlist.workspace_id);
|
||||
if (!zone) return res.status(400).json({ error: 'zone_id not found in this workspace' });
|
||||
}
|
||||
updates.push('zone_id = ?'); values.push(zone_id || null);
|
||||
}
|
||||
if (duration_sec !== undefined) {
|
||||
if (typeof duration_sec !== 'number' || duration_sec < 1) {
|
||||
return res.status(400).json({ error: 'duration_sec must be a positive integer' });
|
||||
|
|
|
|||
Loading…
Reference in a new issue