Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2551dc0ad | |||
| 32821934e6 | |||
| b9c28e52cd | |||
| c94b6268cf | |||
| 439c743fd1 | |||
| 74144f1fc6 | |||
| 99a0376657 | |||
| 21b1e79672 | |||
| cfaee103b6 | |||
| c0e346992d | |||
| 04f45a16c9 | |||
| efad1da74d |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Storage Management Scripts
|
||||||
|
|
||||||
|
## check-storage.js
|
||||||
|
|
||||||
|
Checks the storage directory structure and verifies that photo files exist.
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
```bash
|
||||||
|
# Check overall storage structure
|
||||||
|
node scripts/check-storage.js
|
||||||
|
|
||||||
|
# Check specific event
|
||||||
|
node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it checks:
|
||||||
|
- Storage directory structure and permissions
|
||||||
|
- Event directories
|
||||||
|
- Photo files existence
|
||||||
|
- Thumbnail files existence
|
||||||
|
- Database vs filesystem consistency
|
||||||
|
|
||||||
|
## regenerate-thumbnails.js
|
||||||
|
|
||||||
|
Regenerates missing thumbnails for photos in the database.
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
```bash
|
||||||
|
# Regenerate all missing thumbnails
|
||||||
|
node scripts/regenerate-thumbnails.js
|
||||||
|
|
||||||
|
# Regenerate thumbnails for specific event (by ID)
|
||||||
|
node scripts/regenerate-thumbnails.js 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it does:
|
||||||
|
- Scans photos in database
|
||||||
|
- Checks if thumbnails exist
|
||||||
|
- Generates missing thumbnails using Sharp
|
||||||
|
- Updates database with thumbnail paths
|
||||||
|
- Reports success/error statistics
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
- Node.js environment
|
||||||
|
- Database access
|
||||||
|
- Write permissions to storage directory
|
||||||
|
- Sharp library installed
|
||||||
|
|
||||||
|
## Production Usage
|
||||||
|
|
||||||
|
On your production server:
|
||||||
|
|
||||||
|
1. First, check the storage structure:
|
||||||
|
```bash
|
||||||
|
cd /path/to/picpeak/backend
|
||||||
|
NODE_ENV=production node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If thumbnails are missing, regenerate them:
|
||||||
|
```bash
|
||||||
|
NODE_ENV=production node scripts/regenerate-thumbnails.js 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Replace `2` with the actual event ID from your database.
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to check storage directory structure and verify files
|
||||||
|
* Usage: node scripts/check-storage.js [eventSlug]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const { db } = require('../src/database/db');
|
||||||
|
|
||||||
|
const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||||
|
|
||||||
|
async function checkDirectory(dirPath, description) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
const stats = await fs.stat(dirPath);
|
||||||
|
const files = await fs.readdir(dirPath);
|
||||||
|
console.log(`✓ ${description}: ${dirPath}`);
|
||||||
|
console.log(` - Files/Folders: ${files.length}`);
|
||||||
|
console.log(` - Permissions: ${(stats.mode & parseInt('777', 8)).toString(8)}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`✗ ${description}: ${dirPath} - ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkStorageStructure(eventSlug = null) {
|
||||||
|
console.log('Checking storage structure...');
|
||||||
|
console.log(`Storage base path: ${STORAGE_PATH}\n`);
|
||||||
|
|
||||||
|
// Check main directories
|
||||||
|
await checkDirectory(STORAGE_PATH, 'Storage root');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events'), 'Events directory');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events/active'), 'Active events');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events/archived'), 'Archived events');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'thumbnails'), 'Thumbnails');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'uploads'), 'Uploads');
|
||||||
|
|
||||||
|
console.log('\n---\n');
|
||||||
|
|
||||||
|
// If event slug provided, check specific event
|
||||||
|
if (eventSlug) {
|
||||||
|
console.log(`Checking specific event: ${eventSlug}`);
|
||||||
|
|
||||||
|
const event = await db('events').where('slug', eventSlug).first();
|
||||||
|
if (!event) {
|
||||||
|
console.log(`✗ Event not found in database: ${eventSlug}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Event found in database:`);
|
||||||
|
console.log(` - ID: ${event.id}`);
|
||||||
|
console.log(` - Name: ${event.event_name}`);
|
||||||
|
console.log(` - Active: ${event.is_active}`);
|
||||||
|
console.log(` - Archived: ${event.is_archived}`);
|
||||||
|
|
||||||
|
// Check event directory
|
||||||
|
const eventDir = path.join(STORAGE_PATH, 'events/active', eventSlug);
|
||||||
|
const eventExists = await checkDirectory(eventDir, 'Event directory');
|
||||||
|
|
||||||
|
if (eventExists) {
|
||||||
|
const files = await fs.readdir(eventDir);
|
||||||
|
console.log(` - Photo files: ${files.filter(f => /\.(jpg|jpeg|png|gif)$/i.test(f)).length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check photos in database
|
||||||
|
const photos = await db('photos').where('event_id', event.id).select('id', 'filename', 'path', 'thumbnail_path');
|
||||||
|
console.log(`\nDatabase photos: ${photos.length}`);
|
||||||
|
|
||||||
|
// Check if photo files exist
|
||||||
|
let existingPhotos = 0;
|
||||||
|
let missingPhotos = 0;
|
||||||
|
let existingThumbnails = 0;
|
||||||
|
let missingThumbnails = 0;
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
const photoPath = path.join(STORAGE_PATH, 'events/active', photo.path);
|
||||||
|
try {
|
||||||
|
await fs.access(photoPath);
|
||||||
|
existingPhotos++;
|
||||||
|
} catch {
|
||||||
|
missingPhotos++;
|
||||||
|
console.log(` ✗ Missing photo: ${photo.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photo.thumbnail_path) {
|
||||||
|
const thumbPath = path.join(STORAGE_PATH, photo.thumbnail_path.replace(/^\//, ''));
|
||||||
|
try {
|
||||||
|
await fs.access(thumbPath);
|
||||||
|
existingThumbnails++;
|
||||||
|
} catch {
|
||||||
|
missingThumbnails++;
|
||||||
|
console.log(` ✗ Missing thumbnail: ${photo.thumbnail_path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nFile check summary:`);
|
||||||
|
console.log(` - Photos: ${existingPhotos} exist, ${missingPhotos} missing`);
|
||||||
|
console.log(` - Thumbnails: ${existingThumbnails} exist, ${missingThumbnails} missing`);
|
||||||
|
} else {
|
||||||
|
// List all event directories
|
||||||
|
try {
|
||||||
|
const activeDir = path.join(STORAGE_PATH, 'events/active');
|
||||||
|
const eventDirs = await fs.readdir(activeDir);
|
||||||
|
console.log(`Active event directories: ${eventDirs.length}`);
|
||||||
|
for (const dir of eventDirs.slice(0, 10)) {
|
||||||
|
console.log(` - ${dir}`);
|
||||||
|
}
|
||||||
|
if (eventDirs.length > 10) {
|
||||||
|
console.log(` ... and ${eventDirs.length - 10} more`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not list event directories:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const eventSlug = process.argv[2] || null;
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
checkStorageStructure(eventSlug).then(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
console.log('\nStorage check complete');
|
||||||
|
}).catch(async error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
await db.destroy();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Executable
+141
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to regenerate missing thumbnails for photos in the database
|
||||||
|
* Usage: node scripts/regenerate-thumbnails.js [eventId]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const { db } = require('../src/database/db');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||||
|
const THUMBNAILS_DIR = path.join(STORAGE_PATH, 'thumbnails');
|
||||||
|
|
||||||
|
async function ensureDirectoryExists(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
} catch {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
console.log(`Created directory: ${dirPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateThumbnail(photoPath, thumbnailPath) {
|
||||||
|
try {
|
||||||
|
await sharp(photoPath)
|
||||||
|
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'center'
|
||||||
|
})
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.toFile(thumbnailPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to generate thumbnail for ${photoPath}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateThumbnails(eventId = null) {
|
||||||
|
try {
|
||||||
|
console.log('Starting thumbnail regeneration...');
|
||||||
|
console.log(`Storage path: ${STORAGE_PATH}`);
|
||||||
|
console.log(`Thumbnails directory: ${THUMBNAILS_DIR}`);
|
||||||
|
|
||||||
|
// Ensure thumbnails directory exists
|
||||||
|
await ensureDirectoryExists(THUMBNAILS_DIR);
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
let query = db('photos')
|
||||||
|
.join('events', 'photos.event_id', 'events.id')
|
||||||
|
.select(
|
||||||
|
'photos.id',
|
||||||
|
'photos.filename',
|
||||||
|
'photos.path',
|
||||||
|
'photos.thumbnail_path',
|
||||||
|
'events.slug as event_slug'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventId) {
|
||||||
|
query = query.where('photos.event_id', eventId);
|
||||||
|
console.log(`Filtering for event ID: ${eventId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const photos = await query;
|
||||||
|
console.log(`Found ${photos.length} photos to process`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let skipCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
const photoPath = path.join(STORAGE_PATH, 'events/active', photo.path);
|
||||||
|
const thumbnailFilename = `thumb_${photo.filename}`;
|
||||||
|
const thumbnailPath = path.join(THUMBNAILS_DIR, thumbnailFilename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if photo file exists
|
||||||
|
await fs.access(photoPath);
|
||||||
|
|
||||||
|
// Check if thumbnail already exists
|
||||||
|
try {
|
||||||
|
await fs.access(thumbnailPath);
|
||||||
|
console.log(`Thumbnail already exists for ${photo.filename}, skipping...`);
|
||||||
|
skipCount++;
|
||||||
|
continue;
|
||||||
|
} catch {
|
||||||
|
// Thumbnail doesn't exist, generate it
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generating thumbnail for ${photo.filename}...`);
|
||||||
|
const success = await generateThumbnail(photoPath, thumbnailPath);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Update database with thumbnail path
|
||||||
|
await db('photos')
|
||||||
|
.where('id', photo.id)
|
||||||
|
.update({
|
||||||
|
thumbnail_path: `thumbnails/${thumbnailFilename}`
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
console.log(`✓ Generated thumbnail for ${photo.filename}`);
|
||||||
|
} else {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`✗ Photo file not found: ${photoPath}`);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nThumbnail regeneration complete!');
|
||||||
|
console.log(`- Successfully generated: ${successCount}`);
|
||||||
|
console.log(`- Skipped (already exist): ${skipCount}`);
|
||||||
|
console.log(`- Errors: ${errorCount}`);
|
||||||
|
console.log(`- Total processed: ${photos.length}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during thumbnail regeneration:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const eventId = process.argv[2] ? parseInt(process.argv[2]) : null;
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
regenerateThumbnails(eventId).then(() => {
|
||||||
|
console.log('Script completed successfully');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Script failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+6
-3
@@ -146,14 +146,17 @@ const setCorsHeaders = (req, res, next) => {
|
|||||||
// Import secure static middleware
|
// Import secure static middleware
|
||||||
const secureStatic = require('./src/middleware/secureStatic');
|
const secureStatic = require('./src/middleware/secureStatic');
|
||||||
|
|
||||||
|
// Get storage path from environment or use default
|
||||||
|
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../storage');
|
||||||
|
|
||||||
// Static file serving for photos (protected)
|
// Static file serving for photos (protected)
|
||||||
app.use('/photos', require('./src/middleware/photoAuth'), setCorsHeaders, secureStatic(path.join(__dirname, 'storage/events/active')));
|
app.use('/photos', require('./src/middleware/photoAuth'), setCorsHeaders, secureStatic(path.join(storagePath, 'events/active')));
|
||||||
|
|
||||||
// Static file serving for thumbnails (protected)
|
// Static file serving for thumbnails (protected)
|
||||||
app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, secureStatic(path.join(__dirname, 'storage/thumbnails')));
|
app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, secureStatic(path.join(storagePath, 'thumbnails')));
|
||||||
|
|
||||||
// Static file serving for uploads (public - logos, favicons)
|
// Static file serving for uploads (public - logos, favicons)
|
||||||
app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads')));
|
app.use('/uploads', setCorsHeaders, secureStatic(path.join(storagePath, 'uploads')));
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ async function verifyGalleryAccess(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
const event = await db('events').where({ id: decoded.eventId, is_active: formatBoolean(true) }).first();
|
const event = await db('events')
|
||||||
|
.where({
|
||||||
|
id: decoded.eventId,
|
||||||
|
is_active: formatBoolean(true),
|
||||||
|
is_archived: formatBoolean(false)
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return res.status(404).json({ error: 'Gallery not found or expired' });
|
return res.status(404).json({ error: 'Gallery not found or expired' });
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ async function photoAuth(req, res, next) {
|
|||||||
// Extract event slug from the path
|
// Extract event slug from the path
|
||||||
let eventSlug;
|
let eventSlug;
|
||||||
|
|
||||||
|
console.log('PhotoAuth middleware - path:', req.path);
|
||||||
|
|
||||||
// For thumbnails, we need to parse the filename to get the event info
|
// For thumbnails, we need to parse the filename to get the event info
|
||||||
if (req.path.startsWith('/thumb_')) {
|
if (req.path.startsWith('/thumb_')) {
|
||||||
// For now, we'll rely on JWT token for thumbnail access
|
// For now, we'll rely on JWT token for thumbnail access
|
||||||
@@ -26,9 +28,22 @@ async function photoAuth(req, res, next) {
|
|||||||
|
|
||||||
// Check if it's a gallery token
|
// Check if it's a gallery token
|
||||||
if (decoded.type === 'gallery') {
|
if (decoded.type === 'gallery') {
|
||||||
// For thumbnails, we accept any valid gallery token
|
// For thumbnails, we need to verify the token is for a valid event
|
||||||
if (!eventSlug) {
|
if (!eventSlug) {
|
||||||
const event = await db('events').where({ slug: decoded.eventSlug, is_active: formatBoolean(true) }).first();
|
// Extract event ID from the decoded token
|
||||||
|
if (decoded.eventId) {
|
||||||
|
const event = await db('events')
|
||||||
|
.where({ id: decoded.eventId, is_active: formatBoolean(true) })
|
||||||
|
.first();
|
||||||
|
if (event) {
|
||||||
|
req.event = event;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to slug
|
||||||
|
const event = await db('events')
|
||||||
|
.where({ slug: decoded.eventSlug, is_active: formatBoolean(true) })
|
||||||
|
.first();
|
||||||
if (event) {
|
if (event) {
|
||||||
req.event = event;
|
req.event = event;
|
||||||
return next();
|
return next();
|
||||||
@@ -36,7 +51,9 @@ async function photoAuth(req, res, next) {
|
|||||||
}
|
}
|
||||||
// For regular photos, check if token matches the event
|
// For regular photos, check if token matches the event
|
||||||
else if (decoded.eventSlug === eventSlug) {
|
else if (decoded.eventSlug === eventSlug) {
|
||||||
const event = await db('events').where({ slug: eventSlug, is_active: formatBoolean(true) }).first();
|
const event = await db('events')
|
||||||
|
.where({ slug: eventSlug, is_active: formatBoolean(true) })
|
||||||
|
.first();
|
||||||
if (event) {
|
if (event) {
|
||||||
req.event = event;
|
req.event = event;
|
||||||
return next();
|
return next();
|
||||||
@@ -46,18 +63,12 @@ async function photoAuth(req, res, next) {
|
|||||||
|
|
||||||
// Check if it's an admin token (admins can view all photos)
|
// Check if it's an admin token (admins can view all photos)
|
||||||
if (decoded.type === 'admin') {
|
if (decoded.type === 'admin') {
|
||||||
if (!eventSlug) {
|
// For both thumbnails and photos with admin token, allow access
|
||||||
// For thumbnails with admin token, allow access
|
return next();
|
||||||
return next();
|
|
||||||
}
|
|
||||||
const event = await db('events').where({ slug: eventSlug }).first();
|
|
||||||
if (event) {
|
|
||||||
req.event = event;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Token invalid, fall through to password check
|
// Token invalid, fall through to password check
|
||||||
|
console.error('JWT verification failed:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +79,8 @@ async function photoAuth(req, res, next) {
|
|||||||
return res.status(401).json({ error: 'Authentication required' });
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no eventSlug (thumbnails), we require JWT token
|
// If no eventSlug (thumbnails), and we don't have valid auth yet, deny access
|
||||||
if (!eventSlug) {
|
if (!eventSlug && !password) {
|
||||||
return res.status(401).json({ error: 'Authentication required for thumbnails' });
|
return res.status(401).json({ error: 'Authentication required for thumbnails' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -552,8 +552,8 @@ router.get('/:eventId/photos', adminAuth, async (req, res) => {
|
|||||||
photos: photos.map(photo => ({
|
photos: photos.map(photo => ({
|
||||||
id: photo.id,
|
id: photo.id,
|
||||||
filename: photo.filename,
|
filename: photo.filename,
|
||||||
url: `/api/admin/events/${eventId}/photo/${photo.id}`,
|
url: `/admin/events/${eventId}/photo/${photo.id}`,
|
||||||
thumbnail_url: photo.thumbnail_path ? `/api/admin/events/${eventId}/thumbnail/${photo.id}` : null,
|
thumbnail_url: photo.thumbnail_path ? `/admin/events/${eventId}/thumbnail/${photo.id}` : null,
|
||||||
type: photo.type,
|
type: photo.type,
|
||||||
category_id: photo.category_id,
|
category_id: photo.category_id,
|
||||||
category_name: photo.category_name,
|
category_name: photo.category_name,
|
||||||
@@ -646,4 +646,24 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Debug endpoint to check photo existence
|
||||||
|
router.get('/:eventId/debug', adminAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { eventId } = req.params;
|
||||||
|
|
||||||
|
const event = await db('events').where({ id: eventId }).first();
|
||||||
|
const photoCount = await db('photos').where({ event_id: eventId }).count('id as count').first();
|
||||||
|
const photos = await db('photos').where({ event_id: eventId }).limit(5);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
event: event || 'Not found',
|
||||||
|
photoCount: photoCount.count,
|
||||||
|
samplePhotos: photos,
|
||||||
|
storagePath: getStoragePath()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -119,7 +119,8 @@ router.post('/gallery/verify', [
|
|||||||
color_theme: event.color_theme,
|
color_theme: event.color_theme,
|
||||||
expires_at: event.expires_at,
|
expires_at: event.expires_at,
|
||||||
allow_user_uploads: event.allow_user_uploads,
|
allow_user_uploads: event.allow_user_uploads,
|
||||||
upload_category_id: event.upload_category_id
|
upload_category_id: event.upload_category_id,
|
||||||
|
hero_photo_id: event.hero_photo_id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,35 +6,11 @@ const archiver = require('archiver');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const watermarkService = require('../services/watermarkService');
|
const watermarkService = require('../services/watermarkService');
|
||||||
|
const { verifyGalleryAccess } = require('../middleware/gallery');
|
||||||
|
|
||||||
// Get storage path from environment or default
|
// Get storage path from environment or default
|
||||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
|
|
||||||
// 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: formatBoolean(true), is_archived: formatBoolean(false) })
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
return res.status(404).json({ error: 'Gallery not found or expired' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.event = event;
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying gallery access:', error);
|
|
||||||
res.status(401).json({ error: 'Invalid token', details: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify share token
|
// Verify share token
|
||||||
router.get('/:slug/verify-token/:token', async (req, res) => {
|
router.get('/:slug/verify-token/:token', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -84,7 +60,11 @@ router.get('/:slug/info', async (req, res) => {
|
|||||||
|
|
||||||
// If token provided, verify it matches the share link
|
// If token provided, verify it matches the share link
|
||||||
if (token) {
|
if (token) {
|
||||||
const expectedToken = event.share_link.split('/').pop();
|
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) {
|
if (token !== expectedToken) {
|
||||||
return res.status(404).json({ error: 'Invalid gallery link' });
|
return res.status(404).json({ error: 'Invalid gallery link' });
|
||||||
}
|
}
|
||||||
@@ -157,7 +137,7 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|||||||
id: photo.id,
|
id: photo.id,
|
||||||
filename: photo.filename,
|
filename: photo.filename,
|
||||||
url: `/photos/${photo.path}`,
|
url: `/photos/${photo.path}`,
|
||||||
thumbnail_url: photo.thumbnail_path ? `/thumbnails/${path.basename(photo.thumbnail_path)}` : null,
|
thumbnail_url: photo.thumbnail_path ? `/${photo.thumbnail_path}` : null,
|
||||||
type: photo.type,
|
type: photo.type,
|
||||||
category_id: photo.category_id,
|
category_id: photo.category_id,
|
||||||
category_name: photo.category_name,
|
category_name: photo.category_name,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.30",
|
"version": "1.0.36",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
const { watermarkEnabled } = useWatermarkSettings();
|
const { watermarkEnabled } = useWatermarkSettings();
|
||||||
|
|
||||||
// Fetch photos
|
// Fetch photos
|
||||||
const { data, isLoading, error } = useGalleryPhotos(slug);
|
const { data, isLoading, error, refetch } = useGalleryPhotos(slug);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -294,11 +294,20 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
|
// Check if it's an authentication error (401)
|
||||||
|
const is401Error = (error as any)?.response?.status === 401;
|
||||||
|
|
||||||
|
if (is401Error) {
|
||||||
|
// Authentication failed - logout and let the parent component handle re-authentication
|
||||||
|
logout();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
|
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg text-neutral-600">{t('gallery.failedToLoad')}</p>
|
<p className="text-lg text-neutral-600">{t('gallery.failedToLoad')}</p>
|
||||||
<Button onClick={() => window.location.reload()} className="mt-4">
|
<Button onClick={() => refetch()} className="mt-4">
|
||||||
{t('gallery.tryAgain')}
|
{t('gallery.tryAgain')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+37
-14
@@ -32,14 +32,24 @@ api.interceptors.request.use(
|
|||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For gallery routes, get the slug from the URL path
|
// For gallery routes, try to extract slug from the request URL first
|
||||||
const pathParts = window.location.pathname.split('/');
|
const galleryMatch = config.url?.match(/\/gallery\/([^\/]+)/);
|
||||||
if (pathParts[1] === 'gallery' && pathParts[2]) {
|
if (galleryMatch && galleryMatch[1]) {
|
||||||
const gallerySlug = pathParts[2];
|
const gallerySlug = galleryMatch[1];
|
||||||
const token = localStorage.getItem(`gallery_token_${gallerySlug}`);
|
const token = localStorage.getItem(`gallery_token_${gallerySlug}`);
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to getting slug from the current page URL
|
||||||
|
const pathParts = window.location.pathname.split('/');
|
||||||
|
if (pathParts[1] === 'gallery' && pathParts[2]) {
|
||||||
|
const gallerySlug = pathParts[2];
|
||||||
|
const token = localStorage.getItem(`gallery_token_${gallerySlug}`);
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,21 +83,34 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// Redirect to appropriate login
|
// Check if it's an admin route
|
||||||
const isAdminRoute = error.config?.url?.includes('/admin');
|
const isAdminRoute = error.config?.url?.includes('/admin');
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
if (isAdminRoute) {
|
if (isAdminRoute) {
|
||||||
// Clear admin token on unauthorized
|
// Clear admin token on unauthorized
|
||||||
Cookies.remove(ADMIN_TOKEN_KEY);
|
Cookies.remove(ADMIN_TOKEN_KEY);
|
||||||
window.location.href = '/admin/login';
|
// Only redirect if we're not already on the admin login page
|
||||||
|
if (!currentPath.includes('/admin/login')) {
|
||||||
|
window.location.href = '/admin/login';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// For gallery routes, clear gallery-specific token and redirect
|
// For gallery routes, check if the error is from a gallery API call
|
||||||
const currentPath = window.location.pathname;
|
const galleryMatch = error.config?.url?.match(/\/gallery\/([^\/]+)/);
|
||||||
const pathParts = currentPath.split('/');
|
|
||||||
if (pathParts[1] === 'gallery' && pathParts[2]) {
|
// Don't redirect if we're on any gallery page (to avoid redirect loops during login)
|
||||||
const gallerySlug = pathParts[2];
|
if (currentPath.startsWith('/gallery/')) {
|
||||||
localStorage.removeItem(`gallery_token_${gallerySlug}`);
|
// If we have a gallery match from the API URL, clear that specific gallery's token
|
||||||
localStorage.removeItem(`gallery_event_${gallerySlug}`);
|
if (galleryMatch && galleryMatch[1]) {
|
||||||
window.location.href = `/gallery/${gallerySlug}`;
|
const gallerySlug = galleryMatch[1];
|
||||||
|
localStorage.removeItem(`gallery_token_${gallerySlug}`);
|
||||||
|
localStorage.removeItem(`gallery_event_${gallerySlug}`);
|
||||||
|
}
|
||||||
|
// Don't redirect - let the component handle the auth state
|
||||||
|
} else {
|
||||||
|
// We're not on a gallery page but got a 401 from a gallery API
|
||||||
|
// This shouldn't happen in normal flow, but if it does, redirect to homepage
|
||||||
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export const useGalleryPhotos = (slug: string, enabled: boolean = true) => {
|
|||||||
enabled,
|
enabled,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
// Add a small delay to ensure auth token is properly set
|
||||||
|
retryDelay: 100,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { settingsService } from '../services/settings.service';
|
import { api } from '../config/api';
|
||||||
|
|
||||||
export function useWatermarkSettings() {
|
export function useWatermarkSettings() {
|
||||||
const [watermarkEnabled, setWatermarkEnabled] = useState(false);
|
const [watermarkEnabled, setWatermarkEnabled] = useState(false);
|
||||||
@@ -8,11 +8,13 @@ export function useWatermarkSettings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSettings = async () => {
|
const fetchSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const settings = await settingsService.getSettingsByType('branding');
|
// Use public settings endpoint that doesn't require authentication
|
||||||
const brandingSettings = settingsService.formatBrandingSettings(settings);
|
const response = await api.get('/public/settings');
|
||||||
setWatermarkEnabled(brandingSettings.watermark_enabled);
|
setWatermarkEnabled(response.data.branding_watermark_enabled || false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch watermark settings:', error);
|
console.error('Failed to fetch watermark settings:', error);
|
||||||
|
// Default to false if we can't fetch settings
|
||||||
|
setWatermarkEnabled(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user