feat: add classroomio's server to the monorepo (#168)

* add classroomio server to apps

* ReadME upadte

* Update README.md

* rename project to backend

* fix lock file

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2024-02-28 03:25:25 +01:00
committed by GitHub
co-authored by Rotimi Best
parent d040f9f12e
commit 3a22a73380
24 changed files with 8177 additions and 62 deletions
@@ -0,0 +1,35 @@
const express = require('express');
const zod = require('zod');
const { generateCertificate } = require('../utils/certificate');
const router = express.Router();
router.post('/', async (req, res) => {
try {
// Validate
const mySchema = zod.object({
theme: zod.string(),
studentName: zod.string(),
courseName: zod.string(),
courseDescription: zod.string(),
orgLogoUrl: zod.string(),
orgName: zod.string(),
});
console.log('req.body', req.body);
mySchema.parse(req.body);
const pdfBuffer = await generateCertificate(req.body);
res.contentType('application/pdf');
res.send(pdfBuffer);
} catch (error) {
console.error('Error', error);
res.json({
success: false,
error,
});
}
});
module.exports = router;
+42
View File
@@ -0,0 +1,42 @@
const express = require('express');
const zod = require('zod');
const { generateCoursePdf } = require('../utils/course.js');
const router = express.Router();
// localhost:3002/downloadCourse
router.post('/', async (req, res) => {
try {
// Validate
const mySchema = zod.object({
courseTitle: zod.string(),
orgName: zod.string(),
orgTheme: zod.string(),
lessons: zod.array(
zod.object({
lessonTitle: zod.string(),
lessonNumber: zod.string(),
lessonNote: zod.string(),
slideUrl: zod.string().optional(),
video: zod.string().array().optional(),
})
),
});
console.log('req.body', req.body);
mySchema.parse(req.body);
const pdfBuffer = await generateCoursePdf(req.body);
res.contentType('application/pdf');
res.send(pdfBuffer);
} catch (error) {
console.error('Error', error);
res.status(400).json({
success: false,
error,
});
}
});
module.exports = router;
+47
View File
@@ -0,0 +1,47 @@
const express = require('express');
const zod = require('zod');
const { generateLessonPdf } = require('../utils/lesson');
const router = express.Router();
const lesson = {
title: '',
number: '01',
orgName: '',
note: '',
slideUrl: '',
video: [],
courseTitle: '',
};
// localhost:3002/downloadLesson
router.post('/', async (req, res) => {
try {
// Validate
const mySchema = zod.object({
title: zod.string(),
number: zod.string(),
orgName: zod.string(),
note: zod.string(),
slideUrl: zod.string().optional(),
video: zod.string().array().optional(),
courseTitle: zod.string(),
});
console.log('req.body', req.body);
mySchema.parse(req.body);
const pdfBuffer = await generateLessonPdf(req.body);
res.contentType('application/pdf');
res.send(pdfBuffer);
} catch (error) {
console.error('Error', error);
res.status(400).json({
success: false,
error,
});
}
});
module.exports = router;
+251
View File
@@ -0,0 +1,251 @@
const express = require('express');
const FormData = require('form-data');
const axios = require('axios');
// const Queue = require('bull');
const { promisify } = require('util');
const fs = require('fs');
const unlinkAsync = promisify(fs.unlink);
const {
S3Client,
PutObjectCommand,
// GetObjectCommand,
} = require('@aws-sdk/client-s3');
// const { getSupabase } = require('../utils/supabase');
const genUniqueId = require('../utils/genUniqueId');
const upload = require('../utils/upload');
const {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY,
} = process.env;
const router = express.Router();
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY,
},
});
// const supabase = getSupabase();
// const museUploadQueue = new Queue('video', process.env.REDIS_API_KEY, {
// redis: { tls: { rejectUnauthorized: false } },
// });
router.post('/', upload.single('videoFile'), async (req, res) => {
const {
file,
query: { lessonId },
} = req;
console.log('lessonId', lessonId);
console.log('file', file);
if (!file?.fieldname || !file?.fieldname) {
return res.status(400).json({
success: false,
message: 'You must provide a file to upload',
});
}
if (file?.size > 30 * 1024 * 1024) {
return res.status(400).json({
success: false,
type: 'FILE_TOO_LARGE',
message: 'File is too large',
});
}
const fileData = fs.readFileSync(file.path);
const fileName = `${genUniqueId()}-${file.originalname}`;
const params = {
Bucket: 'videos',
Key: fileName,
Body: fileData,
};
const fileUrl = `https://pub-${CLOUDFLARE_PUBLIC_ACCOUNT_ID}.r2.dev/${fileName}`;
let metadata = {};
try {
const s3Upload = S3.send(new PutObjectCommand(params));
const museUpload = uploadToMuseNow(fileData, file.originalname);
Promise.all([s3Upload, museUpload]).then((values) => {
const [s3UploadRes, museUploadRes] = values;
// Delete file saved by mutler in file
unlinkAsync(file.path);
console.log('s3UploadRes', s3UploadRes);
console.log('museUploadRes', museUploadRes);
if (museUploadRes.metadata) {
metadata = museUploadRes.metadata;
}
res.json({
success: true,
url: fileUrl,
metadata,
message: 'Uploaded successfully',
});
});
} catch (error) {
// error handling.
const { requestId, cfId, extendedRequestId } = error.$$metadata || {};
console.error('Error uploading file', error);
console.log({ requestId, cfId, extendedRequestId });
res.json({
success: false,
message: 'Uploaded failed: ' + error,
});
} finally {
// finally.
console.log('File upload complete');
// console.log('Add muse queue');
// museUploadQueue.add({
// fileName,
// originalFileName: file.originalname,
// lessonId,
// fileUrl,
// unlinkPath: file.path,
// });
}
});
router.get('/', (req, res) => {
res.send('Get upload video');
});
async function uploadToMuseNow(fileData, fileName) {
try {
// Add file data to form
const form = new FormData();
form.append('file', fileData, fileName);
// Send to muse through axios - fetch is a pain in the ass
const response = await axios({
method: 'POST',
url: 'https://api.muse.ai/api/files/upload',
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: {
Key: MUSE_API_KEY,
},
});
return {
metadata: response.data,
};
} catch (error) {
console.log('Upload to Muse failed', error?.response?.data);
return {};
}
}
// /**
// * Uploads video to muse.ai and store response in DB
// *
// * @param {Blod} s3Data AWS s3 data
// * @param {string} fileName Name of the file
// * @param {string} lessonId Associated lesson this video was uploaded to
// * @param {string} fileUrl URL to the video in s3
// * @param {string} unlinkPath Path to mutler file to delete
// */
// async function uploadToMuse(s3Key, fileName, lessonId, fileUrl, unlinkPath) {
// try {
// await unlinkAsync(unlinkPath);
// const s3Data = await S3.send(
// new GetObjectCommand({
// Bucket: 'videos',
// Key: s3Key,
// })
// );
// // Add file data to form
// const form = new FormData();
// form.append('file', s3Data.Body, fileName);
// // Send to muse through axios - fetch is a pain in the ass
// const response = await axios({
// method: 'POST',
// url: 'https://api.muse.ai/api/files/upload',
// data: form,
// maxContentLength: Infinity,
// maxBodyLength: Infinity,
// headers: {
// Key: MUSE_API_KEY,
// },
// });
// console.log('Upload to muse complete', response.data);
// const { data: lesson } = await supabase
// .from('lesson')
// .select(`*`)
// .eq('id', lessonId)
// .single();
// if (lesson) {
// console.log('Found lesson');
// // Add Muse response to metadata
// const videos = lesson.videos.map((v) => {
// if (v.link === fileUrl) {
// console.log('matches');
// v.metadata = JSON.parse(JSON.stringify(response.data));
// }
// return v;
// });
// // Save uploaded data to DB
// const { error } = await supabase
// .from('lesson')
// .update({ videos })
// .eq('id', lessonId)
// .select();
// if (error) {
// console.log('Error updating lesson', error);
// } else {
// console.log('Update DB complete');
// }
// }
// } catch (error) {
// console.log('Upload to Muse failed', error?.response?.data);
// }
// }
// // Queue to Upload to muse.ai
// museUploadQueue.process(async function (job, done) {
// console.log('Initilizing Muse Request\n\n', job.data);
// const { fileName, originalFileName, lessonId, fileUrl, unlinkPath } =
// job.data;
// await uploadToMuse(fileName, originalFileName, lessonId, fileUrl, unlinkPath);
// done();
// });
// museUploadQueue.on('completed', function (job) {
// // Job completed with output result!
// console.log('Completed all jobs in uploading to muse', job.data);
// });
// museUploadQueue.on('error', function (error) {
// // Job completed with output result!
// console.log('error on queue', error);
// });
module.exports = router;
+148
View File
@@ -0,0 +1,148 @@
const { getPdfBuffer } = require('./puppeteer');
function getHtmlTemplate(body) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Qwitcher+Grypen&family=Roboto:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.tailwindcss.com"></script>
<title>Certificate</title>
<style>
body {
width: 1123px;
height: 794px;
}
.student-name {
font-family: 'Qwitcher Grypen', cursive;
}
</style>
</head>
<body>
${body}
</body>
</html>
`;
}
function getProfessionalTheme({
studentName,
courseName,
courseDescription,
orgLogoUrl,
orgName,
}) {
const themeBody = `
<div class="flex items-center justify-center border border-dashed border-blue-400 h-full w-full">
<div class="w-full h-full flex border-l-8 border-blue-800">
<div class="h-full w-full px-5 py-10">
<p class="text-2xl font-normal my-2 mb-10 uppercase text-gray-500">
Certificate<br /> of completion
</p>
<div class="border-b border-gray-500 mb-4">
<p class="text-xl text-black font-normal">This is to certify that</p>
<p class="student-name text-center text-9xl">${studentName}</p>
</div>
<div class="mb-2">
<p class="text-xl text-black font-normal">
has successfully completed introductory courses in
</p>
<div>
<p class="bg-transparent font-semibold text-blue-800 text-center uppercase my-10 text-3xl">
${courseName}
</p>
</div>
</div>
<p class="text-xl font-light mb-4 text-justify">
${courseDescription}
</p>
<div class="flex items-center gap-1 mt-14">
<img
src="${orgLogoUrl}"
alt="logo" class="w-24 h-24 rounded-md" />
<p class="font-semibold capitalize text-5xl">${orgName}</p>
</div>
</div>
</div>
</div>
`;
return getHtmlTemplate(themeBody);
}
function getPlainTheme({
studentName,
courseName,
courseDescription,
orgLogoUrl,
orgName,
}) {
const themeBody = `
<div class="flex flex-col items-center justify-center border-4 border-pink-700 py-3 w-full h-full">
<div class="flex items-center gap-1 my-2 mb-10">
<img
src="${orgLogoUrl}"
alt="logo" class=" w-24 h-24 rounded-md" />
<p class="font-semibold capitalize text-5xl">${orgName}</p>
</div>
<div class="w-full px-5 py-3">
<p class="text-xl text-center font-normal my-2 uppercase tracking-widest text-gray-500 w-full">
Certificate of completion
</p>
<div class="border-b border-gray-500 mb-4">
<p class="text-xl text-black font-normal">This is to certify that</p>
<p class="student-name text-center text-8xl">${studentName}</p>
</div>
<div class="mb-2">
<p class="text-xl text-black font-normal">
has successfully completed a courses in
</p>
<div class="my-10">
<p class="bg-transparent font-semibold text-pink-700 text-center uppercase text-3xl">
${courseName}
</p>
</div>
</div>
<p class="text-xl text-center text-gray-500 font-light">${courseDescription}</p>
</div>
</div>
`;
return getHtmlTemplate(themeBody);
}
const generateCertificate = async ({
theme,
studentName,
courseName,
courseDescription,
orgLogoUrl,
orgName,
}) => {
const params = {
studentName,
courseName,
courseDescription,
orgLogoUrl,
orgName,
};
console.log('theme', theme);
const html =
theme === 'plain' ? getPlainTheme(params) : getProfessionalTheme(params);
console.log('body', html);
return await getPdfBuffer(html);
};
module.exports = {
generateCertificate,
};
+221
View File
@@ -0,0 +1,221 @@
// const { getPdfBuffer } = require('./puppeteer');
const { marked } = require('marked');
const puppeteer = require('puppeteer');
function getHtmlTemplate(body) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300;400&family=Roboto:wght@100;300;400;500;700;900&display=swap"
rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<title>Lesson Note</title>
<style>
html {
-webkit-print-color-adjust: exact;
}
body {
font-family: 'Roboto', sans-serif;
}
.prose h1 {
margin: 0;
}
.prose h2 {
margin: 0.5rem 0 1.5rem 0;
}
.prose h3 {
margin: 0.5rem 0;
}
.roboto-mono {
font-family: 'Roboto Mono', monospace;
}
span {
font-size: 0.8rem;
}
section {
margin-top: 5%;
}
.root {
width: 95%;
}
</style>
<style type="text/tailwindcss">
@tailwind base;
@tailwind base;
@tailwind components;
@tailwind utilities;
.prose {
max-width: unset;
}
</style>
</head>
<body class="w-full break-after-page">
${body}
</body>
</html>
`;
}
function getLessonBody(
{ lessonTitle, lessonNumber, lessonNote, slideUrl, video },
orgName,
orgTheme
) {
const noteHtml = marked.parse(lessonNote);
const showExtraResources = slideUrl || (video && video.length > 0);
return `
<div
class="bg-[${
orgTheme ? orgTheme : '#0030FF'
}] w-full h-40 flex flex-col items-center justify-center gap-2 rounded-lg m-0">
<h3
class="my-0 rounded-full w-10 text-center mx-auto py-1 text-[${
orgTheme ? orgTheme : '#0030FF'
}] bg-white text-md font-bold tracking-tighter">
${lessonNumber}
</h3>
<h1 class="text-white m-0">${lessonTitle}</h1>
<h4 class="roboto-mono text-md font-normal text-white m-0">By ${orgName}</h4>
</div>
<main class="break-after-page">
<section>
${noteHtml}
</section>
<section>
${
showExtraResources
? `
<section>
<h1 class='text-xl font-bold my-1'>Extra Resources</h1>
${
slideUrl
? `<div class="flex mt-2 mb-1">
<p class="m-0 mr-2">Slide Link:</p> <a href=${slideUrl} style="color: blue" class="underline">Open Here</a>
</div>`
: ''
}
${
video && video.length > 0
? `
<p class="m-0">Video Link:</p>
<div class="m-0">
<ol class="m-0">
${video
.map(
(videoData, index) => `
<li key=${
index + 1
}><a href=${videoData} style="color: blue" class="underline">${videoData}</a></li>
`
)
.join('')}
</ol>
</div>
`
: ''
}
</section>
`
: ''
}
</section>
</main>
`;
}
function getCourseBody({ courseTitle, orgName, orgTheme, lessons }) {
return `
<header class="flex justify-center items-center flex-col border-[${
orgTheme ? orgTheme : '#0030FF'
}] border-[40px] mx-3 h-[100vh] m-0">
<h1 class="font-bold text-8xl text-black m-0 mx-96">${courseTitle}</h1>
<h4 class="text-md text-2xl font-light text-black mt-5 ">${orgName}</h4>
</header>
<div class="root mx-auto mt-5 prose break-after-page">
${lessons.map((lesson) => getLessonBody(lesson, orgName, orgTheme))}
</div>
`;
}
async function generateSinglePdfFromHtml(htmlContent, courseTitle, orgTheme) {
const args = [
'--disable-web-security',
'--no-sandbox',
'--disable-setuid-sandbox',
];
const browser = await puppeteer.launch({
headless: 'new',
args,
});
const page = await browser.newPage();
await page.setContent(htmlContent);
const pdfOptions = {
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate:
"<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
footerTemplate: `
<div class="footer" style="position: relative; display: flex; text-align: right; margin-right: 5%; font-size: 8px; width: 297mm; padding-top: 30px;">
<div style="position: absolute; right: 50%; bottom: 3%;"><span class="pageNumber"></span>/<span class="totalPages"></span></div>
<div style="position: absolute; right: 0; bottom: 3%;">${courseTitle} | Powered by <a href="https://app.classroomio.com" style="text-decoration: underline;">ClassroomIO</a></div>
</div>
`,
margin: {
top: '0.3cm',
bottom: '0.5cm',
},
};
const numPages = await page.evaluate(() => {
const content = document.body; // Change this to the specific element containing content
const style = window.getComputedStyle(content);
const contentHeight = parseInt(style.height, 10);
const pageHeight = 900; // Set this to an appropriate value based on your PDF format
return Math.ceil(contentHeight / pageHeight);
});
const pagesPdfBuffer = [];
for (let currentPage = 1; currentPage <= numPages; currentPage++) {
await page.evaluate(() => {});
const pdfBuffer = await page.pdf(pdfOptions);
pagesPdfBuffer.push(pdfBuffer);
}
await browser.close();
return Buffer.concat(pagesPdfBuffer);
}
async function generateCoursePdf(params) {
// Generate the course boddy
const body = getCourseBody(params);
const html = getHtmlTemplate(body);
console.log(html);
return await generateSinglePdfFromHtml(html, params.courseTitle);
}
module.exports = {
generateCoursePdf,
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
+227
View File
@@ -0,0 +1,227 @@
// const { getPdfBuffer } = require('./puppeteer');
const { marked } = require('marked');
const puppeteer = require('puppeteer');
function getHtmlTemplate(body) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300;400&family=Roboto:wght@100;300;400;500;700;900&display=swap"
rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<title>Lesson Note</title>
<style>
html {
-webkit-print-color-adjust: exact;
}
header {
background-color: rgb(0, 48, 255);
}
body {
font-family: 'Roboto', sans-serif;
}
.prose h1 {
margin: 0;
}
.prose h2 {
margin: 0.5rem 0 1.5rem 0;
}
.prose h3 {
margin: 0.5rem 0;
}
.roboto-mono {
font-family: 'Roboto Mono', monospace;
}
span {
font-size: 0.8rem;
}
section {
margin-top: 5%;
}
.root {
width: 95%;
}
</style>
<style type="text/tailwindcss">
@tailwind base;
@tailwind base;
@tailwind components;
@tailwind utilities;
.prose {
max-width: unset;
}
</style>
</head>
<body class="w-full">
${body}
</body>
</html>
`;
}
function getLessonBody({
title,
number,
orgName,
note,
slideUrl,
video,
courseTitle,
}) {
const noteHtml = marked.parse(note);
const showExtraResources = slideUrl || (video && video.length > 0);
return `
<div class="root mx-auto mt-5 prose">
<header
class="w-full h-40 flex flex-col items-center justify-center gap-2 rounded-lg m-0">
<h3
class="my-0 rounded-full w-10 text-center mx-auto py-1 text-blue-700 bg-white text-md font-bold tracking-tighter">
${number}
</h3>
<h1 class="text-white m-0">${title}</h1>
<h4 class="roboto-mono text-md font-normal text-white m-0">By ${orgName}</h4>
</header>
<main>
<section>
${noteHtml}
</section>
<section>
${
showExtraResources
? `
<section>
<h1 class='text-xl font-bold my-1'>Extra Resources</h1>
${
slideUrl
? `<div class="flex mt-2 mb-1">
<p class="m-0 mr-2">Slide Link:</p> <a href=${slideUrl} style="color: blue" class="underline">Open Here</a>
</div>`
: ''
}
${
video && video.length > 0
? `
<p class="m-0">Video Link:</p>
<div class="m-0">
<ol class="m-0">
${video
.map(
(videoData, index) => `
<li key=${
index + 1
}><a href=${videoData} style="color: blue" class="underline">${videoData}</a></li>
`
)
.join('')}
</ol>
</div>
`
: ''
}
</section>
`
: ''
}
</section>
</main>
</div>
`;
}
async function generateSinglePdfFromHtml(htmlContent, courseTitle) {
const args = [
'--disable-web-security',
'--no-sandbox',
'--disable-setuid-sandbox',
];
const browser = await puppeteer.launch({
headless: 'new',
args,
});
const page = await browser.newPage();
await page.setContent(htmlContent);
const pdfOptions = {
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate:
"<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
footerTemplate: `<div class="footer" style="text-align: right; margin-right: 5%; font-size: 8px; width: 297mm; padding-top: 30px;">
${courseTitle} | Powered by <a href="https://app.classroomio.com" style="color: blue; text-decoration: underline;">ClassroomIO</a>
</div>`,
margin: {
top: '0.4cm',
bottom: '1cm',
},
};
const numPages = await page.evaluate(() => {
const content = document.body; // Change this to the specific element containing content
const style = window.getComputedStyle(content);
const contentHeight = parseInt(style.height, 10);
const pageHeight = 900; // Set this to an appropriate value based on your PDF format
return Math.ceil(contentHeight / pageHeight);
});
const pagesPdfBuffer = [];
for (let currentPage = 1; currentPage <= numPages; currentPage++) {
await page.evaluate(() => {});
const pdfBuffer = await page.pdf(pdfOptions);
pagesPdfBuffer.push(pdfBuffer);
}
await browser.close();
return Buffer.concat(pagesPdfBuffer);
}
async function generateLessonPdf(params) {
// Generate the lesson boddy
const body = getLessonBody(params);
const html = getHtmlTemplate(body);
console.log(html);
return await generateSinglePdfFromHtml(html, params.courseTitle);
// return await getPdfBuffer(html, {
// path: 'test.pdf',
// width: '1000px',
// height: undefined,
// printBackground: true,
// format: 'A4',
// displayHeaderFooter: true,
// headerTemplate:
// "<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
// footerTemplate:
// '<div style="text-align: right;width: 297mm;font-size: 8px;"><span style="margin-right: 1cm"><span class="pageNumber"></span> of <span class="totalPages"></span></span></div>',
// margin: {
// top: '1cm',
// bottom: '1cm',
// },
// });
}
module.exports = {
generateLessonPdf,
};
+38
View File
@@ -0,0 +1,38 @@
const puppeteer = require('puppeteer');
const getPdfBuffer = async (html, pdfParams) => {
const args = [
'--disable-web-security',
'--no-sandbox',
'--disable-setuid-sandbox',
];
const defaultPdfParams = {
width: '1123px',
height: '794px',
};
// launch a new chrome instance
const browser = await puppeteer.launch({
headless: 'new',
args,
});
// create a new page
const page = await browser.newPage();
await page.setViewport({ width: 1123, height: 794 });
await page.goto('data:text/html,' + html, { waitUntil: 'networkidle2' });
await page.addStyleTag({ content: '@page { size: auto; }' });
await page.emulateMediaType('screen');
// create a pdf buffer
const pdfBuffer = await page.pdf(pdfParams || defaultPdfParams);
// close the browser
await browser.close();
return pdfBuffer;
};
module.exports = {
getPdfBuffer,
};
+30
View File
@@ -0,0 +1,30 @@
const { createClient } = require('@supabase/supabase-js');
const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = process.env;
let supabase;
const getSupabase = (config) => {
if (supabase) return supabase;
config = {
supabaseConfig: {
bucketPath:
'https://koxqonvbkeakwvmdegcf.supabase.in/storage/v1/object/sign/',
url: PUBLIC_SUPABASE_URL || '',
anonKey: PUBLIC_SUPABASE_ANON_KEY || '',
},
};
supabase = createClient(
config.supabaseConfig.url,
config.supabaseConfig.anonKey,
{ auth: { persistSession: false } }
);
return supabase;
};
module.exports = {
getSupabase,
};
+17
View File
@@ -0,0 +1,17 @@
const multer = require('multer');
// const storage = multer.memoryStorage();
// const upload = multer({ storage: storage });
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, `${new Date().getTime()}-${file.originalname}`); //Appending extension
},
});
var upload = multer({ storage: storage });
module.exports = upload;