fix: Implement gallery-specific authentication tokens

- 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 <[email protected]>
This commit is contained in:
2025-07-09 08:05:04 +02:00
co-authored by Claude
parent c8cfce3e36
commit cf32b01356
4 changed files with 73 additions and 27 deletions
+21 -10
View File
@@ -25,13 +25,23 @@ api.interceptors.request.use(
(config) => { (config) => {
// Check if it's an admin route or gallery route // Check if it's an admin route or gallery route
const isAdminRoute = config.url?.includes('/admin'); const isAdminRoute = config.url?.includes('/admin');
const token = isAdminRoute
? Cookies.get(ADMIN_TOKEN_KEY)
: Cookies.get(GALLERY_TOKEN_KEY);
if (isAdminRoute) {
const token = Cookies.get(ADMIN_TOKEN_KEY);
if (token) { if (token) {
config.headers.Authorization = `Bearer ${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 // Don't set Content-Type for FormData - let browser set it with boundary
if (config.data instanceof FormData) { if (config.data instanceof FormData) {
@@ -63,19 +73,20 @@ api.interceptors.response.use(
} }
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Clear tokens on unauthorized
Cookies.remove(ADMIN_TOKEN_KEY);
Cookies.remove(GALLERY_TOKEN_KEY);
// Redirect to appropriate login // Redirect to appropriate login
const isAdminRoute = error.config?.url?.includes('/admin'); const isAdminRoute = error.config?.url?.includes('/admin');
if (isAdminRoute) { if (isAdminRoute) {
// Clear admin token on unauthorized
Cookies.remove(ADMIN_TOKEN_KEY);
window.location.href = '/admin/login'; window.location.href = '/admin/login';
} else { } 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 currentPath = window.location.pathname;
const gallerySlug = currentPath.split('/')[2]; const pathParts = currentPath.split('/');
if (gallerySlug) { 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}`; window.location.href = `/gallery/${gallerySlug}`;
} }
} }
+37 -10
View File
@@ -1,7 +1,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { getAuthToken } from '../config/api';
import { authService } from '../services'; import { authService } from '../services';
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
interface GalleryEvent { interface GalleryEvent {
id: number; id: number;
@@ -42,20 +42,42 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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(() => { useEffect(() => {
// Clean up old authentication data on mount
cleanupOldGalleryAuth();
// Check if user has a valid token on mount // Check if user has a valid token on mount
const token = getAuthToken(false); const currentSlug = getCurrentGallerySlug();
if (token) { if (currentSlug) {
// Try to restore event data from localStorage // Try to restore event data from localStorage with slug-specific key
const storedEvent = localStorage.getItem('gallery_event'); const storedEvent = localStorage.getItem(`gallery_event_${currentSlug}`);
if (storedEvent) { const storedToken = localStorage.getItem(`gallery_token_${currentSlug}`);
if (storedEvent && storedToken) {
try { try {
const eventData = JSON.parse(storedEvent); const eventData = JSON.parse(storedEvent);
// Verify the stored event matches the current gallery slug
if (eventData && eventData.id) {
setEvent(eventData); setEvent(eventData);
setIsAuthenticated(true); setIsAuthenticated(true);
} else {
// Clear invalid data
localStorage.removeItem(`gallery_event_${currentSlug}`);
localStorage.removeItem(`gallery_token_${currentSlug}`);
}
} catch (error) { } catch (error) {
console.error('Failed to parse stored event data'); 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<GalleryAuthProviderProps> = ({ childr
setEvent(response.event); setEvent(response.event);
setIsAuthenticated(true); setIsAuthenticated(true);
// Store event data in localStorage // Store event data and token in localStorage with slug-specific key
localStorage.setItem('gallery_event', JSON.stringify(response.event)); localStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
localStorage.setItem(`gallery_token_${slug}`, response.token);
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || 'Invalid password'); setError(err.response?.data?.error || 'Invalid password');
throw err; throw err;
@@ -81,10 +104,14 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
}; };
const logout = () => { const logout = () => {
const currentSlug = getCurrentGallerySlug();
if (currentSlug) {
localStorage.removeItem(`gallery_event_${currentSlug}`);
localStorage.removeItem(`gallery_token_${currentSlug}`);
}
authService.galleryLogout(); authService.galleryLogout();
setIsAuthenticated(false); setIsAuthenticated(false);
setEvent(null); setEvent(null);
localStorage.removeItem('gallery_event');
}; };
return ( return (
+2 -2
View File
@@ -28,11 +28,11 @@ export const authService = {
recaptchaToken recaptchaToken
}); });
setAuthToken(response.data.token, false); // Token is now handled by GalleryAuthContext with slug-specific storage
return response.data; return response.data;
}, },
galleryLogout() { galleryLogout() {
clearAuthToken(false); // Logout is now handled by GalleryAuthContext
}, },
}; };
+8
View File
@@ -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=/;';
};