♻️(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:
@@ -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
|
||||||
|
|||||||
@@ -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/>
|
||||||
@@ -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`);
|
||||||
|
|
||||||
|
|||||||
@@ -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)');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './exportResolveFileUrl';
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './api';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user