feat(frontend): add AuthenticatedImg component for authorized image requests (#16525)

This commit is contained in:
euvre
2026-07-01 17:02:44 +08:00
committed by GitHub
parent 97a4c64cc8
commit 81cfcdf2d3
8 changed files with 117 additions and 21 deletions

View File

@@ -1,7 +1,6 @@
import { Authorization } from '@/constants/authorization';
import { restAPIv1 } from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { getSearchValue } from '@/utils/common-util';
import classNames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
@@ -82,19 +81,36 @@ const fetchDocumentImage = (url: string, authorization: string) => {
};
};
// Check if a URL requires authentication (internal API URLs)
// Only attach Authorization headers to same-origin requests to prevent token leakage
const isAuthRequiredUrl = (url: string): boolean => {
try {
const parsedUrl = new URL(url, window.location.origin);
if (parsedUrl.origin !== window.location.origin) {
return false;
}
return (
parsedUrl.pathname.startsWith('/api/v1/') ||
parsedUrl.pathname.includes('/documents/images/')
);
} catch {
return false;
}
};
export const useDocumentImageUrl = (id: string, t?: string | number) => {
const directUrl = useMemo(() => buildDocumentImageUrl(id, t), [id, t]);
const [imageUrl, setImageUrl] = useState(() =>
getAuthorization() && getSearchValue('shared_id') ? '' : directUrl,
);
const [imageUrl, setImageUrl] = useState<string>('');
useEffect(() => {
const authorization = getAuthorization();
if (!authorization || !getSearchValue('shared_id')) {
// For non-API URLs (e.g., base64, external URLs), use directly
if (!isAuthRequiredUrl(directUrl)) {
setImageUrl(directUrl);
return;
}
// For API URLs that require authentication, always fetch with auth headers
const authorization = getAuthorization();
let ignore = false;
setImageUrl('');
const { promise, release } = fetchDocumentImage(directUrl, authorization);
@@ -120,6 +136,72 @@ export const useDocumentImageUrl = (id: string, t?: string | number) => {
return imageUrl;
};
/**
* Hook to convert any authenticated URL to a blob URL for use in <img> tags.
* Use this for thumbnail URLs or any other API URLs that require authentication.
*/
export const useAuthenticatedImageUrl = (url: string | undefined | null) => {
const [imageUrl, setImageUrl] = useState<string>('');
useEffect(() => {
if (!url || !isAuthRequiredUrl(url)) {
setImageUrl(url || '');
return;
}
const authorization = getAuthorization();
let cancelled = false;
setImageUrl('');
const { promise, release } = fetchDocumentImage(url, authorization);
promise
.then((blobUrl) => {
if (!cancelled) {
setImageUrl(blobUrl);
}
})
.catch(() => {
if (!cancelled) {
setImageUrl('');
}
});
return () => {
cancelled = true;
release();
};
}, [url]);
return imageUrl;
};
/**
* Component that renders an <img> tag with proper authentication for API URLs.
* Use this instead of <img src={apiUrl}> when the URL requires authentication.
*/
export const AuthenticatedImg = ({
src,
alt,
className,
fallback,
...props
}: React.ImgHTMLAttributes<HTMLImageElement> & {
fallback?: React.ReactNode;
}) => {
const authenticatedSrc = useAuthenticatedImageUrl(src);
if (!authenticatedSrc) return fallback ?? null;
return (
<img
src={authenticatedSrc}
alt={alt}
className={className}
{...props}
/>
);
};
const Image = ({ id, t, label, className, ...props }: IImage) => {
const src = useDocumentImageUrl(id, t);
const imageElement = (