Fix multiple bugs: thumbnail generation, branding settings, categories, theme, feedback icons, upload limit, email errors

Bug fixes included:

#52 - Thumbnail Generation: Added proper parsing of settings values and validation
      of Sharp fit parameter to handle JSON-encoded strings correctly

#61 - Branding Settings Not Persisting: Added _parseBoolean helper for reliable
      boolean parsing, added hide_powered_by option for white-label support

#55 - Categories Not Applied: Fixed category update logic to properly handle
      numeric category IDs, added updated_at timestamp, improved cache invalidation

#59/#56 - Gallery Layout & Apply Theme: Set isPreviewMode=true so theme changes
      immediately propagate to parent state, hidden redundant Apply button

#58 - Feedback Icons Show When Disabled: Added feedbackEnabled check to comment
      and like buttons in MasonryGalleryLayout and GridGalleryLayout

#57 - Upload Limit 100MB: Increased body parser limit from 100MB to 500MB to
      support larger batch uploads

#54 - Wrong Error Message: Enhanced email error handling with specific error
      codes and translation keys for better user feedback
This commit is contained in:
Claude
2025-11-25 19:31:07 +00:00
parent 0745b11745
commit d91ab436e8
13 changed files with 182 additions and 41 deletions
+39 -8
View File
@@ -173,26 +173,57 @@ router.post('/test', adminAuth, async (req, res) => {
} catch (error) {
console.error('Test email error:', error);
console.error('Error stack:', error.stack);
// Provide more specific error messages
let errorMessage = 'Failed to send test email';
// Provide more specific error messages with translation keys
let errorMessage = 'Error sending email';
let errorKey = 'email.errors.sendFailed';
let details = error.message;
let detailsKey = 'email.errors.unknownError';
if (error.code === 'ECONNREFUSED') {
errorMessage = 'Failed to connect to SMTP server';
errorKey = 'email.errors.connectionRefused';
details = 'Please check your SMTP host and port settings';
detailsKey = 'email.errors.checkHostPort';
} else if (error.code === 'EAUTH') {
errorMessage = 'SMTP authentication failed';
errorKey = 'email.errors.authFailed';
details = 'Please check your SMTP username and password';
detailsKey = 'email.errors.checkCredentials';
} else if (error.code === 'ESOCKET') {
errorMessage = 'Network error';
errorMessage = 'Network error connecting to SMTP server';
errorKey = 'email.errors.networkError';
details = 'Could not establish connection to SMTP server';
detailsKey = 'email.errors.connectionFailed';
} else if (error.code === 'ETIMEDOUT') {
errorMessage = 'Connection to SMTP server timed out';
errorKey = 'email.errors.timeout';
details = 'The server took too long to respond. Please check your network and SMTP settings.';
detailsKey = 'email.errors.timeoutDetails';
} else if (error.code === 'ENOTFOUND') {
errorMessage = 'SMTP server not found';
errorKey = 'email.errors.serverNotFound';
details = 'The SMTP host could not be resolved. Please verify the hostname.';
detailsKey = 'email.errors.checkHostname';
} else if (error.responseCode >= 500) {
errorMessage = 'SMTP server error';
errorKey = 'email.errors.serverError';
details = `Server returned error code ${error.responseCode}`;
detailsKey = 'email.errors.serverErrorDetails';
} else if (error.responseCode >= 400) {
errorMessage = 'Email rejected by server';
errorKey = 'email.errors.rejected';
details = error.response || 'The email was rejected. Check recipient address and settings.';
detailsKey = 'email.errors.rejectedDetails';
}
res.status(500).json({
res.status(500).json({
error: errorMessage,
errorKey: errorKey,
details: details,
code: error.code
detailsKey: detailsKey,
code: error.code,
responseCode: error.responseCode
});
}
});
+42 -13
View File
@@ -484,24 +484,42 @@ router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
}
// Prepare update data
const updateData = {};
const updateData = {
updated_at: new Date()
};
// Handle type-based categories ('individual' or 'collage')
// These are string values that map to the photo.type field
if (category_id === 'individual' || category_id === 'collage') {
updateData.type = category_id;
updateData.category_id = null; // Clear legacy category_id
} else if (category_id === null || category_id === undefined) {
// Explicitly clear category
updateData.category_id = null;
} else {
// Handle legacy numeric category IDs
updateData.category_id = category_id || null;
// Handle numeric category IDs from photo_categories table
const numericCategoryId = parseInt(category_id, 10);
if (!isNaN(numericCategoryId)) {
updateData.category_id = numericCategoryId;
} else {
updateData.category_id = null;
}
}
// Update photo
await db('photos')
.where({ id: photoId })
.where({ id: photoId, event_id: eventId })
.update(updateData);
res.json({ message: 'Photo updated successfully' });
// Fetch and return updated photo for confirmation
const updatedPhoto = await db('photos')
.where({ id: photoId })
.first();
res.json({
message: 'Photo updated successfully',
photo: updatedPhoto
});
} catch (error) {
console.error('Error updating photo:', error);
res.status(500).json({ error: 'Failed to update photo' });
@@ -581,33 +599,44 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
try {
const { eventId } = req.params;
const { photoIds, updates } = req.body;
if (!Array.isArray(photoIds) || photoIds.length === 0) {
return res.status(400).json({ error: 'Invalid photo IDs' });
}
// Verify all photos belong to the event
const photoCount = await db('photos')
.whereIn('id', photoIds)
.where('event_id', eventId)
.count('id as count')
.first();
if (photoCount.count !== photoIds.length) {
if (parseInt(photoCount.count) !== photoIds.length) {
return res.status(400).json({ error: 'Some photos do not belong to this event' });
}
// Prepare update data
const updateData = {};
const updateData = {
updated_at: new Date()
};
if (updates.category_id !== undefined) {
// Handle type-based categories ('individual' or 'collage')
// These are string values that map to the photo.type field
if (updates.category_id === 'individual' || updates.category_id === 'collage') {
updateData.type = updates.category_id;
updateData.category_id = null; // Clear legacy category_id
} else if (updates.category_id === null) {
// Explicitly clear category
updateData.category_id = null;
} else {
// Handle legacy numeric category IDs
updateData.category_id = updates.category_id || null;
// Handle numeric category IDs from photo_categories table
const numericCategoryId = parseInt(updates.category_id, 10);
if (!isNaN(numericCategoryId)) {
updateData.category_id = numericCategoryId;
} else {
updateData.category_id = null;
}
}
}
+4 -2
View File
@@ -191,7 +191,8 @@ router.put('/branding', adminAuth, async (req, res) => {
logo_position,
logo_display_header,
logo_display_hero,
logo_display_mode
logo_display_mode,
hide_powered_by
} = req.body;
const brandingSettings = {
@@ -211,7 +212,8 @@ router.put('/branding', adminAuth, async (req, res) => {
logo_position,
logo_display_header,
logo_display_hero,
logo_display_mode
logo_display_mode,
hide_powered_by
};
// Handle favicon deletion if empty string or null is provided
+30 -4
View File
@@ -18,6 +18,29 @@ const DEFAULT_THUMBNAIL_FORMAT = 'jpeg';
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const getThumbnailPath = () => path.join(getStoragePath(), 'thumbnails');
// Helper to parse setting value (handles both JSON-encoded and plain values)
function parseSettingValue(value) {
if (value === null || value === undefined) {
return null;
}
// Try to parse as JSON first (in case it's a JSON-encoded string like '"cover"')
try {
return JSON.parse(value);
} catch (e) {
// If it's not valid JSON, return the raw value
return value;
}
}
// Validate that fit value is valid for Sharp
function validateFitValue(fit) {
const validFitValues = ['cover', 'contain', 'fill', 'inside', 'outside'];
if (fit && validFitValues.includes(fit)) {
return fit;
}
return DEFAULT_THUMBNAIL_FIT;
}
// Get thumbnail settings from database
async function getThumbnailSettings() {
try {
@@ -30,16 +53,19 @@ async function getThumbnailSettings() {
'thumbnail_format'
])
.select('setting_key', 'setting_value');
const settingsMap = {};
settings.forEach(s => {
settingsMap[s.setting_key] = s.setting_value;
settingsMap[s.setting_key] = parseSettingValue(s.setting_value);
});
// Parse and validate fit value
const fitValue = validateFitValue(settingsMap.thumbnail_fit);
return {
width: parseInt(settingsMap.thumbnail_width) || DEFAULT_THUMBNAIL_WIDTH,
height: parseInt(settingsMap.thumbnail_height) || DEFAULT_THUMBNAIL_HEIGHT,
fit: settingsMap.thumbnail_fit || DEFAULT_THUMBNAIL_FIT,
fit: fitValue,
quality: parseInt(settingsMap.thumbnail_quality) || DEFAULT_THUMBNAIL_QUALITY,
format: settingsMap.thumbnail_format || DEFAULT_THUMBNAIL_FORMAT
};