From cf32b0135672b4ecc6c333359437f1f544df552c Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 9 Jul 2025 08:05:04 +0200 Subject: [PATCH] fix: Implement gallery-specific authentication tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix issue where different galleries shared authentication - Store gallery tokens with slug-specific keys in localStorage - Remove global gallery_token cookie approach - Each gallery now maintains its own authentication state - Add cleanup for legacy authentication data This ensures that accessing different galleries requires separate authentication and prevents cross-gallery authentication leakage. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/src/config/api.ts | 37 +++++++++----- frontend/src/contexts/GalleryAuthContext.tsx | 51 +++++++++++++++----- frontend/src/services/auth.service.ts | 4 +- frontend/src/utils/cleanupGalleryAuth.ts | 8 +++ 4 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/cleanupGalleryAuth.ts diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index c6bc398..57d79cf 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -25,12 +25,22 @@ api.interceptors.request.use( (config) => { // Check if it's an admin route or gallery route const isAdminRoute = config.url?.includes('/admin'); - const token = isAdminRoute - ? Cookies.get(ADMIN_TOKEN_KEY) - : Cookies.get(GALLERY_TOKEN_KEY); - - if (token) { - config.headers.Authorization = `Bearer ${token}`; + + if (isAdminRoute) { + const token = Cookies.get(ADMIN_TOKEN_KEY); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + } else { + // For gallery routes, get the slug from the URL path + 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}`; + } + } } // Don't set Content-Type for FormData - let browser set it with boundary @@ -63,19 +73,20 @@ api.interceptors.response.use( } if (error.response?.status === 401) { - // Clear tokens on unauthorized - Cookies.remove(ADMIN_TOKEN_KEY); - Cookies.remove(GALLERY_TOKEN_KEY); - // Redirect to appropriate login const isAdminRoute = error.config?.url?.includes('/admin'); if (isAdminRoute) { + // Clear admin token on unauthorized + Cookies.remove(ADMIN_TOKEN_KEY); window.location.href = '/admin/login'; } else { - // For gallery routes, redirect to the gallery password page + // For gallery routes, clear gallery-specific token and redirect const currentPath = window.location.pathname; - const gallerySlug = currentPath.split('/')[2]; - if (gallerySlug) { + const pathParts = currentPath.split('/'); + if (pathParts[1] === 'gallery' && pathParts[2]) { + const gallerySlug = pathParts[2]; + localStorage.removeItem(`gallery_token_${gallerySlug}`); + localStorage.removeItem(`gallery_event_${gallerySlug}`); window.location.href = `/gallery/${gallerySlug}`; } } diff --git a/frontend/src/contexts/GalleryAuthContext.tsx b/frontend/src/contexts/GalleryAuthContext.tsx index 6365c0f..c6a906b 100644 --- a/frontend/src/contexts/GalleryAuthContext.tsx +++ b/frontend/src/contexts/GalleryAuthContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import type { ReactNode } from 'react'; -import { getAuthToken } from '../config/api'; import { authService } from '../services'; +import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth'; interface GalleryEvent { id: number; @@ -42,20 +42,42 @@ export const GalleryAuthProvider: React.FC = ({ childr const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + // Get current gallery slug from URL + const getCurrentGallerySlug = () => { + const pathParts = window.location.pathname.split('/'); + if (pathParts[1] === 'gallery' && pathParts[2]) { + return pathParts[2]; + } + return null; + }; + useEffect(() => { + // Clean up old authentication data on mount + cleanupOldGalleryAuth(); + // Check if user has a valid token on mount - const token = getAuthToken(false); - if (token) { - // Try to restore event data from localStorage - const storedEvent = localStorage.getItem('gallery_event'); - if (storedEvent) { + const currentSlug = getCurrentGallerySlug(); + if (currentSlug) { + // Try to restore event data from localStorage with slug-specific key + const storedEvent = localStorage.getItem(`gallery_event_${currentSlug}`); + const storedToken = localStorage.getItem(`gallery_token_${currentSlug}`); + + if (storedEvent && storedToken) { try { const eventData = JSON.parse(storedEvent); - setEvent(eventData); - setIsAuthenticated(true); + // Verify the stored event matches the current gallery slug + if (eventData && eventData.id) { + setEvent(eventData); + setIsAuthenticated(true); + } else { + // Clear invalid data + localStorage.removeItem(`gallery_event_${currentSlug}`); + localStorage.removeItem(`gallery_token_${currentSlug}`); + } } catch (error) { console.error('Failed to parse stored event data'); - localStorage.removeItem('gallery_event'); + localStorage.removeItem(`gallery_event_${currentSlug}`); + localStorage.removeItem(`gallery_token_${currentSlug}`); } } } @@ -70,8 +92,9 @@ export const GalleryAuthProvider: React.FC = ({ childr setEvent(response.event); setIsAuthenticated(true); - // Store event data in localStorage - localStorage.setItem('gallery_event', JSON.stringify(response.event)); + // Store event data and token in localStorage with slug-specific key + localStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event)); + localStorage.setItem(`gallery_token_${slug}`, response.token); } catch (err: any) { setError(err.response?.data?.error || 'Invalid password'); throw err; @@ -81,10 +104,14 @@ export const GalleryAuthProvider: React.FC = ({ childr }; const logout = () => { + const currentSlug = getCurrentGallerySlug(); + if (currentSlug) { + localStorage.removeItem(`gallery_event_${currentSlug}`); + localStorage.removeItem(`gallery_token_${currentSlug}`); + } authService.galleryLogout(); setIsAuthenticated(false); setEvent(null); - localStorage.removeItem('gallery_event'); }; return ( diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 563e880..18750df 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -28,11 +28,11 @@ export const authService = { recaptchaToken }); - setAuthToken(response.data.token, false); + // Token is now handled by GalleryAuthContext with slug-specific storage return response.data; }, galleryLogout() { - clearAuthToken(false); + // Logout is now handled by GalleryAuthContext }, }; \ No newline at end of file diff --git a/frontend/src/utils/cleanupGalleryAuth.ts b/frontend/src/utils/cleanupGalleryAuth.ts new file mode 100644 index 0000000..a4cc226 --- /dev/null +++ b/frontend/src/utils/cleanupGalleryAuth.ts @@ -0,0 +1,8 @@ +// Cleanup function to remove old gallery authentication data +export const cleanupOldGalleryAuth = () => { + // Remove old global gallery authentication + localStorage.removeItem('gallery_event'); + + // Remove old gallery token from cookies if it exists + document.cookie = 'gallery_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; +}; \ No newline at end of file