Send gallery image requests with bearer token fallback (#31)
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
import {
|
||||
getActiveGallerySlug,
|
||||
getGalleryToken,
|
||||
inferGallerySlugFromLocation,
|
||||
resolveSlugFromRequestUrl,
|
||||
} from '../../utils/galleryAuthStorage';
|
||||
|
||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
@@ -52,7 +58,6 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
}) => {
|
||||
const unusedProps = {
|
||||
protectFromDownload,
|
||||
slug,
|
||||
photoId,
|
||||
requiresToken,
|
||||
secureUrlTemplate,
|
||||
@@ -76,7 +81,8 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let objectUrl: string | null = null;
|
||||
let aborted = false;
|
||||
const objectUrls: string[] = [];
|
||||
|
||||
// Determine which token to use based on context
|
||||
if (!src) {
|
||||
@@ -88,37 +94,79 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
setIsLoading(true);
|
||||
setError(false);
|
||||
|
||||
// Create a new URL with auth header
|
||||
const resolveSlug = (candidateSrc?: string): string | null => {
|
||||
if (slug) {
|
||||
return slug;
|
||||
}
|
||||
const fromUrl = candidateSrc ? resolveSlugFromRequestUrl(candidateSrc) : null;
|
||||
if (fromUrl) {
|
||||
return fromUrl;
|
||||
}
|
||||
return getActiveGallerySlug() || inferGallerySlugFromLocation();
|
||||
};
|
||||
|
||||
const fetchWithAuth = async (rawUrl: string | undefined | null): Promise<string> => {
|
||||
if (!rawUrl) {
|
||||
throw new Error('No URL provided');
|
||||
}
|
||||
|
||||
// Build full URL for the image
|
||||
const fullImageUrl = rawUrl.startsWith('/admin')
|
||||
? buildResourceUrl(`/api${rawUrl}`)
|
||||
: rawUrl.startsWith('/')
|
||||
? buildResourceUrl(rawUrl)
|
||||
: rawUrl;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
const slugForRequest = resolveSlug(rawUrl);
|
||||
const token = getGalleryToken(slugForRequest);
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(fullImageUrl, {
|
||||
credentials: 'include',
|
||||
headers: Object.keys(headers).length ? headers : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
objectUrls.push(objectUrl);
|
||||
return objectUrl;
|
||||
};
|
||||
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
// Use the src as-is since it should already be the correct endpoint
|
||||
let imageUrl = src;
|
||||
|
||||
// Build full URL for the image
|
||||
// For API paths that start with /admin, we need to prepend /api
|
||||
const fullImageUrl = imageUrl.startsWith('/admin')
|
||||
? buildResourceUrl(`/api${imageUrl}`)
|
||||
: imageUrl.startsWith('/')
|
||||
? buildResourceUrl(imageUrl)
|
||||
: imageUrl;
|
||||
|
||||
// Fetch authenticated image
|
||||
const response = await fetch(fullImageUrl, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
||||
const primaryUrl = await fetchWithAuth(src);
|
||||
if (!aborted) {
|
||||
setImageSrc(primaryUrl);
|
||||
setError(false);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setImageSrc(objectUrl);
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
// Image loading failed - use fallback
|
||||
setError(true);
|
||||
setImageSrc(fallbackSrc || '');
|
||||
setIsLoading(false);
|
||||
if (fallbackSrc && fallbackSrc !== src) {
|
||||
try {
|
||||
const fallbackUrl = await fetchWithAuth(fallbackSrc);
|
||||
if (!aborted) {
|
||||
setImageSrc(fallbackUrl);
|
||||
setError(false);
|
||||
}
|
||||
return;
|
||||
} catch (fallbackError) {
|
||||
// Swallow and mark error below
|
||||
}
|
||||
}
|
||||
if (!aborted) {
|
||||
setError(true);
|
||||
setImageSrc('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!aborted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -127,11 +175,11 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
aborted = true;
|
||||
objectUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||
};
|
||||
}, [src, fallbackSrc, useWatermark, isGallery]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [src, fallbackSrc, slug]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user