From 63b46e6d0c140f849842e44d85e3df3776f27cbc Mon Sep 17 00:00:00 2001 From: Akemmanuel Date: Sun, 14 Jun 2026 15:38:55 -0300 Subject: [PATCH] fix: render uploaded image previews --- server/web-server.ts | 34 +++++++++++++------ src/components/ImageMentionPreview.tsx | 15 ++++++-- src/components/PromptBox.tsx | 19 ++++++++--- src/components/PromptImageMentions.tsx | 4 +++ src/components/message-list/FilePartView.tsx | 14 +++++--- src/components/message-list/MessageBubble.tsx | 20 +++++++++-- src/components/message-list/PartView.tsx | 12 ++++++- src/components/message-list/TextPartView.tsx | 9 +++-- .../message-list/tools/ToolPartView.tsx | 8 +++-- .../message-list/tools/imageAttachments.ts | 3 +- .../message-list/tools/toolPresentation.ts | 3 +- src/hooks/use-prompt-files.ts | 9 +++-- src/lib/__tests__/attachment-src.test.ts | 6 ++++ src/lib/attachment-src.ts | 2 ++ 14 files changed, 124 insertions(+), 34 deletions(-) diff --git a/server/web-server.ts b/server/web-server.ts index 414c77b..4291223 100644 --- a/server/web-server.ts +++ b/server/web-server.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import { randomUUID } from "node:crypto"; -import { existsSync } from "node:fs"; +import { existsSync, mkdirSync, realpathSync } from "node:fs"; import { mkdir, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path"; @@ -416,12 +416,28 @@ const servesFrontend = !["api", "api-only", "backend", "backend-only"].includes( const authToken = process.env.OPENGUI_AUTH_TOKEN?.trim() || ""; const allowedCorsOrigin = process.env.OPENGUI_CORS_ORIGIN?.trim() || "*"; +const uploadDirectory = join(tmpdir(), "opengui-uploads"); +mkdirSync(uploadDirectory, { recursive: true }); + +function addCanonicalRootVariants(root: string) { + const resolved = resolve(root); + try { + const real = realpathSync(resolved); + return real === resolved ? [resolved] : [resolved, real]; + } catch { + return [resolved]; + } +} + function parseAllowedRoots() { const raw = process.env.OPENGUI_ALLOWED_ROOTS || homedir(); - return raw - .split(",") - .map((entry) => resolve(entry.trim())) - .filter(Boolean); + return Array.from( + new Set( + [...raw.split(",").map((entry) => entry.trim()), uploadDirectory] + .filter(Boolean) + .flatMap(addCanonicalRootVariants), + ), + ); } const allowedRoots = parseAllowedRoots(); @@ -1575,8 +1591,7 @@ app.get("/api/fs/file", async (c) => { ? inputPath : join(await resolveSafeDirectory(directory), inputPath); const actual = await realpath(requestedPath); - const allowed = allowedRoots.some((root) => actual === root || actual.startsWith(`${root}/`)); - if (!allowed) throw new Error("Path outside OPENGUI_ALLOWED_ROOTS"); + if (!isWithinAllowedRoot(actual)) throw new Error("Path outside OPENGUI_ALLOWED_ROOTS"); const info = await stat(actual); if (!info.isFile()) throw new Error("Path is not a file"); return new Response(await readFile(actual), { @@ -1615,8 +1630,7 @@ app.post("/api/fs/upload", async (c) => { if (file.size > uploadMaxFileBytes) throw new Error("File exceeds size limit"); } - const dir = join(tmpdir(), "opengui-uploads"); - await mkdir(dir, { recursive: true }); + await mkdir(uploadDirectory, { recursive: true }); const uploaded: string[] = []; for (const file of files) { @@ -1624,7 +1638,7 @@ app.post("/api/fs/upload", async (c) => { const extension = extname(originalName) .replace(/[^a-zA-Z0-9.]/g, "") .slice(0, 24); - const filePath = join(dir, `${randomUUID()}${extension}`); + const filePath = join(uploadDirectory, `${randomUUID()}${extension}`); await writeFile(filePath, Buffer.from(await file.arrayBuffer())); uploaded.push(filePath); } diff --git a/src/components/ImageMentionPreview.tsx b/src/components/ImageMentionPreview.tsx index 3176bdb..e7611bd 100644 --- a/src/components/ImageMentionPreview.tsx +++ b/src/components/ImageMentionPreview.tsx @@ -13,11 +13,13 @@ function ImageLightbox({ image, serverUrl, baseDirectory, + authToken, onClose, }: { image: ImageMention; serverUrl?: string | null; baseDirectory?: string | null; + authToken?: string | null; onClose: () => void; }) { React.useEffect(() => { @@ -28,7 +30,7 @@ function ImageLightbox({ return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); - const src = resolveAttachmentImageSrc(image.path, serverUrl, baseDirectory); + const src = resolveAttachmentImageSrc(image.path, serverUrl, baseDirectory, authToken); return createPortal(
void; onOpen?: (image: ImageMention) => void; @@ -99,6 +103,7 @@ export function ImageMentionToken({ image={localOpenImage} serverUrl={serverUrl} baseDirectory={baseDirectory} + authToken={authToken} onClose={() => setLocalOpenImage(null)} /> )} @@ -110,6 +115,7 @@ export function ImageMentionThumbnails({ images, serverUrl, baseDirectory, + authToken, activePath, onHover, onOpen, @@ -118,6 +124,7 @@ export function ImageMentionThumbnails({ images: ImageMention[]; serverUrl?: string | null; baseDirectory?: string | null; + authToken?: string | null; activePath?: string | null; onHover?: (path: string | null) => void; onOpen?: (image: ImageMention) => void; @@ -130,7 +137,7 @@ export function ImageMentionThumbnails({ <>
{images.map((image) => { - const src = resolveAttachmentImageSrc(image.path, serverUrl, baseDirectory); + const src = resolveAttachmentImageSrc(image.path, serverUrl, baseDirectory, authToken); const active = activePath === image.path; return (