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.
This commit is contained in:
@@ -152,9 +152,13 @@ describe('Reveal mode (#838)', () => {
|
||||
expect(res.body.photos).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('/photos serves the admin preview everything', async () => {
|
||||
it('/photos serves the admin preview everything (new transport: ?admin_preview=1 + admin cookie, even with a coexisting gallery session)', async () => {
|
||||
// #868/#981: reveal-mode hiding is bypassed for an admin preview via the
|
||||
// new transport (explicit flag + httpOnly admin_token cookie), NOT the
|
||||
// retired ?preview=<jwt>. The coexisting gallery Bearer must not shadow it.
|
||||
const res = await request(app)
|
||||
.get(`/api/gallery/${SLUG}/photos?preview=${encodeURIComponent(adminToken)}`)
|
||||
.get(`/api/gallery/${SLUG}/photos?admin_preview=1`)
|
||||
.set('Cookie', [`admin_token=${adminToken}`])
|
||||
.set('Authorization', `Bearer ${galleryToken()}`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.hidden_until_reveal).toBe(false);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* #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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user