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
+34 -5
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const router = express.Router();
const bcrypt = require('bcrypt');
@@ -12,7 +13,6 @@ const { queueEmail } = require('../services/emailProcessor');
const { escapeLikePattern } = require('../utils/sqlSecurity');
// formatDate import removed - dates are formatted by email processor
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
const { formatBoolean } = require('../utils/dbCompat');
// Create new event
router.post('/', adminAuth, [
@@ -27,7 +27,11 @@ router.post('/', adminAuth, [
body('color_theme').optional().trim(),
body('allow_user_uploads').optional().isBoolean().toBoolean(),
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) => {
try {
console.log('Create event request body:', req.body);
@@ -49,9 +53,26 @@ router.post('/', adminAuth, [
color_theme = null,
expiration_days = 30,
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;
// 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
const passwordValidation = await validatePasswordInContext(password, 'gallery', {
eventName: event_name
@@ -121,7 +142,11 @@ router.post('/', adminAuth, [
expires_at: expires_at.toISOString(),
created_at: new Date().toISOString(),
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');
// 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
const num = Number(value);
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) => {
try {
const errors = validationResult(req);
+22 -3
View File
@@ -46,7 +46,8 @@ router.get('/:slug/info', async (req, res) => {
const event = await db('events')
.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();
if (!event) {
@@ -78,7 +79,11 @@ router.get('/:slug/info', async (req, res) => {
is_active: event.is_active,
is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
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) {
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,
color_theme: req.event.color_theme,
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 => ({
id: cat.id,
@@ -157,6 +166,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
try {
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')
.where({ id: photoId, event_id: req.event.id })
.first();
@@ -205,6 +219,11 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
// Download all photos as ZIP
router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
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
const photos = await db('photos')
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')