41857ec499
Implemented Feature Request 1 from github.com/the-luap/picpeak/issues/17: - Added filter functionality to display only liked or favorited photos - Integrated feedback filter directly into PhotoFilterBar component - Implemented responsive design with proper mobile/tablet/desktop layouts - Filter only shows when feedback is enabled for the gallery - Added proper count display for liked and favorited photos Improvements: - Fixed responsive breakpoints (mobile <768px, tablet 768-1023px, desktop ≥1024px) - Feedback filter shows inline with categories on desktop with vertical divider - On mobile/tablet, filter appears below categories to prevent layout issues - Added horizontal scrolling for category buttons to prevent cut-off Code cleanup: - Removed all debug console.log statements from production code - Removed test route from backend gallery.js - Cleaned up unnecessary logging in frontend components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
658 lines
22 KiB
JavaScript
658 lines
22 KiB
JavaScript
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const { db } = require('../database/db');
|
|
const { formatBoolean } = require('../utils/dbCompat');
|
|
const archiver = require('archiver');
|
|
const path = require('path');
|
|
const router = express.Router();
|
|
const watermarkService = require('../services/watermarkService');
|
|
const { verifyGalleryAccess } = require('../middleware/gallery');
|
|
const secureImageService = require('../services/secureImageService');
|
|
const secureImageMiddleware = require('../middleware/secureImageMiddleware');
|
|
const logger = require('../utils/logger');
|
|
|
|
// Get storage path from environment or default
|
|
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
|
|
|
// Verify share token
|
|
router.get('/:slug/verify-token/:token', async (req, res) => {
|
|
try {
|
|
const { slug, token } = req.params;
|
|
|
|
const event = await db('events')
|
|
.where({ share_link: slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) })
|
|
.select('id', 'share_link')
|
|
.first();
|
|
|
|
if (!event) {
|
|
return res.status(404).json({ error: 'Gallery not found' });
|
|
}
|
|
|
|
// Extract token from share link and verify
|
|
const expectedToken = event.share_link.split('/').pop();
|
|
if (token !== expectedToken) {
|
|
return res.status(404).json({ error: 'Invalid gallery link' });
|
|
}
|
|
|
|
res.json({ valid: true });
|
|
} catch (error) {
|
|
console.error('Error verifying token:', error);
|
|
res.status(500).json({ error: 'Failed to verify token', details: error.message });
|
|
}
|
|
});
|
|
|
|
// Get gallery info (with optional token verification)
|
|
router.get('/:slug/info', async (req, res) => {
|
|
try {
|
|
const { slug } = req.params;
|
|
const { token } = req.query;
|
|
|
|
const event = await db('events')
|
|
.where({ slug: slug })
|
|
.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) {
|
|
return res.status(404).json({ error: 'Gallery not found' });
|
|
}
|
|
|
|
// Check if event is archived
|
|
if (event.is_archived) {
|
|
return res.status(404).json({ error: 'Gallery has been archived and is no longer available' });
|
|
}
|
|
|
|
// If token provided, verify it matches the share link
|
|
if (token) {
|
|
let expectedToken = event.share_link;
|
|
// Handle both formats: full URL or just token
|
|
if (event.share_link && event.share_link.includes('/')) {
|
|
expectedToken = event.share_link.split('/').pop();
|
|
}
|
|
if (token !== expectedToken) {
|
|
return res.status(404).json({ error: 'Invalid gallery link' });
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
event_name: event.event_name,
|
|
event_type: event.event_type,
|
|
event_date: event.event_date,
|
|
expires_at: event.expires_at,
|
|
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,
|
|
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);
|
|
res.status(500).json({ error: 'Failed to fetch gallery info', details: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all photos
|
|
router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|
try {
|
|
// Get filter parameters from query
|
|
const { filter, guest_id } = req.query;
|
|
const feedbackService = require('../services/feedbackService');
|
|
|
|
// First get all photos
|
|
let photos = await db('photos')
|
|
.where('photos.event_id', req.event.id)
|
|
.select('photos.*')
|
|
.orderBy('photos.uploaded_at', 'desc');
|
|
|
|
// Apply filtering if requested
|
|
if (filter && guest_id) {
|
|
let filters = {};
|
|
|
|
// Parse filter parameter
|
|
if (filter === 'liked') {
|
|
filters.liked = true;
|
|
} else if (filter === 'favorited') {
|
|
filters.favorited = true;
|
|
} else if (filter === 'liked,favorited' || filter === 'favorited,liked') {
|
|
filters.liked = true;
|
|
filters.favorited = true;
|
|
filters.operator = 'OR';
|
|
}
|
|
|
|
// Get filtered photo IDs
|
|
const filteredPhotoIds = await feedbackService.getFilteredPhotos(
|
|
req.event.id,
|
|
guest_id,
|
|
filters
|
|
);
|
|
|
|
// Filter photos to only include those with feedback
|
|
photos = photos.filter(photo => filteredPhotoIds.includes(photo.id));
|
|
}
|
|
|
|
// Then get comment counts separately
|
|
const commentCounts = await db('photo_feedback')
|
|
.whereIn('photo_id', photos.map(p => p.id))
|
|
.where('feedback_type', 'comment')
|
|
.where('is_approved', true)
|
|
.where('is_hidden', false)
|
|
.groupBy('photo_id')
|
|
.select('photo_id', db.raw('COUNT(*) as comment_count'));
|
|
|
|
// Create a map for quick lookup
|
|
const commentMap = {};
|
|
commentCounts.forEach(c => {
|
|
commentMap[c.photo_id] = parseInt(c.comment_count);
|
|
});
|
|
|
|
// Get distinct photo types for this event
|
|
const categoryResults = await db('photos')
|
|
.where('event_id', req.event.id)
|
|
.select('type')
|
|
.distinct('type')
|
|
.orderBy('type', 'asc');
|
|
|
|
// Convert types to category-like objects
|
|
const categories = categoryResults.map(result => ({
|
|
id: result.type,
|
|
name: result.type === 'individual' ? 'Individual Photos' : 'Collages',
|
|
slug: result.type,
|
|
is_global: false
|
|
}));
|
|
|
|
// Log view
|
|
await db('access_logs').insert({
|
|
event_id: req.event.id,
|
|
ip_address: req.ip,
|
|
user_agent: req.headers['user-agent'],
|
|
action: 'view'
|
|
});
|
|
|
|
// Include protection settings in response
|
|
const protectionSettings = {
|
|
protection_level: req.event.protection_level || 'standard',
|
|
image_quality: req.event.image_quality || 85,
|
|
use_canvas_rendering: req.event.use_canvas_rendering === true,
|
|
fragmentation_level: req.event.fragmentation_level || 3,
|
|
overlay_protection: req.event.overlay_protection !== false
|
|
};
|
|
|
|
|
|
res.json({
|
|
event: {
|
|
id: req.event.id,
|
|
event_name: req.event.event_name,
|
|
event_type: req.event.event_type,
|
|
event_date: req.event.event_date,
|
|
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,
|
|
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,
|
|
...protectionSettings
|
|
},
|
|
categories: categories,
|
|
photos: photos.map(photo => {
|
|
const useJwtUrl = (protectionSettings.protection_level === 'basic' || protectionSettings.protection_level === 'standard');
|
|
const photoUrl = useJwtUrl ?
|
|
`/api/gallery/${req.params.slug}/photo/${photo.id}` :
|
|
`/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`;
|
|
|
|
return {
|
|
id: photo.id,
|
|
filename: photo.filename,
|
|
url: photoUrl,
|
|
thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}` : null,
|
|
secure_url_template: `/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`,
|
|
download_url_template: `/api/secure-images/${req.params.slug}/secure-download/${photo.id}/{{token}}`,
|
|
type: photo.type,
|
|
category_id: photo.type,
|
|
category_name: photo.type === 'individual' ? 'Individual Photos' : 'Collages',
|
|
category_slug: photo.type,
|
|
size: photo.size_bytes,
|
|
uploaded_at: photo.uploaded_at,
|
|
// Fixed: Use the calculated useJwtUrl variable instead of recalculating
|
|
requires_token: !useJwtUrl,
|
|
// Feedback data
|
|
has_feedback: (commentMap[photo.id] > 0 || photo.average_rating > 0 || photo.like_count > 0),
|
|
average_rating: photo.average_rating || 0,
|
|
comment_count: commentMap[photo.id] || 0,
|
|
like_count: photo.like_count || 0,
|
|
favorite_count: photo.favorite_count || 0
|
|
};
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching photos:', error);
|
|
res.status(500).json({ error: 'Failed to fetch photos', details: error.message });
|
|
}
|
|
});
|
|
|
|
// Download single photo
|
|
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();
|
|
|
|
if (!photo) {
|
|
return res.status(404).json({ error: 'Photo not found' });
|
|
}
|
|
|
|
// Update download count
|
|
await db('photos').where('id', photoId).increment('download_count', 1);
|
|
|
|
// Log download
|
|
await db('access_logs').insert({
|
|
event_id: req.event.id,
|
|
ip_address: req.ip,
|
|
user_agent: req.headers['user-agent'],
|
|
action: 'download',
|
|
photo_id: photoId
|
|
});
|
|
|
|
// Photo path should be in storage/events/active directory
|
|
// Handle both legacy paths (just slug/filename) and new paths (events/active/slug/filename)
|
|
const storagePath = getStoragePath();
|
|
let filePath;
|
|
if (photo.path.startsWith('events/active/')) {
|
|
// New format: path already includes events/active/ prefix
|
|
filePath = path.join(storagePath, photo.path);
|
|
} else {
|
|
// Legacy format: path is just slug/filename
|
|
filePath = path.join(storagePath, 'events/active', photo.path);
|
|
}
|
|
|
|
// Get watermark settings
|
|
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
|
|
|
if (watermarkSettings && watermarkSettings.enabled) {
|
|
// Apply watermark and send
|
|
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
|
|
|
res.set({
|
|
'Content-Type': photo.mime_type || 'image/jpeg',
|
|
'Content-Disposition': `attachment; filename="${photo.filename}"`,
|
|
'Content-Length': watermarkedBuffer.length
|
|
});
|
|
|
|
res.send(watermarkedBuffer);
|
|
} else {
|
|
// Send original file
|
|
res.download(filePath, photo.filename);
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to download photo' });
|
|
}
|
|
});
|
|
|
|
// 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
|
|
const photos = await db('photos')
|
|
.where('photos.event_id', req.event.id)
|
|
.select('photos.*')
|
|
.orderBy('photos.type', 'asc')
|
|
.orderBy('photos.uploaded_at', 'desc');
|
|
|
|
if (photos.length === 0) {
|
|
return res.status(404).json({ error: 'No photos found' });
|
|
}
|
|
|
|
// Count unique types
|
|
const uniqueTypes = new Set(photos.map(p => p.type)).size;
|
|
const hasMultipleTypes = uniqueTypes > 1;
|
|
|
|
res.setHeader('Content-Type', 'application/zip');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${req.event.slug}.zip"`);
|
|
|
|
const archive = archiver('zip', { zlib: { level: 5 } });
|
|
archive.on('error', (err) => {
|
|
throw err;
|
|
});
|
|
|
|
archive.pipe(res);
|
|
|
|
// Get watermark settings
|
|
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
|
|
|
// Add photos to archive
|
|
for (const photo of photos) {
|
|
// Photo path should be in storage/events/active directory
|
|
// Handle both legacy paths (just slug/filename) and new paths (events/active/slug/filename)
|
|
const storagePath = getStoragePath();
|
|
let filePath;
|
|
if (photo.path.startsWith('events/active/')) {
|
|
// New format: path already includes events/active/ prefix
|
|
filePath = path.join(storagePath, photo.path);
|
|
} else {
|
|
// Legacy format: path is just slug/filename
|
|
filePath = path.join(storagePath, 'events/active', photo.path);
|
|
}
|
|
|
|
// Determine the file name in the archive
|
|
let archiveName;
|
|
if (hasMultipleTypes) {
|
|
// Use photo type as folder
|
|
const folderName = photo.type === 'individual' ? 'Individual Photos' : 'Collages';
|
|
archiveName = path.join(folderName, photo.filename);
|
|
} else {
|
|
// No folders, just the filename
|
|
archiveName = photo.filename;
|
|
}
|
|
|
|
if (watermarkSettings && watermarkSettings.enabled) {
|
|
// Apply watermark
|
|
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
|
archive.append(watermarkedBuffer, { name: archiveName });
|
|
} else {
|
|
// Add original file
|
|
archive.file(filePath, { name: archiveName });
|
|
}
|
|
}
|
|
|
|
await archive.finalize();
|
|
|
|
// Log bulk download
|
|
await db('access_logs').insert({
|
|
event_id: req.event.id,
|
|
ip_address: req.ip,
|
|
user_agent: req.headers['user-agent'],
|
|
action: 'download_all'
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to create download archive' });
|
|
}
|
|
});
|
|
|
|
|
|
// View single photo (with watermark if enabled)
|
|
router.get('/:slug/photo/:photoId',
|
|
verifyGalleryAccess,
|
|
async (req, res) => {
|
|
try {
|
|
const { photoId } = req.params;
|
|
|
|
const photo = await db('photos')
|
|
.where({ id: photoId, event_id: req.event.id })
|
|
.first();
|
|
|
|
|
|
if (!photo) {
|
|
return res.status(404).json({ error: 'Photo not found' });
|
|
}
|
|
|
|
// Check protection level - basic and standard protection allow direct JWT access
|
|
const protectionLevel = req.event.protection_level || 'standard';
|
|
|
|
if (protectionLevel === 'enhanced' || protectionLevel === 'maximum') {
|
|
// For enhanced/maximum protection, redirect to secure endpoint
|
|
return res.status(302).json({
|
|
error: 'Secure access required',
|
|
secureEndpoint: `/api/secure-images/${req.params.slug}/generate-token`,
|
|
photoId: photoId
|
|
});
|
|
}
|
|
|
|
// Photo path should be in storage/events/active directory
|
|
// Handle both legacy paths (just slug/filename) and new paths (events/active/slug/filename)
|
|
const storagePath = getStoragePath();
|
|
|
|
let filePath;
|
|
if (photo.path.startsWith('events/active/')) {
|
|
// New format: path already includes events/active/ prefix
|
|
filePath = path.join(storagePath, photo.path);
|
|
} else {
|
|
// Legacy format: path is just slug/filename
|
|
filePath = path.join(storagePath, 'events/active', photo.path);
|
|
}
|
|
|
|
|
|
// Log access - temporarily disabled for debugging
|
|
// await secureImageService.logImageAccess(
|
|
// photoId,
|
|
// req.event.id,
|
|
// req.clientInfo,
|
|
// 'view_basic'
|
|
// );
|
|
|
|
// Get watermark settings
|
|
const watermarkSettings = await watermarkService.getWatermarkSettings();
|
|
|
|
if (watermarkSettings && watermarkSettings.enabled) {
|
|
// Apply watermark and send
|
|
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
|
|
|
|
res.set({
|
|
'Content-Type': photo.mime_type || 'image/jpeg',
|
|
'Cache-Control': 'private, max-age=1800', // Cache for 30 minutes
|
|
'X-Protection-Level': 'basic'
|
|
});
|
|
|
|
res.send(watermarkedBuffer);
|
|
} else {
|
|
// Send original file with basic protection headers
|
|
res.set({
|
|
'Cache-Control': 'private, max-age=1800',
|
|
'X-Protection-Level': 'basic'
|
|
});
|
|
// Ensure absolute path for res.sendFile
|
|
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
res.sendFile(absolutePath);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error serving photo:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
photoId: req.params.photoId,
|
|
eventId: req.event?.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to serve photo', details: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Serve thumbnail
|
|
router.get('/:slug/thumbnail/:photoId',
|
|
verifyGalleryAccess,
|
|
async (req, res) => {
|
|
try {
|
|
const { photoId } = req.params;
|
|
|
|
const photo = await db('photos')
|
|
.where({ id: photoId, event_id: req.event.id })
|
|
.first();
|
|
|
|
if (!photo || !photo.thumbnail_path) {
|
|
return res.status(404).json({ error: 'Thumbnail not found' });
|
|
}
|
|
|
|
const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
|
|
|
|
// Check if file exists
|
|
const fs = require('fs').promises;
|
|
try {
|
|
await fs.access(thumbPath);
|
|
} catch (error) {
|
|
return res.status(404).json({ error: 'Thumbnail file not found' });
|
|
}
|
|
|
|
// Log thumbnail access
|
|
await secureImageService.logImageAccess(
|
|
photoId,
|
|
req.event.id,
|
|
req.clientInfo,
|
|
'thumbnail'
|
|
);
|
|
|
|
// Set appropriate headers with enhanced security
|
|
res.set({
|
|
'Content-Type': 'image/jpeg',
|
|
'Cache-Control': 'private, max-age=1800', // Reduced cache time
|
|
'Cross-Origin-Resource-Policy': 'cross-origin',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'X-Protected-Thumbnail': 'true'
|
|
});
|
|
|
|
// Send file
|
|
res.sendFile(path.resolve(thumbPath));
|
|
} catch (error) {
|
|
logger.error('Error serving thumbnail:', {
|
|
error: error.message,
|
|
photoId: req.params.photoId,
|
|
eventId: req.event?.id
|
|
});
|
|
res.status(500).json({ error: 'Failed to serve thumbnail' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get feedback settings for gallery
|
|
router.get('/:slug/feedback-settings', verifyGalleryAccess, async (req, res) => {
|
|
try {
|
|
const feedbackService = require('../services/feedbackService');
|
|
const settings = await feedbackService.getEventFeedbackSettings(req.event.id);
|
|
|
|
res.json({
|
|
feedback_enabled: settings.feedback_enabled || false,
|
|
allow_ratings: settings.allow_ratings,
|
|
allow_likes: settings.allow_likes,
|
|
allow_comments: settings.allow_comments,
|
|
allow_favorites: settings.allow_favorites,
|
|
show_feedback_to_guests: settings.show_feedback_to_guests
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching feedback settings:', error);
|
|
res.status(500).json({ error: 'Failed to fetch feedback settings' });
|
|
}
|
|
});
|
|
|
|
// Get photo stats
|
|
router.get('/:slug/stats', verifyGalleryAccess, async (req, res) => {
|
|
try {
|
|
const totalPhotos = await db('photos')
|
|
.where('event_id', req.event.id)
|
|
.count('id as count')
|
|
.first();
|
|
|
|
const totalViews = await db('access_logs')
|
|
.where('event_id', req.event.id)
|
|
.where('action', 'view')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
const totalDownloads = await db('photos')
|
|
.where('event_id', req.event.id)
|
|
.sum('download_count as total')
|
|
.first();
|
|
|
|
const uniqueVisitors = await db('access_logs')
|
|
.where('event_id', req.event.id)
|
|
.countDistinct('ip_address as count')
|
|
.first();
|
|
|
|
res.json({
|
|
total_photos: totalPhotos.count,
|
|
total_views: totalViews.count,
|
|
total_downloads: totalDownloads.total || 0,
|
|
unique_visitors: uniqueVisitors.count
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
}
|
|
});
|
|
|
|
// User photo upload endpoint
|
|
router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
|
|
try {
|
|
const eventId = parseInt(req.params.eventId);
|
|
|
|
// Verify the event matches the token
|
|
if (req.event.id !== eventId) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
// Check if user uploads are allowed
|
|
if (!req.event.allow_user_uploads) {
|
|
return res.status(403).json({ error: 'User uploads are not allowed for this event' });
|
|
}
|
|
|
|
// Import multer and photo processing
|
|
const multer = require('multer');
|
|
const upload = multer({
|
|
dest: '/tmp/uploads/',
|
|
limits: {
|
|
fileSize: 50 * 1024 * 1024, // 50MB
|
|
files: 10 // Max 10 files at once
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type'));
|
|
}
|
|
}
|
|
}).array('photos', 10);
|
|
|
|
// Handle upload
|
|
upload(req, res, async (err) => {
|
|
if (err) {
|
|
console.error('Upload error:', err);
|
|
return res.status(400).json({ error: err.message });
|
|
}
|
|
|
|
if (!req.files || req.files.length === 0) {
|
|
return res.status(400).json({ error: 'No files uploaded' });
|
|
}
|
|
|
|
const { processUploadedPhotos } = require('../services/photoProcessor');
|
|
const categoryId = req.body.category_id || req.event.upload_category_id || null;
|
|
|
|
try {
|
|
// Process uploaded photos
|
|
const results = await processUploadedPhotos(req.files, eventId, 'user', categoryId);
|
|
|
|
// Clean up temp files
|
|
const fs = require('fs').promises;
|
|
for (const file of req.files) {
|
|
await fs.unlink(file.path).catch(console.error);
|
|
}
|
|
|
|
res.json({
|
|
message: 'Photos uploaded successfully',
|
|
count: results.length,
|
|
photos: results
|
|
});
|
|
} catch (processError) {
|
|
console.error('Photo processing error:', processError);
|
|
res.status(500).json({ error: 'Failed to process photos' });
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload route error:', error);
|
|
res.status(500).json({ error: 'Failed to upload photos' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|