Add gallery routes
This commit is contained in:
@@ -0,0 +1,199 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { db } = require('../database/db');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
const path = require('path');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Middleware to verify gallery access
|
||||||
|
async function verifyGalleryAccess(req, res, next) {
|
||||||
|
try {
|
||||||
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'No token provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
const event = await db('events').where({ id: decoded.eventId, is_active: true }).first();
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({ error: 'Gallery not found or expired' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.event = event;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gallery info
|
||||||
|
router.get('/:slug/info', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { slug } = req.params;
|
||||||
|
|
||||||
|
const event = await db('events')
|
||||||
|
.where({ slug })
|
||||||
|
.select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({ error: 'Gallery not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...event,
|
||||||
|
is_expired: !event.is_active || new Date(event.expires_at) < new Date()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch gallery info' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all photos
|
||||||
|
router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const photos = await db('photos')
|
||||||
|
.where('event_id', req.event.id)
|
||||||
|
.orderBy('uploaded_at', 'desc');
|
||||||
|
|
||||||
|
// Log view
|
||||||
|
await db('access_logs').insert({
|
||||||
|
event_id: req.event.id,
|
||||||
|
ip_address: req.ip,
|
||||||
|
user_agent: req.headers['user-agent'],
|
||||||
|
action: 'view'
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
photos: photos.map(photo => ({
|
||||||
|
id: photo.id,
|
||||||
|
filename: photo.filename,
|
||||||
|
url: `/photos/${req.event.slug}/${photo.path}`,
|
||||||
|
thumbnail_url: photo.thumbnail_path ? `/photos/${photo.thumbnail_path}` : null,
|
||||||
|
type: photo.type,
|
||||||
|
size: photo.size_bytes,
|
||||||
|
uploaded_at: photo.uploaded_at
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch photos' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download single photo
|
||||||
|
router.get('/:slug/download/: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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, '../../../storage/events/active', req.event.slug, photo.path);
|
||||||
|
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 {
|
||||||
|
const photos = await db('photos').where('event_id', req.event.id);
|
||||||
|
|
||||||
|
if (photos.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'No photos found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Add photos to archive
|
||||||
|
for (const photo of photos) {
|
||||||
|
const filePath = path.join(__dirname, '../../../storage/events/active', req.event.slug, photo.path);
|
||||||
|
archive.file(filePath, { name: photo.path });
|
||||||
|
}
|
||||||
|
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user