diff --git a/.eslintrc b/.eslintrc index 3cf00050..5af1e125 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,6 @@ { "globals": { + "atob": true, "Blob": true, "fetch": true, "window": true, @@ -24,6 +25,7 @@ "no-param-reassign": 0, "consistent-return": 0, "no-nested-ternary": 0, - "react/prop-types": 0 + "react/prop-types": 0, + "no-plusplus": 0 } } diff --git a/firebase.json b/firebase.json index 1abe7e7a..4e4e65ed 100644 --- a/firebase.json +++ b/firebase.json @@ -43,15 +43,6 @@ "value": "cache-control: public, max-age=0, must-revalidate" } ] - }, - { - "source": "page-data/**", - "headers": [ - { - "key": "cache-control", - "value": "cache-control: public, max-age=0, must-revalidate" - } - ] } ] }], diff --git a/functions/index.js b/functions/index.js index 75cc8d06..676b3eb4 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,26 +1,31 @@ const functions = require('firebase-functions'); const puppeteer = require('puppeteer'); -const cors = require('cors')({ origin: 'https://rxresume-staging.web.app' }); -const BASE_URL = 'https:/rxresu.me/r/'; +const BASE_URL = 'https://rxresume-staging.web.app/r/'; function timeout(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -exports.printSinglePageResume = functions.https.onRequest((req, res) => { - if (req.method !== 'POST') { - return res.status(403).send('Forbidden!'); - } +exports.printSinglePageResume = functions.https.onCall( + async ({ id }, { auth }) => { + if (!id) { + throw new functions.https.HttpsError( + 'invalid-argument', + 'The function must be called with one arguments "id" containing the resume ID.', + ); + } - if (!req.query.id) { - return res.status(400).send('Bad Request!'); - } + if (!auth) { + throw new functions.https.HttpsError( + 'failed-precondition', + 'The function must be called while authenticated.', + ); + } - async function run() { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); - await page.goto(BASE_URL + req.query.id); + await page.goto(BASE_URL + id); await timeout(5000); await page.emulateMediaType('print'); const height = await page.evaluate(() => { @@ -44,48 +49,36 @@ exports.printSinglePageResume = functions.https.onRequest((req, res) => { pageRanges: '1', }); await browser.close(); - return pdf; - } + return Buffer.from(pdf).toString('base64'); + }, +); - return cors(req, res, async () => { - const pdf = await run(); - res.set({ - 'Content-Type': 'application/pdf', - 'Content-Length': pdf.length, - }); - return res.send(pdf); - }); -}); +exports.printMultiPageResume = functions.https.onCall( + async ({ id }, { auth }) => { + if (!id) { + throw new functions.https.HttpsError( + 'invalid-argument', + 'The function must be called with one arguments "id" containing the resume ID.', + ); + } -exports.printMultiPageResume = functions.https.onRequest((req, res) => { - if (req.method !== 'POST') { - return res.status(403).send('Forbidden!'); - } + if (!auth) { + throw new functions.https.HttpsError( + 'failed-precondition', + 'The function must be called while authenticated.', + ); + } - if (!req.query.id) { - return res.status(400).send('Bad Request!'); - } - - async function run() { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); - await page.goto(BASE_URL + req.query.id); + await page.goto(BASE_URL + id); await timeout(5000); await page.emulateMediaType('print'); const pdf = await page.pdf({ + format: 'A4', printBackground: true, - width: `21cm`, }); await browser.close(); - return pdf; - } - - return cors(req, res, async () => { - const pdf = await run(); - res.set({ - 'Content-Type': 'application/pdf', - 'Content-Length': pdf.length, - }); - return res.send(pdf); - }); -}); + return Buffer.from(pdf).toString('base64'); + }, +); diff --git a/functions/package-lock.json b/functions/package-lock.json index b9d70d0a..8c4882b8 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -1875,14 +1875,6 @@ "agent-base": "5", "debug": "4" } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } } } }, @@ -1937,6 +1929,14 @@ "through2": "^3.0.1" } }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/gatsby-browser.js b/gatsby-browser.js index e3886aff..05ab514c 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -3,6 +3,7 @@ import 'animate.css'; import 'firebase/analytics'; import 'firebase/auth'; import 'firebase/database'; +import 'firebase/functions'; import 'firebase/storage'; import React from 'react'; import { DatabaseProvider } from './src/contexts/DatabaseContext'; @@ -11,10 +12,10 @@ import { ResumeProvider } from './src/contexts/ResumeContext'; import { StorageProvider } from './src/contexts/StorageContext'; import { ThemeProvider } from './src/contexts/ThemeContext'; import { UserProvider } from './src/contexts/UserContext'; +import './src/styles/global.css'; import './src/styles/shadows.css'; import './src/styles/tailwind.css'; import './src/styles/toastify.css'; -import './src/styles/global.css'; const theme = createMuiTheme({ typography: { diff --git a/gatsby-ssr.js b/gatsby-ssr.js index ed78c271..75f5bb39 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.js @@ -1,4 +1,5 @@ import 'firebase/analytics'; import 'firebase/auth'; import 'firebase/database'; +import 'firebase/functions'; import 'firebase/storage'; diff --git a/src/modals/sections/ExportModal.js b/src/modals/sections/ExportModal.js index 693fa324..e037882d 100644 --- a/src/modals/sections/ExportModal.js +++ b/src/modals/sections/ExportModal.js @@ -1,9 +1,11 @@ +import firebase from 'gatsby-plugin-firebase'; import { clone } from 'lodash'; import React, { memo, useContext, useEffect, useState } from 'react'; import { FaPrint } from 'react-icons/fa'; import Button from '../../components/shared/Button'; import ModalContext from '../../contexts/ModalContext'; import { useSelector } from '../../contexts/ResumeContext'; +import { b64toBlob } from '../../utils'; import BaseModal from '../BaseModal'; const ExportModal = () => { @@ -11,7 +13,6 @@ const ExportModal = () => { const [open, setOpen] = useState(false); const [isLoadingSingle, setLoadingSingle] = useState(false); const [isLoadingMulti, setLoadingMulti] = useState(false); - const functionsUrl = 'https://us-central1-rx-resume.cloudfunctions.net'; const { emitter, events } = useContext(ModalContext); @@ -37,20 +38,22 @@ const ExportModal = () => { const handleSinglePageDownload = async () => { setLoadingSingle(true); - fetch(`${functionsUrl}/printSinglePageResume?id=${state.id}`, { - method: 'POST', - }) - .then((response) => response.blob()) - .then(openFile); + const printSinglePageResume = firebase + .functions() + .httpsCallable('printSinglePageResume'); + const { data } = await printSinglePageResume({ id: state.id }); + const blob = b64toBlob(data, 'application/pdf'); + openFile(blob); }; const handleMultiPageDownload = async () => { setLoadingMulti(true); - fetch(`${functionsUrl}/printMultiPageResume?id=${state.id}`, { - method: 'POST', - }) - .then((response) => response.blob()) - .then(openFile); + const printMultiPageResume = firebase + .functions() + .httpsCallable('printMultiPageResume'); + const { data } = await printMultiPageResume({ id: state.id }); + const blob = b64toBlob(data, 'application/pdf'); + openFile(blob); }; const handleExportToJson = () => { diff --git a/src/utils/index.js b/src/utils/index.js index 86b25647..132c8ad4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -69,3 +69,23 @@ export const move = ( return result; }; + +export const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; +};