Files
picpeak/backend/migrations
Paul Nothaft e8dad4b40d feat(slideshow): guest-scannable share-link QR overlay (#848)
* feat(slideshow): guest-scannable share-link QR overlay (#837)

- Global settings (Settings → Slideshow): slideshow_qr_enabled/position/
  opacity/size — same option shape and cascade as the watermark.
- Per-event tri-state show_qr (migration 163): NULL inherits the global,
  true/false force on/off; editable in the per-event slideshow card.
- State endpoint ships the QR as a PNG data URI (cached per share URL —
  the 3s projector poll never re-encodes), so the kiosk needs no QR lib
  and no extra authenticated request.
- Kiosk renders the QR in a white padded corner box so it stays
  scannable on any photo.
- i18n: en + de (the slideshow namespace has no other locales yet).

* fix(slideshow): persist per-event QR override, show QR on empty shows, bound the QR cache (codex review of #848)

- OverviewTab never passed event.show_qr into the settings card (and the
  Event type lacked the field), so a stored true/false override always
  displayed as 'inherit' and the next save silently reset it to NULL.
- The QR overlay was nested inside the photos.length > 0 branch — an
  empty or category-filtered live gallery showed only 'Waiting for
  photos', exactly when 'scan to add the first photos' matters most.
  Now rendered for any running show.
- slideshowQrCache: insertion-order eviction at 50 entries — rotated
  tokens and past events no longer accumulate base64 PNGs forever.

* fix(slideshow): derive the QR origin from the kiosk request when the base is loopback (codex review of #848, round 2)

With the compose-default FRONTEND_URL=http://localhost:3000 (or no base
configured) the overlay QR sent scanning phones to their own localhost.
The state poll comes from the kiosk browser itself, so its Host header +
protocol (trust proxy is configured) are exactly the public origin
guests can reach — used whenever the configured base is missing or
loopback. Mirrors the ?origin= fallback #847 uses for the admin-side
QR downloads.

* fix(slideshow): kiosk passes its origin for the QR fallback (codex review of #848, round 3)

req.get('host') is not the browser origin behind the standard proxies —
frontend/nginx.conf forwards $host with the port stripped, so a compose
LAN deployment on :3000 encoded port 80. The kiosk now sends
window.location.origin with the session/state calls (validated
server-side, same pattern as #847's admin downloads); the Host-derived
origin remains as second fallback.

* fix(slideshow): reject loopback kiosk origins, throttle QR regeneration per event (codex review of #848, confirmation round)

- A loopback window.location.origin from the kiosk is no more
  guest-reachable than the loopback base it would replace — rejected;
  when no reachable URL remains the overlay is suppressed entirely (no
  QR beats a QR that sends phones to their own localhost). New test
  pins the suppression.
- The QR cache is keyed by event id with a 60s regeneration throttle:
  the origin is caller-influenced when the base is loopback, so
  URL-keyed caching let a slideshow-link holder force a fresh
  QRCode.toDataURL per request via unique origins — a cheap CPU
  exhaustion path. Encode rate is now bounded per event regardless of
  input. QR margin also raised to the 4-module spec quiet zone,
  matching #847.

* fix(slideshow): never serve a mismatched cached QR + single-flight encoding (codex review of #848, final round)

- A slideshow-token holder could poison the projector's QR: an
  attacker-origin entry cached per event was served to the legitimate
  kiosk for the rest of the throttle window. A cached artifact is now
  only served when its URL matches the request; mismatches inside the
  window suppress the overlay briefly instead of showing foreign
  content.
- Cold-cache stampede closed: concurrent polls share one in-flight
  encode promise instead of each scheduling a 512px render.

Rejected from the same round (false positive, verified empirically):
the loopback regex claim — /^https?:\/\/(localhost|127\.)/ matches
'http://localhost:3000' and '127.0.0.1:port' just fine (no trailing
slash required), and the suppression test runs green.
2026-07-19 22:36:03 +02:00
..

Database Migrations

This directory contains database migrations for the PicPeak photo sharing platform.

Directory Structure

/core

Essential migrations that are always run for new deployments. These include:

  • init.js - Initial database schema creation
  • Backup service tables (029-035)
  • Gallery feedback tables (033)
  • Pre-generated watermarks (061)

/legacy

Migrations needed only when upgrading from older versions. New deployments can skip these as the core schema already includes all necessary tables and columns.

For New Deployments

If you're deploying this application for the first time:

  1. The initializeDatabase() function in src/database/db.js will create all necessary tables
  2. Only migrations in the /core directory will be run
  3. This ensures a clean, optimized database schema

For Existing Deployments

If you're upgrading from an older version:

  1. All migrations (both core and legacy) will be run in sequence
  2. The migration system tracks which migrations have been applied
  3. Only new migrations will be executed

Running Migrations

# Development
npm run migrate

# Production
npm run migrate:prod

Note on Duplicate Migration Numbers

The legacy directory contains renamed duplicates:

  • 014_add_host_name_to_events_duplicate.js (was duplicate of 014)
  • 027_add_rate_limit_settings_duplicate.js (was duplicate of 027)

These have been renamed to avoid conflicts while preserving the migration history.