Files
picpeak/backend/src/utils/revealMode.js
T
Paul Nothaft 839bf4e464 fix(security): close four middleware gaps around the API edge
- 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
2026-09-03 10:51:53 +02:00

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 };