From 605f773a7e577e15c6b11d3bb93710e1fb80a540 Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 15 Jul 2025 11:47:24 +0200 Subject: [PATCH] fix: resolve gallery photo/thumbnail serving issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change gallery photo URLs from static paths to API endpoints - Add dedicated thumbnail serving endpoint for galleries - Add test script to diagnose authentication issues - Add nginx configuration documentation for Authorization header This fixes the issue where photos and thumbnails work in admin but not in gallery view. The problem was that static file routes with auth middleware often have Authorization headers stripped by reverse proxies. Using API endpoints ensures proper authentication handling. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/scripts/test-photo-auth.js | 86 ++++++++++++++++++++++++++++++ backend/src/routes/gallery.js | 40 +++++++++++++- docs/nginx-fix.md | 59 ++++++++++++++++++++ 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100755 backend/scripts/test-photo-auth.js create mode 100644 docs/nginx-fix.md 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