diff --git a/packages/auth/src/RocketChatAuth.ts b/packages/auth/src/RocketChatAuth.ts
index 0f2c55f196..d70c681629 100644
--- a/packages/auth/src/RocketChatAuth.ts
+++ b/packages/auth/src/RocketChatAuth.ts
@@ -180,6 +180,22 @@ class RocketChatAuth {
async save() {
await this.saveToken(this.currentUser.authToken);
+ try {
+ if (typeof window !== "undefined") {
+ const proxyUrl = "/api/proxy-auth";
+ await fetch(proxyUrl, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ rc_token: this.currentUser.authToken,
+ rc_uid: this.currentUser.userId,
+ host: this.host,
+ }),
+ }).catch(() => null); // Fail silently if no proxy is configured
+ }
+ } catch (e) {
+ // Ignore proxy errors
+ }
this.notifyAuthListeners();
}
diff --git a/packages/react/.storybook/middleware.js b/packages/react/.storybook/middleware.js
new file mode 100644
index 0000000000..d03d504992
--- /dev/null
+++ b/packages/react/.storybook/middleware.js
@@ -0,0 +1,80 @@
+// packages/react/.storybook/middleware.js
+const express = require('express');
+
+// Simple in-memory session store for development
+const sessions = {};
+
+module.exports = function expressMiddleware(app) {
+ app.use(express.json());
+
+ // 1. Session Setup Endpoint
+ app.post('/api/proxy-auth', (req, res) => {
+ const { rc_token, rc_uid, host } = req.body;
+ if (!rc_token || !rc_uid || !host) {
+ return res.status(400).json({ error: 'Missing parameters' });
+ }
+
+ // Generate a simple session ID
+ const sessionId = Math.random().toString(36).substring(2, 15);
+ sessions[sessionId] = { rc_token, rc_uid, host };
+
+ // Set HTTP-only cookie
+ res.cookie('ec_session', sessionId, {
+ httpOnly: true,
+ secure: false, // For local Storybook (http://localhost:6006)
+ sameSite: 'lax',
+ path: '/',
+ });
+
+ res.json({ success: true });
+ });
+
+ // 2. Image Proxy Endpoint
+ app.get('/api/proxy-media', async (req, res) => {
+ const { url } = req.query;
+ if (!url) {
+ return res.status(400).send('Missing url parameter');
+ }
+
+ // Parse cookies manually to avoid needing cookie-parser
+ const cookieHeader = req.headers.cookie || '';
+ const cookies = cookieHeader.split(';').reduce((acc, cookieStr) => {
+ const [key, val] = cookieStr.split('=').map((s) => s.trim());
+ if (key && val) acc[key] = val;
+ return acc;
+ }, {});
+
+ const sessionId = cookies['ec_session'];
+ const session = sessionId ? sessions[sessionId] : null;
+
+ const headers = new Headers();
+ if (session) {
+ headers.append('X-Auth-Token', session.rc_token);
+ headers.append('X-User-Id', session.rc_uid);
+ }
+
+ try {
+ // Fetch the file from the remote Rocket.Chat server
+ const proxyRes = await fetch(url, { headers });
+
+ if (!proxyRes.ok) {
+ return res.status(proxyRes.status).send('RC Server returned an error');
+ }
+
+ // Copy relevant headers (Content-Type, Content-Length)
+ const contentType = proxyRes.headers.get('content-type');
+ const contentLength = proxyRes.headers.get('content-length');
+ if (contentType) res.setHeader('Content-Type', contentType);
+ if (contentLength) res.setHeader('Content-Length', contentLength);
+
+ // Pipe the stream using Node API
+ // proxyRes.body is a web stream in Node 18+, convert to Node stream if needed or use Response.arrayBuffer
+ const arrayBuffer = await proxyRes.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ res.send(buffer);
+ } catch (e) {
+ console.error('Proxy Fetch Error:', e);
+ res.status(500).send('Proxy backend error');
+ }
+ });
+};
diff --git a/packages/react/src/views/AttachmentHandler/AuthenticatedImage.js b/packages/react/src/views/AttachmentHandler/AuthenticatedImage.js
new file mode 100644
index 0000000000..b1c4d1c0ef
--- /dev/null
+++ b/packages/react/src/views/AttachmentHandler/AuthenticatedImage.js
@@ -0,0 +1,24 @@
+import React, { useState } from 'react';
+
+const AuthenticatedImage = ({ url, alt, ...props }) => {
+ const [hasError, setHasError] = useState(false);
+
+ if (!url) return null;
+
+ // The proxy endpoint established in Storybook middleware.
+ // In a real application, the host would provide their own media proxy URL.
+ const proxyUrl = `/api/proxy-media?url=${encodeURIComponent(url)}`;
+
+ if (hasError) return null;
+
+ return (
+
setHasError(true)}
+ {...props}
+ />
+ );
+};
+
+export default AuthenticatedImage;
diff --git a/packages/react/src/views/AttachmentHandler/ImageAttachment.js b/packages/react/src/views/AttachmentHandler/ImageAttachment.js
index 84516ce65a..c5d40220c7 100644
--- a/packages/react/src/views/AttachmentHandler/ImageAttachment.js
+++ b/packages/react/src/views/AttachmentHandler/ImageAttachment.js
@@ -1,10 +1,11 @@
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, useEffect } from 'react';
import { css } from '@emotion/react';
import PropTypes from 'prop-types';
import { Box, Avatar, useTheme } from '@embeddedchat/ui-elements';
import AttachmentMetadata from './AttachmentMetadata';
import ImageGallery from '../ImageGallery/ImageGallery';
import RCContext from '../../context/RCInstance';
+import AuthenticatedImage from './AuthenticatedImage';
const ImageAttachment = ({
attachment,
@@ -16,6 +17,7 @@ const ImageAttachment = ({
}) => {
const { RCInstance } = useContext(RCContext);
const [showGallery, setShowGallery] = useState(false);
+
const getUserAvatarUrl = (icon) => {
const instanceHost = RCInstance.getHost();
const URL = `${instanceHost}${icon}`;
@@ -95,8 +97,8 @@ const ImageAttachment = ({
{isExpanded && (
setShowGallery(true)}>
-
-
{
const { theme } = useTheme();
@@ -104,7 +105,7 @@ const ImageGallery = ({ currentFileId, setShowGallery }) => {
{files.map(({ _id, url }) => (
-
+
))}