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:
+21
-10
@@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -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=/;';
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user