fix: resolve gallery login redirect issue
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s

- Updated API interceptor to better handle gallery authentication
- Fixed 401 error handling to prevent redirect loops on gallery pages
- Improved token extraction logic for gallery API requests
- Consolidated duplicate verifyGalleryAccess middleware
- Added proper error handling in GalleryView component
- Gallery authentication now properly distinguishes from admin routes

The issue was caused by the API interceptor redirecting to admin login
when gallery API calls failed with 401, even when users were already
on gallery pages attempting to authenticate.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-15 08:34:40 +02:00
parent cfaee103b6
commit 21b1e79672
5 changed files with 58 additions and 42 deletions
+7 -1
View File
@@ -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' });
+1 -25
View File
@@ -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 {
@@ -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
View File
@@ -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 = '/';
} }
} }
} }
+2
View File
@@ -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,
}); });
}; };