♻️(frontend) replace cors proxy for export

We were using the cors proxy of Blocknote.js
to export the document. Now we use our own proxy
to avoid CORS issues.
This commit is contained in:
Anthony LC
2025-03-13 11:52:11 +01:00
committed by Anthony LC
parent 6efc2377fe
commit 9176328200
9 changed files with 57 additions and 40 deletions

View File

@@ -11,8 +11,10 @@ and this project adheres to
## Changed ## Changed
- 🧑‍💻(frontend) change literal section open source #702 - 🧑‍💻(frontend) change literal section open source #702
- ♻️(frontend) replace cors proxy for export #695
## Fixed ## Fixed
- 🐛(frontend) remove scroll listener table content #688 - 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690 - 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(backend) refactor to fix filtering on children - 🐛(backend) refactor to fix filtering on children

View File

@@ -1,2 +1,2 @@
<img width="200" src="https://impress-staging.beta.numerique.gouv.fr/assets/logo-gouv.png" /> <img width="200" src="http://localhost:3000/assets/logo-gouv.png" />
<br/> <br/>

View File

@@ -136,6 +136,11 @@ test.describe('Doc Export', () => {
test('it exports the docs with images', async ({ page, browserName }) => { test('it exports the docs with images', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1); const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const responseCorsPromise = page.waitForResponse(
(response) =>
response.url().includes('/cors-proxy/') && response.status() === 200,
);
const fileChooserPromise = page.waitForEvent('filechooser'); const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download', (download) => { const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`); return download.suggestedFilename().includes(`${randomDoc}.pdf`);
@@ -160,6 +165,14 @@ test.describe('Doc Export', () => {
await expect(image).toBeVisible(); await expect(image).toBeVisible();
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByRole('tab', { name: 'Embed' }).click();
await page
.getByRole('textbox', { name: 'Enter URL' })
.fill('https://docs.numerique.gouv.fr/assets/logo-gouv.png');
await page.getByText('Embed image').click();
await page await page
.getByRole('button', { .getByRole('button', {
name: 'download', name: 'download',
@@ -188,6 +201,8 @@ test.describe('Doc Export', () => {
}) })
.click(); .click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
const download = await downloadPromise; const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);

View File

@@ -57,7 +57,7 @@ export const FileDownloadButton = ({
* If not hosted on our domain, means not a file uploaded by the user, * If not hosted on our domain, means not a file uploaded by the user,
* we do what Blocknote was doing initially. * we do what Blocknote was doing initially.
*/ */
if (!url.includes(window.location.hostname)) { if (!url.includes(window.location.hostname) && !url.includes('base64')) {
if (!editor.resolveFileUrl) { if (!editor.resolveFileUrl) {
window.open(url); window.open(url);
} else { } else {
@@ -70,11 +70,11 @@ export const FileDownloadButton = ({
} }
if (!url.includes('-unsafe')) { if (!url.includes('-unsafe')) {
const blob = (await exportResolveFileUrl(url, undefined)) as Blob; const blob = (await exportResolveFileUrl(url)) as Blob;
downloadFile(blob, url.split('/').pop() || 'file'); downloadFile(blob, url.split('/').pop() || 'file');
} else { } else {
const onConfirm = async () => { const onConfirm = async () => {
const blob = (await exportResolveFileUrl(url, undefined)) as Blob; const blob = (await exportResolveFileUrl(url)) as Blob;
downloadFile(blob, url.split('/').pop() || 'file (unsafe)'); downloadFile(blob, url.split('/').pop() || 'file (unsafe)');
}; };

View File

@@ -0,0 +1,30 @@
import { baseApiUrl } from '@/api';
import { Doc } from '@/features/docs/doc-management';
export const exportCorsResolveFileUrl = async (
docId: Doc['id'],
url: string,
) => {
let resolvedUrl = url;
// If the url is not from the same origin, better to proxy the request
// to avoid CORS issues
if (!url.includes(window.location.hostname) && !url.includes('base64')) {
resolvedUrl = `${baseApiUrl()}documents/${docId}/cors-proxy/?url=${encodeURIComponent(url)}`;
}
return exportResolveFileUrl(resolvedUrl);
};
export const exportResolveFileUrl = async (url: string) => {
try {
const response = await fetch(url, {
credentials: 'include',
});
return response.blob();
} catch {
console.error(`Failed to fetch image: ${url}`);
}
return url;
};

View File

@@ -0,0 +1 @@
export * from './exportResolveFileUrl';

View File

@@ -18,10 +18,11 @@ import { Box, Text } from '@/components';
import { useEditorStore } from '@/features/docs/doc-editor'; import { useEditorStore } from '@/features/docs/doc-editor';
import { Doc, useTrans } from '@/features/docs/doc-management'; import { Doc, useTrans } from '@/features/docs/doc-management';
import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates'; import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
import { docxDocsSchemaMappings } from '../mappingDocx'; import { docxDocsSchemaMappings } from '../mappingDocx';
import { pdfDocsSchemaMappings } from '../mappingPDF'; import { pdfDocsSchemaMappings } from '../mappingPDF';
import { downloadFile, exportResolveFileUrl } from '../utils'; import { downloadFile } from '../utils';
enum DocDownloadFormat { enum DocDownloadFormat {
PDF = 'pdf', PDF = 'pdf',
@@ -88,26 +89,14 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
let blobExport: Blob; let blobExport: Blob;
if (format === DocDownloadFormat.PDF) { if (format === DocDownloadFormat.PDF) {
const defaultExporter = new PDFExporter(
editor.schema,
pdfDocsSchemaMappings,
);
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, { const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
resolveFileUrl: async (url) => resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
}); });
const pdfDocument = await exporter.toReactPDFDocument(exportDocument); const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
blobExport = await pdf(pdfDocument).toBlob(); blobExport = await pdf(pdfDocument).toBlob();
} else { } else {
const defaultExporter = new DOCXExporter(
editor.schema,
docxDocsSchemaMappings,
);
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, { const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
resolveFileUrl: async (url) => resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
}); });
blobExport = await exporter.toBlob(exportDocument); blobExport = await exporter.toBlob(exportDocument);

View File

@@ -1,2 +1,3 @@
export * from './api';
export * from './components'; export * from './components';
export * from './utils'; export * from './utils';

View File

@@ -17,27 +17,6 @@ export function downloadFile(blob: Blob, filename: string) {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} }
export const exportResolveFileUrl = async (
url: string,
resolveFileUrl: ((url: string) => Promise<string | Blob>) | undefined,
) => {
if (!url.includes(window.location.hostname) && resolveFileUrl) {
return resolveFileUrl(url);
}
try {
const response = await fetch(url, {
credentials: 'include',
});
return response.blob();
} catch {
console.error(`Failed to fetch image: ${url}`);
}
return url;
};
export function docxBlockPropsToStyles( export function docxBlockPropsToStyles(
props: Partial<DefaultProps>, props: Partial<DefaultProps>,
colors: typeof COLORS_DEFAULT, colors: typeof COLORS_DEFAULT,