fix: resolve GitHub issues #4, #8, #9, and #10

- Fix missing database columns for password reset (#8)
  - Add must_change_password column to admin_users table
  - Add password_changed_at column for tracking password changes

- Fix feedback functionality (#9)
  - Add require_moderation column to event_feedback_settings table
  - Add missing host_name column to events table

- Add download control features (#10)
  - Add allow_downloads, disable_right_click, watermark_downloads columns to events
  - Implement download restrictions in gallery endpoints
  - Update event creation and update endpoints to support new fields
  - Prevent downloads when disabled for an event

- Login functionality (#4) verified working with proper credentials

All database migrations included and tested with Docker environment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-21 20:06:49 +02:00
parent a699a0477b
commit 934d6ddc58
4 changed files with 212 additions and 8 deletions
@@ -0,0 +1,87 @@
// Fix missing columns identified in GitHub issues
exports.up = async function(knex) {
console.log('Adding missing columns to database tables...');
// Add must_change_password column to admin_users table
const hasMustChangePassword = await knex.schema.hasColumn('admin_users', 'must_change_password');
if (!hasMustChangePassword) {
console.log('Adding must_change_password column to admin_users table...');
await knex.schema.table('admin_users', (table) => {
table.boolean('must_change_password').defaultTo(false);
});
}
// Add password_changed_at column to admin_users table
const hasPasswordChangedAt = await knex.schema.hasColumn('admin_users', 'password_changed_at');
if (!hasPasswordChangedAt) {
console.log('Adding password_changed_at column to admin_users table...');
await knex.schema.table('admin_users', (table) => {
table.datetime('password_changed_at');
});
}
// Add require_moderation column to event_feedback_settings table
const hasEventFeedbackSettings = await knex.schema.hasTable('event_feedback_settings');
if (hasEventFeedbackSettings) {
const hasRequireModeration = await knex.schema.hasColumn('event_feedback_settings', 'require_moderation');
if (!hasRequireModeration) {
console.log('Adding require_moderation column to event_feedback_settings table...');
await knex.schema.table('event_feedback_settings', (table) => {
table.boolean('require_moderation').defaultTo(true);
});
}
}
// Add host_name column to events table if missing
const hasHostName = await knex.schema.hasColumn('events', 'host_name');
if (!hasHostName) {
console.log('Adding host_name column to events table...');
await knex.schema.table('events', (table) => {
table.string('host_name');
});
}
console.log('Missing columns have been added successfully');
};
exports.down = async function(knex) {
console.log('Removing added columns...');
// Remove must_change_password column from admin_users table
const hasMustChangePassword = await knex.schema.hasColumn('admin_users', 'must_change_password');
if (hasMustChangePassword) {
await knex.schema.table('admin_users', (table) => {
table.dropColumn('must_change_password');
});
}
// Remove password_changed_at column from admin_users table
const hasPasswordChangedAt = await knex.schema.hasColumn('admin_users', 'password_changed_at');
if (hasPasswordChangedAt) {
await knex.schema.table('admin_users', (table) => {
table.dropColumn('password_changed_at');
});
}
// Remove require_moderation column from event_feedback_settings table
const hasEventFeedbackSettings = await knex.schema.hasTable('event_feedback_settings');
if (hasEventFeedbackSettings) {
const hasRequireModeration = await knex.schema.hasColumn('event_feedback_settings', 'require_moderation');
if (hasRequireModeration) {
await knex.schema.table('event_feedback_settings', (table) => {
table.dropColumn('require_moderation');
});
}
}
// Remove host_name column from events table
const hasHostName = await knex.schema.hasColumn('events', 'host_name');
if (hasHostName) {
await knex.schema.table('events', (table) => {
table.dropColumn('host_name');
});
}
console.log('Columns removed');
};
@@ -0,0 +1,69 @@
// Add download control features to events table
exports.up = async function(knex) {
console.log('Adding download control columns to events table...');
// Add download control columns to events table
const hasAllowDownloads = await knex.schema.hasColumn('events', 'allow_downloads');
if (!hasAllowDownloads) {
await knex.schema.table('events', (table) => {
table.boolean('allow_downloads').defaultTo(true);
table.boolean('disable_right_click').defaultTo(false);
table.boolean('watermark_downloads').defaultTo(false);
table.text('watermark_text');
});
}
// Add download control settings to app_settings
const downloadSettingExists = await knex('app_settings')
.where('setting_key', 'default_allow_downloads')
.first();
if (!downloadSettingExists) {
await knex('app_settings').insert([
{
setting_key: 'default_allow_downloads',
setting_value: JSON.stringify(true),
setting_type: 'gallery'
},
{
setting_key: 'default_disable_right_click',
setting_value: JSON.stringify(false),
setting_type: 'gallery'
},
{
setting_key: 'default_watermark_downloads',
setting_value: JSON.stringify(false),
setting_type: 'gallery'
}
]);
}
console.log('Download control features added successfully');
};
exports.down = async function(knex) {
console.log('Removing download control columns...');
// Remove app settings
await knex('app_settings')
.whereIn('setting_key', [
'default_allow_downloads',
'default_disable_right_click',
'default_watermark_downloads'
])
.delete();
// Remove columns from events table
const hasAllowDownloads = await knex.schema.hasColumn('events', 'allow_downloads');
if (hasAllowDownloads) {
await knex.schema.table('events', (table) => {
table.dropColumn('allow_downloads');
table.dropColumn('disable_right_click');
table.dropColumn('watermark_downloads');
table.dropColumn('watermark_text');
});
}
console.log('Download control columns removed');
};
+34 -5
View File
@@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const { body, query, validationResult } = require('express-validator'); const { body, query, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db'); const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth-enhanced-v2'); const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router(); const router = express.Router();
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
@@ -12,7 +13,6 @@ const { queueEmail } = require('../services/emailProcessor');
const { escapeLikePattern } = require('../utils/sqlSecurity'); const { escapeLikePattern } = require('../utils/sqlSecurity');
// formatDate import removed - dates are formatted by email processor // formatDate import removed - dates are formatted by email processor
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation'); const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
const { formatBoolean } = require('../utils/dbCompat');
// Create new event // Create new event
router.post('/', adminAuth, [ router.post('/', adminAuth, [
@@ -27,7 +27,11 @@ router.post('/', adminAuth, [
body('color_theme').optional().trim(), body('color_theme').optional().trim(),
body('allow_user_uploads').optional().isBoolean().toBoolean(), body('allow_user_uploads').optional().isBoolean().toBoolean(),
body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(), body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(),
body('host_name').notEmpty().trim() body('host_name').notEmpty().trim(),
body('allow_downloads').optional().isBoolean(),
body('disable_right_click').optional().isBoolean(),
body('watermark_downloads').optional().isBoolean(),
body('watermark_text').optional().trim()
], async (req, res) => { ], async (req, res) => {
try { try {
console.log('Create event request body:', req.body); console.log('Create event request body:', req.body);
@@ -49,9 +53,26 @@ router.post('/', adminAuth, [
color_theme = null, color_theme = null,
expiration_days = 30, expiration_days = 30,
allow_user_uploads = false, allow_user_uploads = false,
upload_category_id = null upload_category_id = null,
allow_downloads = true,
disable_right_click = false,
watermark_downloads = false,
watermark_text = null
} = req.body; } = req.body;
// Debug logging
console.log('Download control values:', {
allow_downloads,
disable_right_click,
watermark_downloads,
watermark_text,
types: {
allow_downloads: typeof allow_downloads,
disable_right_click: typeof disable_right_click,
watermark_downloads: typeof watermark_downloads
}
});
// Validate password strength // Validate password strength
const passwordValidation = await validatePasswordInContext(password, 'gallery', { const passwordValidation = await validatePasswordInContext(password, 'gallery', {
eventName: event_name eventName: event_name
@@ -121,7 +142,11 @@ router.post('/', adminAuth, [
expires_at: expires_at.toISOString(), expires_at: expires_at.toISOString(),
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
allow_user_uploads, allow_user_uploads,
upload_category_id upload_category_id,
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false),
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
watermark_text
}).returning('id'); }).returning('id');
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs) // Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
@@ -341,7 +366,11 @@ router.put('/:id', adminAuth, [
// Check if it's a number or can be converted to a valid integer // Check if it's a number or can be converted to a valid integer
const num = Number(value); const num = Number(value);
return !isNaN(num) && Number.isInteger(num); return !isNaN(num) && Number.isInteger(num);
}).withMessage('hero_photo_id must be an integer or null') }).withMessage('hero_photo_id must be an integer or null'),
body('allow_downloads').optional().isBoolean(),
body('disable_right_click').optional().isBoolean(),
body('watermark_downloads').optional().isBoolean(),
body('watermark_text').optional().trim()
], async (req, res) => { ], async (req, res) => {
try { try {
const errors = validationResult(req); const errors = validationResult(req);
+22 -3
View File
@@ -46,7 +46,8 @@ router.get('/:slug/info', async (req, res) => {
const event = await db('events') const event = await db('events')
.where({ slug }) .where({ slug })
.select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active', 'is_archived', 'share_link') .select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active', 'is_archived', 'share_link',
'allow_downloads', 'disable_right_click', 'watermark_downloads', 'watermark_text')
.first(); .first();
if (!event) { if (!event) {
@@ -78,7 +79,11 @@ router.get('/:slug/info', async (req, res) => {
is_active: event.is_active, is_active: event.is_active,
is_expired: !event.is_active || new Date(event.expires_at) < new Date(), is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
requires_password: true, requires_password: true,
color_theme: event.color_theme color_theme: event.color_theme,
allow_downloads: event.allow_downloads !== false,
disable_right_click: event.disable_right_click === true,
watermark_downloads: event.watermark_downloads === true,
watermark_text: event.watermark_text
}); });
} catch (error) { } catch (error) {
console.error('Error fetching gallery info:', error); console.error('Error fetching gallery info:', error);
@@ -125,7 +130,11 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
welcome_message: req.event.welcome_message, welcome_message: req.event.welcome_message,
color_theme: req.event.color_theme, color_theme: req.event.color_theme,
expires_at: req.event.expires_at, expires_at: req.event.expires_at,
hero_photo_id: req.event.hero_photo_id hero_photo_id: req.event.hero_photo_id,
allow_downloads: req.event.allow_downloads !== false,
disable_right_click: req.event.disable_right_click === true,
watermark_downloads: req.event.watermark_downloads === true,
watermark_text: req.event.watermark_text
}, },
categories: categories.map(cat => ({ categories: categories.map(cat => ({
id: cat.id, id: cat.id,
@@ -157,6 +166,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
try { try {
const { photoId } = req.params; const { photoId } = req.params;
// Check if downloads are allowed for this event
if (req.event.allow_downloads === false) {
return res.status(403).json({ error: 'Downloads are disabled for this gallery' });
}
const photo = await db('photos') const photo = await db('photos')
.where({ id: photoId, event_id: req.event.id }) .where({ id: photoId, event_id: req.event.id })
.first(); .first();
@@ -205,6 +219,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
// Download all photos as ZIP // Download all photos as ZIP
router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => { router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
try { try {
// Check if downloads are allowed for this event
if (req.event.allow_downloads === false) {
return res.status(403).json({ error: 'Downloads are disabled for this gallery' });
}
// Fetch photos with category information // Fetch photos with category information
const photos = await db('photos') const photos = await db('photos')
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id') .leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')