From 759784a4d1cfe7e67c825293760169ad6904f090 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:59:46 +0200 Subject: [PATCH] fix(slideshow): feature flag is a master kill-switch, not just admin UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disabling the `slideshow` feature previously only hid the admin UI — the public /show/:token route ignored the flag, so already-minted links kept working. Gate resolveSlideshow on isFeatureEnabled('slideshow') so every /session and /state 404s when the feature is off: clicking Start shows "link not active" and a running projector stops within one /state poll. Belt-and-braces: also gate the admin generate + settings PATCH endpoints with requireFeatureFlag so links can't be minted/changed while off (disable stays open so stale tokens can be cleared). --- backend/src/routes/adminEvents.js | 5 +++-- backend/src/routes/gallery.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 97c0faf2..13557a3f 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -25,6 +25,7 @@ const { normaliseEventTimeTriple } = require('../services/eventService'); const { hasColumnCached } = require('../utils/schemaCache'); const { validateFileType } = require('../utils/fileSecurityUtils'); const { requireEventOwnership } = require('../middleware/ownership'); +const { requireFeatureFlag } = require('../middleware/requireFeatureFlag'); const { getFrontendBaseUrl } = require('../utils/frontendUrl'); const downloadZipService = require('../services/downloadZipService'); @@ -1929,7 +1930,7 @@ async function loadOwnedEvent(req) { // Generate (or rotate) the slideshow share token. Idempotent in intent: each // call mints a fresh token, which both "Generate" (first time) and "Regenerate" // (rotate, kills the old link) use. -router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => { +router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, async (req, res) => { try { const event = await loadOwnedEvent(req); if (!event) { @@ -1988,7 +1989,7 @@ router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit' // Update the LIVE slideshow settings (display time / transition style / speed). // A running projector picks these up via the show-page settings poll within a // few seconds — no need to regenerate the link. -router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requireEventOwnership, [ +router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, [ body('show_interval_ms').optional().isInt({ min: 1000, max: 120000 }), body('show_transition').optional().isIn(SLIDESHOW_TRANSITIONS), body('show_transition_ms').optional().isInt({ min: 100, max: 5000 }), diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 2d198270..f2b7226f 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -29,6 +29,7 @@ const { setGalleryAuthCookies } = require('../utils/tokenUtils'); // Read globals from app_settings (the real table) — settingsService.getSetting // queries a non-existent `settings` table and throws. const { getAppSetting } = require('../utils/appSettings'); +const { isFeatureEnabled } = require('../middleware/requireFeatureFlag'); const fs = require('fs'); // Get storage path from environment or default @@ -245,6 +246,10 @@ function slideshowPhotosQuery(eventId) { // dead link reveals nothing and stops any projector on its next poll. async function resolveSlideshow(slug, token) { if (!token) return null; + // The `slideshow` feature flag is a master kill-switch: when an admin turns + // Live Slideshow off, every existing /show/ link dies on its next request + // (the running projector stops within one /state poll), not just the admin UI. + if (!(await isFeatureEnabled('slideshow'))) return null; const event = await db('events') .where({ slug,