♻️(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
- 🧑‍💻(frontend) change literal section open source #702
- ♻️(frontend) replace cors proxy for export #695
## Fixed
- 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(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/>

View File

@@ -136,6 +136,11 @@ test.describe('Doc Export', () => {
test('it exports the docs with images', async ({ page, browserName }) => {
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 downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
@@ -160,6 +165,14 @@ test.describe('Doc Export', () => {
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
.getByRole('button', {
name: 'download',
@@ -188,6 +201,8 @@ test.describe('Doc Export', () => {
})
.click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
const download = await downloadPromise;
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,
* 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) {
window.open(url);
} else {
@@ -70,11 +70,11 @@ export const FileDownloadButton = ({
}
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');
} else {
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)');
};

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

View File

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

View File

@@ -17,27 +17,6 @@ export function downloadFile(blob: Blob, filename: string) {
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(
props: Partial<DefaultProps>,
colors: typeof COLORS_DEFAULT,