Files
yaak-mountain-loop/apps/yaak-client/components/responseViewers/PdfViewer.tsx
T
Gregory Schier 448d349af8 Address response bodies by response id instead of a filesystem path
The body commands took a path from the client, so a token holder could
read any file the process could. They now take a response id and resolve
the location themselves, and the UI never sees a path at all: the desktop
host asks the backend where the file is, and the bridge fetches
/responses/:id/body.

Ephemeral responses (GraphQL introspection) never reach the database, so
resolution falls back to the path send writes them to.
2026-08-14 17:02:05 -07:00

82 lines
2.4 KiB
TypeScript

import "react-pdf/dist/Page/TextLayer.css";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "./PdfViewer.css";
import type { PDFDocumentProxy } from "pdfjs-dist";
import { useMemo, useRef, useState } from "react";
import { Document, Page } from "react-pdf";
import { useContainerSize } from "@yaakapp-internal/ui";
import { fireAndForget } from "../../lib/fireAndForget";
fireAndForget(
import("react-pdf").then(({ pdfjs }) => {
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url,
).toString();
}),
);
interface Props {
/** A URL the host resolved, for a body it already stored. */
url?: string;
data?: Uint8Array;
}
const options = {
cMapUrl: "/cmaps/",
standardFontDataUrl: "/standard_fonts/",
};
export function PdfViewer({ url, data }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [numPages, setNumPages] = useState<number>();
const { width: containerWidth } = useContainerSize(containerRef);
// During render, not in an effect: an effect leaves the first paint with no file, and
// `Document` renders its "Failed to load PDF file" state for that frame before recovering
const src = useMemo(() => {
if (url) {
return url;
}
if (data) {
// Create a copy to avoid "Buffer is already detached" errors
// This happens when the ArrayBuffer is transferred/detached elsewhere
return { data: new Uint8Array(data) };
}
return undefined;
}, [url, data]);
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
setNumPages(nextNumPages);
};
// Nothing to show yet, rather than the failure state `Document` renders for an empty file
if (src == null) {
return null;
}
return (
<div ref={containerRef} className="w-full h-full overflow-y-auto">
<Document
file={src}
options={options}
onLoadSuccess={onDocumentLoadSuccess}
externalLinkTarget="_blank"
externalLinkRel="noopener noreferrer"
>
{Array.from({ length: numPages ?? 0 }, (_, index) => (
<Page
className="mb-6 select-all"
renderTextLayer
renderAnnotationLayer
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth}
/>
))}
</Document>
</div>
);
}