- maintenance mode classified paths case-sensitively while Express routes case-insensitively, so /API/... walked past the gate - the general rate limiter skipped anyone holding any verified JWT; a gallery token is minted for free on password-less galleries and slideshow links, so that was an unlimited budget for every /api route. Only admin sessions skip now - ?admin_preview=1 trusted a verified signature alone; it now applies the same revocation, restore-cutoff, deactivation and password-change checks adminAuth does, and reveal-mode reads the verified flag instead of re-decoding the token - the 50mb JSON limit is scoped to /api/admin and /api/v1; everything else gets 2mb, so an unauthenticated body can no longer stall JSON.parse - the CSRF Content-Type gate accepted multipart from any origin; cross-site form posts are now rejected via Sec-Fetch-Site / Origin, with a Host match fallback for same-origin installs that leave FRONTEND_URL unset
77 lines
3.0 KiB
JavaScript
77 lines
3.0 KiB
JavaScript
/**
|
|
* 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;
|
|
// req.isAdminPreview is set by verifyAdminPreview() only after the full
|
|
// session check (revocation, deactivation, password change). Re-decoding
|
|
// the token here would re-grant the bypass to a session that check just
|
|
// rejected.
|
|
return req.isAdminPreview === true;
|
|
}
|
|
|
|
/** 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 };
|