fix(api/v1/events): also honour require_password + branding defaults (#592 follow-up)

Same class of bug as the devtools-detection gap landed in 2304b25.
v1 POST /events was hardcoding require_password=true in the destructure
default and skipping getBrandingDefaults entirely, so:

- Admins who disabled "require password by default" globally still
  got password-required galleries through the API.
- API-created events ignored the global branding_logo_display_hero
  and branding_logo_size toggles, defaulting to visible/medium
  regardless of the admin's preferred branding chrome.

Mirror the readBooleanSetting + getBrandingDefaults pattern from
adminEvents.js inline (helpers aren't exported, and pulling them out
is out-of-scope for this fix). Adds validators, fallback resolution,
and the three resolved values to the events insert. hero_logo_position
stays at 'top' since #357 / migration 084 explicitly disconnected it
from the header-bar branding_logo_position setting. OpenAPI updated.
This commit is contained in:
Paul Nothaft
2026-05-31 22:47:01 +02:00
parent 2304b25624
commit 2d44b1ab2d
+58 -5
View File
@@ -86,12 +86,15 @@ const photoUpload = multer({
* customer_email: { type: string, format: email, nullable: true }
* customer_phone: { type: string, nullable: true, description: "Only persisted when the global phone-field setting is enabled." }
* admin_email: { type: string, format: email, nullable: true }
* require_password: { type: boolean, default: true }
* password: { type: string, nullable: true, description: "Required when require_password is true." }
* require_password: { type: boolean, nullable: true, description: "When omitted, falls back to the global event_default_require_password setting." }
* password: { type: string, nullable: true, description: "Required when require_password resolves to true." }
* expires_at: { type: string, format: date-time, nullable: true }
* color_theme: { type: string, nullable: true, description: "Preset name (e.g. 'default') or JSON-encoded ThemeConfig. Persisted as-is on the event row." }
* feedback_enabled: { type: boolean, nullable: true, description: "Enable guest feedback for this gallery. When omitted, falls back to the global event_default_feedback_enabled setting." }
* enable_devtools_protection: { type: boolean, nullable: true, description: "Block right-click / devtools shortcuts in the gallery. When omitted, falls back to the global enable_devtools_protection setting." }
* hero_logo_visible: { type: boolean, nullable: true, description: "Show event logo in the hero block. When omitted, falls back to the global branding_logo_display_hero setting." }
* hero_logo_size: { type: string, nullable: true, enum: [small, medium, large, xlarge], description: "Hero logo size. When omitted, falls back to the global branding_logo_size setting." }
* hero_logo_position: { type: string, nullable: true, enum: [top, center, bottom], description: "Hero logo position. Defaults to 'top' (not settings-backed — see migration 084)." }
* responses:
* 201:
* description: Event created
@@ -125,7 +128,10 @@ router.post(
body('expires_at').optional({ nullable: true, checkFalsy: true }).isISO8601(),
body('color_theme').optional({ nullable: true }).isString().trim(),
body('feedback_enabled').optional().isBoolean(),
body('enable_devtools_protection').optional().isBoolean()
body('enable_devtools_protection').optional().isBoolean(),
body('hero_logo_visible').optional().isBoolean(),
body('hero_logo_size').optional().isIn(['small', 'medium', 'large', 'xlarge']),
body('hero_logo_position').optional().isIn(['top', 'center', 'bottom'])
],
async (req, res) => {
try {
@@ -134,11 +140,16 @@ router.post(
const {
event_name, event_type, event_date,
customer_name = null, customer_email = null, customer_phone = null,
admin_email = null, require_password = true, password,
admin_email = null,
require_password: requirePasswordInput,
password,
expires_at = null,
color_theme = null,
feedback_enabled: feedbackEnabledInput,
enable_devtools_protection: devtoolsInput
enable_devtools_protection: devtoolsInput,
hero_logo_visible: heroLogoVisibleInput,
hero_logo_size: heroLogoSizeInput,
hero_logo_position: heroLogoPositionInput
} = req.body;
// Issue #550 — mirror the admin POST path so API-created events
@@ -174,6 +185,44 @@ router.post(
}
const enable_devtools_protection = parseBooleanInput(devtoolsInput, devtoolsFallback);
// Same shape as the feedback / devtools fallbacks: honour the global
// event_default_require_password toggle (#317). Without this an admin
// who disabled "require password by default" globally still got
// password-required galleries through the API.
let requirePasswordFallback = true;
if (requirePasswordInput === undefined) {
const setting = await db('app_settings').where('setting_key', 'event_default_require_password').first();
if (setting) {
try {
const parsed = JSON.parse(setting.setting_value);
if (typeof parsed === 'boolean') requirePasswordFallback = parsed;
} catch { /* keep true */ }
}
}
const require_password = parseBooleanInput(requirePasswordInput, requirePasswordFallback);
// Branding inheritance (Feature 7) — mirror adminEvents.js
// getBrandingDefaults so API-created events inherit the global
// hero logo visibility + size. hero_logo_position is intentionally
// NOT settings-backed (see migration 084 / #357 — branding_logo_position
// is the *header bar*, a different concept than the hero block).
let heroLogoVisibleFallback = true;
let heroLogoSizeFallback = 'medium';
const brandingRows = await db('app_settings')
.whereIn('setting_key', ['branding_logo_display_hero', 'branding_logo_size'])
.select('setting_key', 'setting_value');
for (const row of brandingRows) {
let value = row.setting_value;
if (typeof value === 'string') {
try { value = JSON.parse(value); } catch { /* keep raw */ }
}
if (row.setting_key === 'branding_logo_display_hero') heroLogoVisibleFallback = value !== false;
if (row.setting_key === 'branding_logo_size' && value) heroLogoSizeFallback = value;
}
const hero_logo_visible = heroLogoVisibleInput !== undefined ? heroLogoVisibleInput : heroLogoVisibleFallback;
const hero_logo_size = heroLogoSizeInput || heroLogoSizeFallback;
const hero_logo_position = heroLogoPositionInput || 'top';
if (require_password && (!password || password.length < 6)) {
return res.status(400).json({ error: 'Password is required when require_password is true (min 6 chars)' });
}
@@ -225,6 +274,10 @@ router.post(
// Issue #592 — write the resolved devtools setting (input value
// or global fallback) so the column default doesn't shadow it.
enable_devtools_protection: formatBoolean(enable_devtools_protection),
// Branding inheritance — resolved value from body or app_settings.
hero_logo_visible: formatBoolean(hero_logo_visible),
hero_logo_size,
hero_logo_position,
...(customer_name ? { customer_name } : {}),
...(customer_email ? { customer_email } : {}),
...(persistPhone ? { customer_phone: persistPhone } : {})