Merge pull request #319 from the-luap/feat/prezip-and-photo-replace

fix: discussion #317 issues and #318 archive crash
This commit is contained in:
Paul Nothaft
2026-04-26 22:51:46 +02:00
committed by GitHub
12 changed files with 293 additions and 72 deletions
+48 -2
View File
@@ -113,6 +113,31 @@ const getEventFieldRequirements = async () => {
}
};
// Helper to read app_settings booleans by key, used to inherit per-setting
// defaults onto new events. Returns `undefined` for missing/non-boolean rows
// so callers can fall back to a legacy default.
const readBooleanSetting = async (key) => {
try {
const setting = await db('app_settings').where('setting_key', key).first();
if (!setting) return undefined;
let value = setting.setting_value;
if (typeof value === 'string') {
try { value = JSON.parse(value); } catch { /* keep raw */ }
}
return typeof value === 'boolean' ? value : undefined;
} catch (error) {
logger.error('Failed to read app setting', { key, error: error.message });
return undefined;
}
};
// Helper to read the global "enable_devtools_protection" admin setting so
// new events inherit it instead of always falling back to the DB column default
// (#317 — admin disabled it globally but new events still got it ON).
const getDownloadProtectionDefaults = async () => {
return { enable_devtools_protection: await readBooleanSetting('enable_devtools_protection') };
};
// Helper to get branding defaults for new events (Feature 7: Branding Inheritance)
const getBrandingDefaults = async () => {
try {
@@ -246,6 +271,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(),
body('allow_downloads').optional().isBoolean(),
body('disable_right_click').optional().isBoolean(),
body('enable_devtools_protection').optional().isBoolean(),
body('watermark_downloads').optional().isBoolean(),
body('watermark_text').optional().trim(),
body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt(),
@@ -291,9 +317,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [
upload_category_id = null,
allow_downloads = true,
disable_right_click = false,
enable_devtools_protection: enableDevtoolsProtectionInput,
watermark_downloads = false,
watermark_text = null,
require_password: requirePasswordInput = true,
require_password: requirePasswordInput,
// Feedback settings
feedback_enabled = false,
allow_ratings = true,
@@ -349,7 +376,14 @@ router.post('/', adminAuth, requirePermission('events.create'), [
return res.status(400).json({ errors: validationErrors });
}
const requirePassword = parseBooleanInput(requirePasswordInput, true);
// Default require_password from global "event_default_require_password"
// setting when the body omits it (#317 — admins want to flip the default).
let requirePasswordFallback = true;
if (requirePasswordInput === undefined) {
const setting = await readBooleanSetting('event_default_require_password');
if (setting !== undefined) requirePasswordFallback = setting;
}
const requirePassword = parseBooleanInput(requirePasswordInput, requirePasswordFallback);
// Debug logging
logger.debug('Download control values', {
@@ -457,6 +491,17 @@ router.post('/', adminAuth, requirePermission('events.create'), [
const effectiveHeroLogoSize = req.body.hero_logo_size || brandingDefaults.hero_logo_size;
const effectiveHeroLogoPosition = req.body.hero_logo_position || brandingDefaults.hero_logo_position;
// Inherit "Detect dev tools" from the global Image Security setting unless
// the request explicitly overrides it (#317 — admin disabled it globally
// but new events still got it ON because the column default is true).
const protectionDefaults = await getDownloadProtectionDefaults();
const effectiveEnableDevtoolsProtection =
enableDevtoolsProtectionInput !== undefined
? enableDevtoolsProtectionInput
: protectionDefaults.enable_devtools_protection !== undefined
? protectionDefaults.enable_devtools_protection
: true;
// Insert into database
const insertResult = await db('events').insert({
slug,
@@ -479,6 +524,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
upload_category_id,
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false),
enable_devtools_protection: formatBoolean(effectiveEnableDevtoolsProtection),
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
watermark_text,
require_password: formatBoolean(requirePassword),
+9 -1
View File
@@ -13,7 +13,11 @@ router.get('/', async (req, res) => {
this.whereIn('setting_type', ['branding', 'theme', 'general', 'security', 'analytics', 'boolean'])
.orWhere('setting_key', 'like', 'analytics_%')
.orWhere('setting_key', 'like', 'event_require_%')
.orWhereIn('setting_key', ['seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai']);
.orWhereIn('setting_key', [
'seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai',
'event_default_require_password',
'gallery_show_filter_bar'
]);
})
.select('setting_key', 'setting_value');
});
@@ -78,6 +82,10 @@ router.get('/', async (req, res) => {
event_require_admin_email: settingsObject.event_require_admin_email !== false,
event_require_event_date: settingsObject.event_require_event_date !== false,
event_require_expiration: settingsObject.event_require_expiration !== false,
// Default value for "Require password" toggle in event creation form
event_default_require_password: settingsObject.event_default_require_password !== false,
// Whether to show the search/sort filter bar in public galleries (default: true)
gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false,
// Upload settings (safe to expose - needed for client-side validation)
allowed_file_types: settingsObject.general_allowed_file_types || 'jpg,jpeg,png,webp',
// SEO meta tag flags (safe to expose - these are intended for crawlers)
+35 -24
View File
@@ -61,32 +61,43 @@ async function archiveEvent(event) {
}
output.on('close', async () => {
logger.info(`Archive created: ${archiveName} (${archive.pointer()} bytes)`);
// Update database
await db('events').where('id', event.id).update({
is_archived: true,
archive_path: path.relative(getStoragePath(), archivePath),
archived_at: new Date()
});
// Delete original files
await fs.rm(eventPath, { recursive: true });
// Delete thumbnails
const photos = await db('photos').where('event_id', event.id);
for (const photo of photos) {
if (photo.thumbnail_path) {
const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
await fs.unlink(thumbPath).catch(() => {}); // Ignore if already deleted
try {
logger.info(`Archive created: ${archiveName} (${archive.pointer()} bytes)`);
// Update database
await db('events').where('id', event.id).update({
is_archived: true,
archive_path: path.relative(getStoragePath(), archivePath),
archived_at: new Date()
});
// Delete original files
await fs.rm(eventPath, { recursive: true });
// Delete thumbnails
const photos = await db('photos').where('event_id', event.id);
for (const photo of photos) {
if (photo.thumbnail_path) {
const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
await fs.unlink(thumbPath).catch(() => {}); // Ignore if already deleted
}
}
// Queue completion email — admin_email is nullable on events (migration 073);
// skip queueing rather than violating email_queue.recipient_email NOT NULL.
if (event.admin_email) {
await queueEmail(event.id, event.admin_email, 'archive_complete', {
event_name: event.event_name,
archive_size: (archive.pointer() / 1024 / 1024).toFixed(2) + ' MB'
});
} else {
logger.info(`Skipping archive_complete email for event ${event.slug}: no admin_email set`);
}
} catch (err) {
// Never let the close handler reject — it runs detached from the caller,
// and an unhandled rejection here crashes the backend process.
logger.error(`Post-archive cleanup failed for event ${event.slug}:`, err);
}
// Queue completion email
await queueEmail(event.id, event.admin_email, 'archive_complete', {
event_name: event.event_name,
archive_size: (archive.pointer() / 1024 / 1024).toFixed(2) + ' MB'
});
});
archive.pipe(output);