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
16 changes: 16 additions & 0 deletions packages/auth/src/RocketChatAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
80 changes: 80 additions & 0 deletions packages/react/.storybook/middleware.js
Original file line number Diff line number Diff line change
@@ -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');
}
});
};
24 changes: 24 additions & 0 deletions packages/react/src/views/AttachmentHandler/AuthenticatedImage.js
Original file line number Diff line number Diff line change
@@ -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 (
<img
src={proxyUrl}
alt={alt}
onError={() => setHasError(true)}
{...props}
/>
);
};

export default AuthenticatedImage;
12 changes: 7 additions & 5 deletions packages/react/src/views/AttachmentHandler/ImageAttachment.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}`;
Expand Down Expand Up @@ -95,8 +97,8 @@ const ImageAttachment = ({
</Box>
{isExpanded && (
<Box onClick={() => setShowGallery(true)}>
<img
src={host + attachment.image_url}
<AuthenticatedImage
url={host + attachment.image_url}
style={{
maxWidth: '100%',
objectFit: 'contain',
Expand Down Expand Up @@ -159,8 +161,8 @@ const ImageAttachment = ({
}
variantStyles={variantStyles}
/>
<img
src={host + nestedAttachment.image_url}
<AuthenticatedImage
url={host + nestedAttachment.image_url}
style={{
maxWidth: '100%',
objectFit: 'contain',
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/views/ImageGallery/ImageGallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { useRCContext } from '../../context/RCInstance';
import { Swiper, SwiperSlide } from './Swiper';
import getImageGalleryStyles from './ImageGallery.styles';
import AuthenticatedImage from '../AttachmentHandler/AuthenticatedImage';

const ImageGallery = ({ currentFileId, setShowGallery }) => {
const { theme } = useTheme();
Expand Down Expand Up @@ -104,7 +105,7 @@ const ImageGallery = ({ currentFileId, setShowGallery }) => {
{files.map(({ _id, url }) => (
<SwiperSlide key={_id}>
<Box css={styles.imageContainer}>
<img src={url} css={styles.image} />
<AuthenticatedImage url={url} css={styles.image} />
</Box>
</SwiperSlide>
))}
Expand Down