diff --git a/packages/react/src/hooks/useAuthenticatedMediaUrl.js b/packages/react/src/hooks/useAuthenticatedMediaUrl.js
new file mode 100644
index 000000000..2a23fcb19
--- /dev/null
+++ b/packages/react/src/hooks/useAuthenticatedMediaUrl.js
@@ -0,0 +1,109 @@
+import { useState, useEffect, useContext } from 'react';
+import RCContext from '../context/RCInstance';
+
+/**
+ * Builds an authenticated URL for Rocket.Chat media uploads.
+ * Only appends credentials if the target is an internal /file-upload/ path.
+ * External URLs are strictly returned untouched to prevent credential leakage.
+ */
+export const buildAuthenticatedUrl = (mediaPath, host, token, userId) => {
+ if (!mediaPath || typeof mediaPath !== 'string') {
+ return '';
+ }
+
+ const isHttpUrl = /^https?:\/\//i.test(mediaPath);
+
+ if (!isHttpUrl && !host) {
+ return mediaPath;
+ }
+
+ try {
+ const targetUrl = isHttpUrl ? new URL(mediaPath) : new URL(mediaPath, host);
+ const hostUrl = host ? new URL(host) : null;
+
+ const isSameHost = hostUrl
+ ? targetUrl.origin === hostUrl.origin
+ : !isHttpUrl;
+ const isFileUpload = targetUrl.pathname.startsWith('/file-upload/');
+
+ if (isFileUpload && isSameHost && token && userId) {
+ targetUrl.searchParams.set('rc_token', token);
+ targetUrl.searchParams.set('rc_uid', userId);
+ return targetUrl.toString();
+ }
+
+ return isHttpUrl ? mediaPath : targetUrl.toString();
+ } catch (e) {
+ return isHttpUrl ? mediaPath : `${host || ''}${mediaPath}`;
+ }
+};
+
+const getCredentialsFromUser = (user) => {
+ if (!user) return { token: null, userId: null };
+ const token = user.authToken || user.data?.authToken || null;
+ const userId = user.userId || user.data?.userId || user._id || null;
+ return { token, userId };
+};
+
+const useAuthenticatedMediaUrl = (mediaPath, host) => {
+ const { RCInstance } = useContext(RCContext);
+ const effectiveHost = host || RCInstance?.getHost?.() || '';
+
+ const currentUser = RCInstance?.auth?.currentUser;
+ const initialCreds = getCredentialsFromUser(currentUser);
+
+ const [authenticatedUrl, setAuthenticatedUrl] = useState(() =>
+ buildAuthenticatedUrl(
+ mediaPath,
+ effectiveHost,
+ initialCreds.token,
+ initialCreds.userId
+ )
+ );
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const updateUrl = (user) => {
+ if (!isMounted) return;
+ const { token, userId } = getCredentialsFromUser(user);
+ const newUrl = buildAuthenticatedUrl(
+ mediaPath,
+ effectiveHost,
+ token,
+ userId
+ );
+ setAuthenticatedUrl(newUrl);
+ };
+
+ if (RCInstance?.auth?.getCurrentUser) {
+ RCInstance.auth
+ .getCurrentUser()
+ .then((user) => {
+ updateUrl(user);
+ })
+ .catch(() => {
+ // Gracefully maintain existing url state on failure
+ });
+ }
+
+ const handleAuthChange = (user) => {
+ updateUrl(user);
+ };
+
+ if (RCInstance?.auth?.onAuthChange) {
+ RCInstance.auth.onAuthChange(handleAuthChange);
+ }
+
+ return () => {
+ isMounted = false;
+ if (RCInstance?.auth?.removeAuthListener) {
+ RCInstance.auth.removeAuthListener(handleAuthChange);
+ }
+ };
+ }, [RCInstance, mediaPath, effectiveHost]);
+
+ return authenticatedUrl;
+};
+
+export default useAuthenticatedMediaUrl;
diff --git a/packages/react/src/views/AttachmentHandler/ImageAttachment.js b/packages/react/src/views/AttachmentHandler/ImageAttachment.js
index 84516ce65..6b5071063 100644
--- a/packages/react/src/views/AttachmentHandler/ImageAttachment.js
+++ b/packages/react/src/views/AttachmentHandler/ImageAttachment.js
@@ -5,12 +5,107 @@ import { Box, Avatar, useTheme } from '@embeddedchat/ui-elements';
import AttachmentMetadata from './AttachmentMetadata';
import ImageGallery from '../ImageGallery/ImageGallery';
import RCContext from '../../context/RCInstance';
+import useAuthenticatedMediaUrl from '../../hooks/useAuthenticatedMediaUrl';
+
+const NestedImageAttachment = ({
+ nestedAttachment,
+ host,
+ variantStyles = {},
+ theme,
+ getUserAvatarUrl,
+ extractIdFromUrl,
+ setShowGallery,
+ showGallery,
+}) => {
+ const mediaUrl = useAuthenticatedMediaUrl(nestedAttachment.image_url, host);
+ const titleUrl = useAuthenticatedMediaUrl(
+ nestedAttachment.title_link || nestedAttachment.image_url,
+ host
+ );
+
+ return (
+
+ setShowGallery(true)}
+ css={[
+ css`
+ cursor: pointer;
+ border-radius: inherit;
+ line-height: 0;
+ padding: 0.5rem;
+ `,
+ (nestedAttachment.attachments?.[0]?.type
+ ? variantStyles.pinnedContainer
+ : variantStyles.quoteContainer) ||
+ css`
+ ${nestedAttachment.attachments?.[0]?.type === 'file'
+ ? `border: 2px solid ${theme.colors.border};`
+ : ''}
+ `,
+ ]}
+ >
+ {nestedAttachment.type === 'file' ? (
+
+
+ @{nestedAttachment.author_name}
+
+ ) : (
+ ''
+ )}
+
+
+
+ {showGallery && (
+
+ )}
+
+ );
+};
+
+NestedImageAttachment.propTypes = {
+ nestedAttachment: PropTypes.object,
+ host: PropTypes.string,
+ variantStyles: PropTypes.object,
+ theme: PropTypes.object,
+ getUserAvatarUrl: PropTypes.func,
+ extractIdFromUrl: PropTypes.func,
+ setShowGallery: PropTypes.func,
+ showGallery: PropTypes.bool,
+};
const ImageAttachment = ({
attachment,
host,
type,
- author,
+ author = {},
variantStyles = {},
msg,
}) => {
@@ -22,7 +117,7 @@ const ImageAttachment = ({
return URL;
};
const extractIdFromUrl = (url) => {
- const match = url.match(/\/file-upload\/(.*?)\//);
+ const match = url?.match(/\/file-upload\/(.*?)\//);
return match ? match[1] : null;
};
@@ -35,6 +130,12 @@ const ImageAttachment = ({
setIsExpanded((prevState) => !prevState);
};
+ const mediaUrl = useAuthenticatedMediaUrl(attachment.image_url, host);
+ const titleUrl = useAuthenticatedMediaUrl(
+ attachment.title_link || attachment.image_url,
+ host
+ );
+
return (
setShowGallery(true)}>
(
-
- setShowGallery(true)}
- css={[
- css`
- cursor: pointer;
- border-radius: inherit;
- line-height: 0;
- padding: 0.5rem;
- `,
- (nestedAttachment.attachments[0].type
- ? variantStyles.pinnedContainer
- : variantStyles.quoteContainer) ||
- css`
- ${nestedAttachment.attachments[0].type === 'file'
- ? `border: 2px solid ${theme.colors.border};`
- : ''}
- `,
- ]}
- >
- {nestedAttachment.type === 'file' ? (
- <>
-
-
- @{nestedAttachment.author_name}
-
- >
- ) : (
- ''
- )}
-
-
-
- {showGallery && (
-
- )}
-
+
))}
{showGallery && (
@@ -193,4 +237,8 @@ export default ImageAttachment;
ImageAttachment.propTypes = {
attachment: PropTypes.object,
host: PropTypes.string,
+ type: PropTypes.string,
+ author: PropTypes.object,
+ variantStyles: PropTypes.object,
+ msg: PropTypes.object,
};