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 ( + {alt} 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 }) => ( - + ))}