Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/react/src/hooks/useAuthenticatedMediaUrl.js
Original file line number Diff line number Diff line change
@@ -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;
192 changes: 120 additions & 72 deletions packages/react/src/views/AttachmentHandler/ImageAttachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Box css={variantStyles.imageAttachmentContainer}>
<Box
onClick={() => 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' ? (
<Box
css={[
css`
display: flex;
gap: 0.3rem;
align-items: center;
`,
variantStyles.textUserInfo,
]}
>
<Avatar
url={getUserAvatarUrl(nestedAttachment.author_icon)}
alt="avatar"
size="1.2em"
/>
<Box>@{nestedAttachment.author_name}</Box>
</Box>
) : (
''
)}
<AttachmentMetadata
attachment={nestedAttachment}
url={titleUrl || mediaUrl}
variantStyles={variantStyles}
/>
<img
src={mediaUrl}
style={{
maxWidth: '100%',
objectFit: 'contain',
borderBottomLeftRadius: 'inherit',
borderBottomRightRadius: 'inherit',
}}
/>
</Box>
{showGallery && (
<ImageGallery
currentFileId={extractIdFromUrl(nestedAttachment.title_link)}
setShowGallery={setShowGallery}
/>
)}
</Box>
);
};

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,
}) => {
Expand All @@ -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;
};

Expand All @@ -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 (
<Box css={variantStyles.imageAttachmentContainer}>
<Box
Expand Down Expand Up @@ -86,7 +187,7 @@ const ImageAttachment = ({
>
<AttachmentMetadata
attachment={attachment}
url={host + (attachment.title_link || attachment.image_url)}
url={titleUrl || mediaUrl}
variantStyles={variantStyles}
msg={msg}
onExpandCollapseClick={toggleExpanded}
Expand All @@ -96,7 +197,7 @@ const ImageAttachment = ({
{isExpanded && (
<Box onClick={() => setShowGallery(true)}>
<img
src={host + attachment.image_url}
src={mediaUrl}
style={{
maxWidth: '100%',
objectFit: 'contain',
Expand All @@ -108,74 +209,17 @@ const ImageAttachment = ({
)}
{attachment.attachments &&
attachment.attachments.map((nestedAttachment, index) => (
<Box css={variantStyles.imageAttachmentContainer} key={index}>
<Box
onClick={() => 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' ? (
<>
<Box
css={[
css`
display: flex;
gap: 0.3rem;
align-items: center;
`,
variantStyles.textUserInfo,
]}
>
<Avatar
url={getUserAvatarUrl(nestedAttachment.author_icon)}
alt="avatar"
size="1.2em"
/>
<Box>@{nestedAttachment.author_name}</Box>
</Box>
</>
) : (
''
)}
<AttachmentMetadata
attachment={nestedAttachment}
url={
host +
(nestedAttachment.title_link || nestedAttachment.image_url)
}
variantStyles={variantStyles}
/>
<img
src={host + nestedAttachment.image_url}
style={{
maxWidth: '100%',
objectFit: 'contain',
borderBottomLeftRadius: 'inherit',
borderBottomRightRadius: 'inherit',
}}
/>
</Box>
{showGallery && (
<ImageGallery
currentFileId={extractIdFromUrl(nestedAttachment.title_link)}
setShowGallery={setShowGallery}
/>
)}
</Box>
<NestedImageAttachment
key={index}
nestedAttachment={nestedAttachment}
host={host}
variantStyles={variantStyles}
theme={theme}
getUserAvatarUrl={getUserAvatarUrl}
extractIdFromUrl={extractIdFromUrl}
setShowGallery={setShowGallery}
showGallery={showGallery}
/>
))}
</Box>
{showGallery && (
Expand All @@ -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,
};