Files
picpeak/backend/__tests__/middleware/galleryAdminPreview.test.js
T
Luca f00661511c feat(gallery): admin preview skips the password on protected galleries (#981)
Closes #868.

A logged-in admin opening a published, password-protected gallery is let
straight in, mirroring the existing draft-visibility bypass.

Mechanism: an explicit ?admin_preview=1 intent flag AND a verified admin session
read from the httpOnly admin_token cookie (or an admin-typed Bearer) — never a
token from the URL. This retires the old ?preview=<raw-admin-JWT> scheme, which
leaked a 24h admin token into the address bar, referrers and proxy logs.

Per-request bypass only: no gallery JWT is minted, the password endpoint is
never reached so the login_attempts lockout buckets stay clean, and admin
previews are excluded from guest analytics (access_logs, download counts,
per-photo view_count, notification bells).

Review (two rounds) closed three blockers and two concerns:

- Transport: verifyGalleryAccess now resolves admin preview before any gallery
  credential, and isAdminPreview reads the admin cookie first and type-checks
  every candidate — so an admin Bearer no longer 403s on the type gate, and a
  coexisting gallery session can no longer shadow the admin cookie.
- Reveal mode (#838) is a second consumer of isAdminPreview; its bypass is
  unchanged, only the transport moves. revealMode.test.js updated off the
  retired scheme and now carries a coexisting gallery Bearer.
- Admin previews no longer inflate per-photo view counts, and the internal photo
  redirects preserve the flag via withPreview() so they still authorise.
- Happy path: GalleryPage renders GalleryView directly for a preview instead of
  attempting the public empty-password auto-login, which 401'd against a
  genuinely protected gallery and stranded the page on the skeleton.

The backend job timed out once at the 10-minute CI limit; a re-run completed in
2m02s, in line with main's ~2m10s baseline, so that was a runner flake rather
than a hang.
2026-08-04 16:48:07 +02:00

68 lines
3.0 KiB
JavaScript

/**
* #868 — the admin gallery-preview gate. isAdminPreview must fail CLOSED: it
* grants the draft/password bypass only for an explicit `?admin_preview=1` flag
* AND a verified admin JWT (type 'admin', issuer 'picpeak-auth') read from the
* httpOnly admin_token cookie or a Bearer header — never from the URL, never for
* a guest/gallery token.
*/
process.env.JWT_SECRET = process.env.JWT_SECRET || 'admin-preview-test-secret';
const jwt = require('jsonwebtoken');
const { isAdminPreview } = require('../../src/middleware/gallery');
// Read the secret at call time — a jest setup file can set JWT_SECRET after this
// module loads, and isAdminPreview verifies against the live value.
const adminToken = () => jwt.sign({ type: 'admin', id: 1 }, process.env.JWT_SECRET, { issuer: 'picpeak-auth' });
const galleryToken = () => jwt.sign({ type: 'gallery', eventId: 1 }, process.env.JWT_SECRET, { issuer: 'picpeak-auth' });
function req({ flag, cookie, bearer } = {}) {
return {
query: flag === undefined ? {} : { admin_preview: flag },
cookies: cookie ? { admin_token: cookie } : {},
headers: bearer ? { authorization: `Bearer ${bearer}` } : {},
};
}
describe('isAdminPreview (#868) fails closed', () => {
it('false without the explicit flag, even with a valid admin cookie (plain link stays guest-identical)', () => {
expect(isAdminPreview(req({ cookie: adminToken() }))).toBe(false);
});
it('false with the flag but no session token', () => {
expect(isAdminPreview(req({ flag: '1' }))).toBe(false);
});
it('true with the flag + a valid admin cookie', () => {
expect(isAdminPreview(req({ flag: '1', cookie: adminToken() }))).toBe(true);
});
it('true with the flag + a valid admin Bearer header', () => {
expect(isAdminPreview(req({ flag: '1', bearer: adminToken() }))).toBe(true);
});
it('false for a gallery (guest) token — must be type admin', () => {
expect(isAdminPreview(req({ flag: '1', cookie: galleryToken() }))).toBe(false);
});
it('true from the admin cookie even when a gallery Bearer is also present (#981 coexisting session)', () => {
expect(isAdminPreview(req({ flag: '1', cookie: adminToken(), bearer: galleryToken() }))).toBe(true);
});
it('false when only a gallery Bearer is present — a gallery header can never satisfy it (#981)', () => {
expect(isAdminPreview(req({ flag: '1', bearer: galleryToken() }))).toBe(false);
});
it('false on a tampered token', () => {
expect(isAdminPreview(req({ flag: '1', cookie: `${adminToken()}x` }))).toBe(false);
});
it('false on the wrong issuer', () => {
const t = jwt.sign({ type: 'admin' }, process.env.JWT_SECRET, { issuer: 'not-picpeak' });
expect(isAdminPreview(req({ flag: '1', cookie: t }))).toBe(false);
});
it('false when the flag is anything other than exactly "1"', () => {
expect(isAdminPreview(req({ flag: 'true', cookie: adminToken() }))).toBe(false);
expect(isAdminPreview(req({ flag: '0', cookie: adminToken() }))).toBe(false);
});
});