diff --git a/backend/scripts/test-photo-auth.js b/backend/scripts/test-photo-auth.js new file mode 100755 index 0000000..955f269 --- /dev/null +++ b/backend/scripts/test-photo-auth.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * Script to test photo authentication + * Usage: node scripts/test-photo-auth.js + */ + +const axios = require('axios'); + +async function testPhotoAuth(token) { + if (!token) { + console.error('Usage: node scripts/test-photo-auth.js '); + console.error('\nTo get a token, login to a gallery and check localStorage for gallery_token_'); + process.exit(1); + } + + const baseUrl = process.env.API_URL || 'http://localhost:3001'; + + console.log(`Testing photo authentication with token: ${token.substring(0, 20)}...`); + console.log(`Base URL: ${baseUrl}\n`); + + // Test URLs + const tests = [ + { + name: 'Thumbnail via static route', + url: `${baseUrl}/thumbnails/thumb_Test_Gallery_uncategorized_5210.jpg`, + headers: { 'Authorization': `Bearer ${token}` } + }, + { + name: 'Photo via static route', + url: `${baseUrl}/photos/wedding-test-gallery-2025-07-14-1/Test_Gallery_uncategorized_5210.jpg`, + headers: { 'Authorization': `Bearer ${token}` } + }, + { + name: 'Gallery photos API', + url: `${baseUrl}/api/gallery/wedding-test-gallery-2025-07-14-1/photos`, + headers: { 'Authorization': `Bearer ${token}` } + } + ]; + + for (const test of tests) { + console.log(`Testing: ${test.name}`); + console.log(`URL: ${test.url}`); + + try { + const response = await axios.get(test.url, { + headers: test.headers, + validateStatus: () => true // Don't throw on any status + }); + + console.log(`Status: ${response.status}`); + console.log(`Headers:`, response.headers['content-type']); + + if (response.status === 200) { + if (test.name.includes('API')) { + console.log(`Photos count: ${response.data.photos?.length || 0}`); + } else { + console.log(`Content length: ${response.headers['content-length']} bytes`); + } + } else { + console.log(`Error:`, response.data); + } + } catch (error) { + console.log(`Network error:`, error.message); + } + + console.log('---\n'); + } + + // Decode token to show info + try { + const parts = token.split('.'); + const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); + console.log('Token payload:', payload); + } catch (error) { + console.log('Failed to decode token'); + } +} + +// Get token from command line +const token = process.argv[2]; + +testPhotoAuth(token).catch(error => { + console.error('Test failed:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index be72d97..025f6d0 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -136,8 +136,8 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { photos: photos.map(photo => ({ id: photo.id, filename: photo.filename, - url: `/photos/${photo.path}`, - thumbnail_url: photo.thumbnail_path ? `/${photo.thumbnail_path}` : null, + url: `/api/gallery/${req.params.slug}/photo/${photo.id}`, + thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}` : null, type: photo.type, category_id: photo.category_id, category_name: photo.category_name, @@ -320,6 +320,42 @@ router.get('/:slug/photo/:photoId', verifyGalleryAccess, async (req, res) => { } }); +// 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' }); + } + + // Set appropriate headers + res.setHeader('Content-Type', 'image/jpeg'); + res.setHeader('Cache-Control', 'private, max-age=3600'); + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + + // Send file + res.sendFile(path.resolve(thumbPath)); + } catch (error) { + console.error('Error serving thumbnail:', error); + res.status(500).json({ error: 'Failed to serve thumbnail' }); + } +}); + // Get photo stats router.get('/:slug/stats', verifyGalleryAccess, async (req, res) => { try { diff --git a/docs/nginx-fix.md b/docs/nginx-fix.md new file mode 100644 index 0000000..a3407cf --- /dev/null +++ b/docs/nginx-fix.md @@ -0,0 +1,59 @@ +# Nginx Configuration Fix for Photo Authentication + +If photos and thumbnails are not loading in gallery view but work in admin, it's likely that the Authorization header is being stripped by nginx or another reverse proxy. + +## Common Issue + +The `Authorization` header is often not passed through by default in nginx proxy configurations. + +## Fix + +Add these lines to your nginx configuration for the PicPeak location block: + +```nginx +location / { + proxy_pass http://localhost:3001; + + # Important: Pass the Authorization header + proxy_pass_header Authorization; + proxy_set_header Authorization $http_authorization; + + # Other standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +## Alternative Fix Using Traefik + +If using Traefik, ensure headers are passed: + +```yaml +services: + picpeak: + labels: + - "traefik.http.middlewares.picpeak-headers.headers.customrequestheaders.Authorization=" +``` + +## Testing + +1. Check if Authorization header is reaching the backend: + ```bash + curl -H "Authorization: Bearer YOUR_TOKEN" https://picpeak.yourdomain.com/thumbnails/test.jpg -v + ``` + +2. Check nginx logs to see if the header is present: + ```bash + tail -f /var/log/nginx/access.log + ``` + +## Docker Compose Fix + +If using docker-compose with nginx proxy, add: + +```yaml +environment: + - NGINX_PROXY_PASS_HEADER=Authorization +``` \ No newline at end of file