* feat(gallery): reveal mode — hide gallery from guests until reveal (#838) Guests can upload during the event but see no photos until the host reveals the gallery, manually ("Reveal now") or at a scheduled time. - migration 165: events.reveal_mode / reveal_at / revealed_at. Effective visibility is computed at REQUEST time (reveal_at <= now opens the gate exactly on schedule); the minutely scheduler only stamps revealed_at durably and emits a gallery.revealed workflow trigger - server-side enforcement in gallery.js: /photos returns the event shell with photos: [] + hidden_until_reveal for plain guests; image/download/stats endpoints 403 with GALLERY_HIDDEN (photo IDs are sequential — listing-only gating would be probeable); feedback-summary gated too. Slideshow tokens (surprise beamer), client access and the admin preview bypass; the guest upload route stays open - admin: reveal toggle + optional scheduled datetime next to the guest upload settings, status line and "Reveal now" button on the overview; re-enabling the toggle clears revealed_at so a gallery can re-hide - guest UI: upload-only view (hero, friendly message, scheduled time, upload button) for every layout; i18n for all 8 locales - timestamps written as ISO strings — the SQLite driver stringifies raw Date objects into garbage; ISO round-trips on both engines - 14 integration tests over minted gallery/slideshow/client/admin tokens * fix(gallery): reveal/re-arm semantics + upload button i18n key (#838) - "Reveal now" also clears a pending reveal_at: the schedule is consumed, so the full-form admin save can't accidentally re-hide a revealed gallery with a stale future date - setting a FUTURE reveal_at on a revealed gallery re-arms hiding — the one intentional way to re-hide without double-toggling the mode - guest upload button uses the existing upload.uploadPhotos key (gallery.uploadPhotos never existed; the button showed EN everywhere) * fix(gallery): close reveal bypasses from review round 1 (#838) - the hero-derivative route and the secure-images token-mint + secure-download routes are now reveal-gated: hero serves a 1920px derivative of ANY sequential photo id and secure tokens fetch originals — both were open bypasses while hidden. blockHiddenGallery moved to utils/revealMode.js and shared - customer-portal tokens (via:'customer', no accessLevel) now bypass reveal mode — they are the host/customer, not a guest, and were getting the upload-only view - an open hidden guest view refetches exactly at reveal_at plus a 60s fallback poll, so the gallery appears without a manual reload - gallery.revealed added to the workflow editor's trigger picker so the advertised notification hook is reachable in the UI - migration 165 guards each column independently (partial-state safe) * fix(gallery): reveal round 2 — remaining bypass surfaces + lifecycle edges (#838) - legacy /api/images router reveal-gated (view, secure-token + signed-url minting), and the signed-URL SERVE path re-checks hidden state via a backward-compatible bypass flag in the token payload - secure-image tokens record revealBypass at mint and are re-validated at serve time — a re-hide kills in-flight guest tokens within the request, while slideshow/client tokens keep working - OG metadata and the unauthenticated /og cover fall back to the brand logo / 404 while hidden — no hero-photo spoiler for social crawlers - photo-feedback GET/POST reveal-gated (sequential ids were enumerable); /my-feedback returns the empty back-compat shape (rows leak filename + storage path) - the reveal scheduler skips drafts — no premature stamp/notification for unpublished galleries - emitWorkflowEvent gains an additive dedupSuffix; both reveal emitters pass the reveal timestamp so a re-hidden gallery's second reveal fires workflows again instead of deduping into silence * fix(gallery): reveal round 3 — schedule consumption + two-way client sync (#838) - the scheduler now consumes reveal_at when stamping (matching "Reveal now"), and re-arming via a partial API update clears a stale PAST schedule — previously {reveal_mode:true} without reveal_at could instantly re-open the gate through the leftover date - /photos exposes reveal_armed so an open VISIBLE gallery keeps a 60s poll while the mode is on — a re-hide now propagates to open clients in both directions, not just hidden→visible Codex round-3 claim about timestamp-without-timezone drift on non-UTC Postgres was verified FALSE: knex's table.timestamp() creates timestamptz on PG (confirmed via information_schema on a live install), which stores absolute instants regardless of server TZ. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
3d6c9848dc
commit
2f05fcc39d
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Reveal mode (#838): effective-visibility math, shared by the gallery
|
||||
* routes, the admin routes and the reveal scheduler.
|
||||
*
|
||||
* The gate is computed from the event row at request time — a scheduled
|
||||
* reveal opens EXACTLY at reveal_at even if the minutely scheduler (which
|
||||
* only stamps revealed_at durably and fires notifications) lags behind.
|
||||
*/
|
||||
|
||||
/** Truthy check that survives SQLite 0/1 and Postgres booleans. */
|
||||
function isTrue(value) {
|
||||
return value === true || value === 1 || value === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a timestamp column that may arrive as a Date (Postgres), an ISO
|
||||
* string, or a millisecond number/number-string (SQLite stores knex Dates
|
||||
* as ms — `new Date("178…")` on that string would be Invalid Date and
|
||||
* silently keep the gallery hidden past its scheduled reveal).
|
||||
*/
|
||||
function toDate(value) {
|
||||
if (value instanceof Date) return value;
|
||||
if (typeof value === 'number') return new Date(value);
|
||||
const asNumber = Number(value);
|
||||
if (!Number.isNaN(asNumber) && String(value).trim() !== '') return new Date(asNumber);
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the gallery is currently hidden from plain guests.
|
||||
* Host/admin/slideshow/client access bypasses this at the route layer.
|
||||
*/
|
||||
function isGalleryHidden(event, now = new Date()) {
|
||||
if (!isTrue(event.reveal_mode)) return false;
|
||||
if (event.revealed_at) return false;
|
||||
if (event.reveal_at && toDate(event.reveal_at) <= now) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which access levels see the full gallery while it is hidden:
|
||||
* the live slideshow (the "surprise beamer" case), client access and
|
||||
* customer-portal-minted tokens (both are the host/customer reviewing
|
||||
* their own event — those tokens carry via:'customer' with NO accessLevel,
|
||||
* so accessLevel alone would misclassify them as guests) and the admin
|
||||
* preview.
|
||||
*/
|
||||
function bypassesReveal(req) {
|
||||
if (req.accessLevel === 'slideshow' || req.accessLevel === 'client') return true;
|
||||
if (req.viaCustomer) return true;
|
||||
// Lazy require avoids a cycle: middleware/gallery requires nothing from
|
||||
// here, but keeping the import local makes that permanent.
|
||||
const { isAdminPreview } = require('../middleware/gallery');
|
||||
return Boolean(isAdminPreview(req));
|
||||
}
|
||||
|
||||
/** Route guard result: is THIS request blocked by reveal mode? */
|
||||
function guestBlockedByReveal(req, now = new Date()) {
|
||||
return isGalleryHidden(req.event, now) && !bypassesReveal(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard: hard 403 for plain guests on photo/derivative/download
|
||||
* endpoints while the gallery is hidden. Photo IDs are sequential, so
|
||||
* gating only the listing would leave images probeable. Mount AFTER
|
||||
* verifyGalleryAccess (needs req.event / req.accessLevel).
|
||||
*/
|
||||
function blockHiddenGallery(req, res, next) {
|
||||
if (guestBlockedByReveal(req)) {
|
||||
return res.status(403).json({ error: 'Gallery is hidden until reveal', code: 'GALLERY_HIDDEN' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { isGalleryHidden, bypassesReveal, guestBlockedByReveal, blockHiddenGallery };
|
||||
Reference in New Issue
Block a user