- 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:
@@ -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');
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user