feat(gallery): per-event toggle to hide the logo on the password page (#894) (#928)

* feat(gallery): per-event toggle to hide the logo on the password page (#894)

* fix(admin): harden login_logo_visible coercion for SQLite + string booleans (#894)

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-07-31 08:56:48 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 926a4a540d
commit 08ff9f20e7
12 changed files with 122 additions and 11 deletions
@@ -201,6 +201,34 @@ describe('admin events CRUD endpoints (smoke)', () => {
});
expect(res.status).toBe(400);
});
// #894 — per-event password-page logo toggle: false hides, null
// restores the default (show).
it('stores login_logo_visible: false and clears it back to NULL', async () => {
const id = await insertEvent(db, adminId);
const hide = await auth(request(app).put(`/api/admin/events/${id}`)).send({
login_logo_visible: false,
});
expect(hide.status).toBe(200);
let row = await db('events').where({ id }).first();
expect([false, 0]).toContain(row.login_logo_visible);
const clear = await auth(request(app).put(`/api/admin/events/${id}`)).send({
login_logo_visible: null,
});
expect(clear.status).toBe(200);
row = await db('events').where({ id }).first();
expect(row.login_logo_visible).toBeNull();
// The string "false" passes isBoolean() validation — it must be
// parsed, not treated as a truthy string (would store 1 = show).
const hideStr = await auth(request(app).put(`/api/admin/events/${id}`)).send({
login_logo_visible: 'false',
});
expect(hideStr.status).toBe(200);
row = await db('events').where({ id }).first();
expect([false, 0]).toContain(row.login_logo_visible);
});
});
describe('DELETE /:id', () => {
@@ -0,0 +1,21 @@
/**
* Migration 166: per-event toggle to hide the branding logo on the
* gallery password page (#894).
*
* NULL (the default) keeps today's behaviour — the global branding logo is
* shown above the password form. Only an explicit `false` hides it for
* that gallery; the admin login page and other surfaces are unaffected.
*/
exports.up = async function (knex) {
if (await knex.schema.hasColumn('events', 'login_logo_visible')) return;
await knex.schema.alterTable('events', (t) => {
t.boolean('login_logo_visible').nullable();
});
};
exports.down = async function (knex) {
if (!(await knex.schema.hasColumn('events', 'login_logo_visible'))) return;
await knex.schema.alterTable('events', (t) => {
t.dropColumn('login_logo_visible');
});
};
+13
View File
@@ -1090,6 +1090,7 @@ module.exports = (router) => {
hero_logo_visible: source.hero_logo_visible,
hero_logo_size: source.hero_logo_size,
hero_logo_position: source.hero_logo_position,
login_logo_visible: source.login_logo_visible,
header_style: source.header_style || 'standard',
hero_divider_style: source.hero_divider_style || 'wave',
hero_image_anchor: source.hero_image_anchor || 'center',
@@ -1237,6 +1238,8 @@ module.exports = (router) => {
body('hero_logo_visible').optional({ nullable: true }).isBoolean(),
body('hero_logo_size').optional({ nullable: true }).isIn(['small', 'medium', 'large', 'xlarge']),
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom']),
// Password-page logo toggle (#894). null = default (show).
body('login_logo_visible').optional({ nullable: true }).isBoolean(),
// Header style settings (decoupled from layout)
body('header_style').optional().isIn(['hero', 'standard', 'banner', 'minimal', 'none']),
body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']),
@@ -1452,6 +1455,16 @@ module.exports = (router) => {
: formatBoolean(updates.hero_logo_visible);
}
// Password-page logo toggle (#894): null passes through; other
// accepted representations ("false", 0, …) are parsed before the
// DB formatting — formatBoolean alone would store the truthy
// string "false" as 1.
if (Object.prototype.hasOwnProperty.call(updates, 'login_logo_visible')) {
updates.login_logo_visible = updates.login_logo_visible === null
? null
: formatBoolean(parseBooleanInput(updates.login_logo_visible, true));
}
// Per-event opt-in for hero-photo OG share image (#474). Coerce so
// SQLite stores 0/1 and Postgres stores boolean true/false.
if (Object.prototype.hasOwnProperty.call(updates, 'og_image_share_enabled')) {
+4
View File
@@ -221,6 +221,7 @@ router.get('/:slug/info', async (req, res) => {
'hero_logo_size',
'hero_logo_position',
'hero_logo_url',
'login_logo_visible',
'header_style',
'hero_divider_style',
'hero_image_anchor',
@@ -290,6 +291,9 @@ router.get('/:slug/info', async (req, res) => {
enable_devtools_protection: event.enable_devtools_protection === true || event.enable_devtools_protection === 1 || event.enable_devtools_protection === '1',
use_canvas_rendering: event.use_canvas_rendering === true || event.use_canvas_rendering === 1 || event.use_canvas_rendering === '1',
hero_logo_visible: resolveHeroLogoVisible(event.hero_logo_visible, globalHeroLogoVisible),
// #894: only an explicit false hides the logo on the password page;
// NULL keeps the default (show).
login_logo_visible: !(event.login_logo_visible === false || event.login_logo_visible === 0 || event.login_logo_visible === '0'),
// #756: NULL per-event size inherits the global branding_logo_size.
hero_logo_size: event.hero_logo_size || globalLogoSize || 'medium',
hero_logo_position: event.hero_logo_position || 'top',