fix(gallery): no Logout button on galleries that don't require a password (#1149) (#1154)

Stable twin of #1152. The reporter is on v3.46.1, so this branch is where the bug was actually seen.

showLogout was hard-coded true, so a gallery with no password showed a Logout button, and clicking it stranded the visitor on the loading skeleton — GalleryPage's auto-login is a one-shot latch that never re-fires.

The button is gated at both call sites, including the full-page layouts which render it on the callback rather than a flag. accessLevel and viaCustomer now come from /auth/session instead of per-tab sessionStorage, which silently downgraded a PIN-client session in a second tab. The public-gallery branch shows a reason and a Retry once auto-login has run and failed, instead of a skeleton that never stops.

Carries the full main fix including viaCustomer, even though reveal mode does not exist here, so the branches do not drift.

Merged with admin privileges: the author cannot self-approve.
This commit is contained in:
Paul Nothaft
2026-08-23 22:07:05 +02:00
committed by GitHub
parent e46260ad07
commit d46397d92a
5 changed files with 154 additions and 10 deletions
@@ -135,9 +135,9 @@ function signAdminToken({ id = 1, username = 'admin', iat, exp }) {
);
}
function signGalleryToken({ eventId = 100, eventSlug = 'wedding' } = {}) {
function signGalleryToken({ eventId = 100, eventSlug = 'wedding', ...extra } = {}) {
return jwt.sign(
{ eventId, eventSlug, type: 'gallery' },
{ eventId, eventSlug, type: 'gallery', ...extra },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'picpeak-auth' }
);
@@ -288,6 +288,56 @@ describe('GET /auth/session — symmetry with protected middleware', () => {
expect(res.body.valid).toBe(true);
});
/**
* What KIND of gallery session this is (#1149).
*
* The frontend used to keep this in sessionStorage, which is per-TAB while
* the cookie is per-browser: a gallery reopened in a second tab lost
* 'client' even though the backend still served it as one, and the UI hid
* the only control that clears the privileged cookie. Reported from the
* token so a restored session knows what it actually is.
*/
describe('gallery session kind', () => {
beforeEach(() => {
fakeDb.events.push({
id: 100,
slug: 'wedding',
is_active: true,
is_archived: false,
expires_at: new Date(Date.now() + 86400_000),
});
});
it('reports a PIN-client session as client', async () => {
const res = await request(makeApp())
.get('/auth/session?slug=wedding')
.set('Authorization', `Bearer ${signGalleryToken({ accessLevel: 'client' })}`);
expect(res.body.valid).toBe(true);
expect(res.body.accessLevel).toBe('client');
expect(res.body.viaCustomer).toBe(false);
});
it('reports a customer-portal session, which looks like a guest', async () => {
const res = await request(makeApp())
.get('/auth/session?slug=wedding')
.set('Authorization', `Bearer ${signGalleryToken({ via: 'customer', customerId: 7 })}`);
expect(res.body.valid).toBe(true);
expect(res.body.accessLevel).toBe('guest');
expect(res.body.viaCustomer).toBe(true);
});
it('reports a plain guest as neither', async () => {
// The flags have to discriminate, or they would just hand every visitor
// a Logout button back.
const res = await request(makeApp())
.get('/auth/session?slug=wedding')
.set('Authorization', `Bearer ${signGalleryToken()}`);
expect(res.body.valid).toBe(true);
expect(res.body.accessLevel).toBe('guest');
expect(res.body.viaCustomer).toBe(false);
});
});
it('returns valid:false when the token is revoked', async () => {
fakeDb.adminUsers.push({
id: 1,