Send gallery image requests with bearer token fallback (#31)
Test and Lint / backend-test (push) Successful in 1m26s
Test and Lint / frontend-test (push) Successful in 1m53s

This commit is contained in:
Paul Nothaft
2025-10-13 20:28:53 +02:00
parent 8c690155bf
commit df10fc677e
@@ -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 (