@medicus.ai/medicus-report-pdf-generator 1.3.8 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +60 -30
- package/lib/wellbeing_report_generator.js +28 -14
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -7,10 +7,18 @@ const {
|
|
|
7
7
|
} = require("./lib/wellbeing_report_generator");
|
|
8
8
|
const { generateSanusXReport, generateHTMLSanusXReport } = require("./lib/sanusx_report_generator");
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const crypto = require('crypto');
|
|
10
12
|
const { sendNascoEmail, sendEmailNotification } = require('./lib/sendEmail')
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const { generatePDFCorporateReport, generateHTMLCorporateReport } = require('./lib/corporate_report_generator')
|
|
13
15
|
|
|
16
|
+
// Best-effort fire-and-forget cleanup for per-call temp files. Never throws.
|
|
17
|
+
const safeUnlink = (filePath) => {
|
|
18
|
+
if (!filePath) return Promise.resolve();
|
|
19
|
+
return fs.promises.unlink(filePath).catch(() => { });
|
|
20
|
+
};
|
|
21
|
+
|
|
14
22
|
module.exports = {
|
|
15
23
|
|
|
16
24
|
generateHTMLStaging,
|
|
@@ -187,10 +195,19 @@ module.exports = {
|
|
|
187
195
|
let reportData = JSON.parse(data);
|
|
188
196
|
let base64Object = reportData.data;
|
|
189
197
|
|
|
198
|
+
// Per-call unique identifiers so concurrent callers never share a path.
|
|
199
|
+
// Even when two requests overlap, each one owns its own input/output
|
|
200
|
+
// files and email attachment, eliminating cross-user PDF swapping.
|
|
201
|
+
const callId = crypto.randomUUID();
|
|
202
|
+
const tmpDir = os.tmpdir();
|
|
203
|
+
const tmpInputPath = path.join(tmpDir, `wb-${callId}-in.pdf`);
|
|
204
|
+
const tmpEncryptedPath = path.join(tmpDir, `wb-${callId}-enc.pdf`);
|
|
205
|
+
let tmpEmailAttachmentPath = null;
|
|
206
|
+
|
|
190
207
|
let LOGS = '';
|
|
191
|
-
const LOGS_FILE_PATH =
|
|
192
|
-
|
|
193
|
-
|
|
208
|
+
const LOGS_FILE_PATH = isDebugging
|
|
209
|
+
? path.join(__dirname, 'output', `LOGS-${callId}.txt`)
|
|
210
|
+
: null;
|
|
194
211
|
|
|
195
212
|
const mailConfig = {
|
|
196
213
|
host: reportData.host,
|
|
@@ -201,12 +218,6 @@ module.exports = {
|
|
|
201
218
|
secure: reportData.secure,
|
|
202
219
|
};
|
|
203
220
|
|
|
204
|
-
if (isDebugging) {
|
|
205
|
-
if (fs.existsSync(LOGS_FILE_PATH)) fs.unlinkSync(LOGS_FILE_PATH);
|
|
206
|
-
if (fs.existsSync(PDF_FILE_PATH)) fs.unlinkSync(PDF_FILE_PATH);
|
|
207
|
-
if (fs.existsSync(ENCRYPTED_PDF_PATH)) fs.unlinkSync(ENCRYPTED_PDF_PATH);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
221
|
if (isDebugging) {
|
|
211
222
|
console.log('2: save json string');
|
|
212
223
|
LOGS += '2: save json string\n===============\n' + base64Object + '\n\n\n';
|
|
@@ -242,45 +253,64 @@ module.exports = {
|
|
|
242
253
|
LOGS += '3: save PDF buffer\n===============\n' + '[binary content omitted]' + '\n\n\n';
|
|
243
254
|
}
|
|
244
255
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
try {
|
|
257
|
+
// Write the unencrypted PDF to a per-call temp file.
|
|
258
|
+
// qpdf needs a real input path; using a uuid-suffixed file in
|
|
259
|
+
// os.tmpdir() guarantees no concurrent caller can overwrite it.
|
|
260
|
+
await fs.promises.writeFile(tmpInputPath, fileBuffer);
|
|
248
261
|
|
|
249
|
-
return await (async () => {
|
|
250
262
|
const qpdf = await import('node-qpdf2');
|
|
251
263
|
|
|
264
|
+
// Encrypt to a per-call output file. We can't use qpdf's stdout
|
|
265
|
+
// mode (output omitted) because node-qpdf2's spawn helper joins
|
|
266
|
+
// stdout chunks as strings, which corrupts binary PDF bytes.
|
|
252
267
|
await qpdf.encrypt({
|
|
253
|
-
input:
|
|
254
|
-
output:
|
|
268
|
+
input: tmpInputPath,
|
|
269
|
+
output: tmpEncryptedPath,
|
|
255
270
|
keyLength: 256,
|
|
256
271
|
password: reportData.pdfPassword,
|
|
257
|
-
})
|
|
258
|
-
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const encryptedBuffer = await fs.promises.readFile(tmpEncryptedPath);
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
const base64data = Buffer.from(encryptedBuffer).toString('base64');
|
|
276
|
+
const base64data = encryptedBuffer.toString('base64');
|
|
262
277
|
if (isDebugging) {
|
|
263
278
|
console.log('4: save encoded base64');
|
|
264
279
|
LOGS += '4: save encoded base64\n===============\n' + base64data + '\n\n\n';
|
|
265
280
|
}
|
|
266
281
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
282
|
+
if (isDebugging && LOGS_FILE_PATH) {
|
|
283
|
+
await fs.promises.writeFile(LOGS_FILE_PATH, LOGS, 'utf8');
|
|
284
|
+
console.log('5: save logs to file');
|
|
285
|
+
}
|
|
270
286
|
|
|
271
287
|
if (isDownloadable) {
|
|
272
288
|
return encryptedBuffer;
|
|
273
|
-
} else {
|
|
274
|
-
if (shouldSendEmail) {
|
|
275
|
-
return await sendNascoEmail(decodedJSON.wellbeing, ENCRYPTED_PDF_PATH, mailConfig, reportData.client);
|
|
276
|
-
} else {
|
|
277
|
-
return base64data;
|
|
278
|
-
}
|
|
279
289
|
}
|
|
280
290
|
|
|
281
|
-
|
|
282
|
-
|
|
291
|
+
if (shouldSendEmail) {
|
|
292
|
+
// Write the encrypted buffer to a dedicated per-call file just
|
|
293
|
+
// for the email attachment. sendNascoEmail expects a path
|
|
294
|
+
// (nodemailer reads it from disk), so we materialize it once,
|
|
295
|
+
// pass it in, then unlink in the finally block.
|
|
296
|
+
tmpEmailAttachmentPath = path.join(tmpDir, `wb-${callId}-mail.pdf`);
|
|
297
|
+
await fs.promises.writeFile(tmpEmailAttachmentPath, encryptedBuffer);
|
|
298
|
+
return await sendNascoEmail(
|
|
299
|
+
decodedJSON.wellbeing,
|
|
300
|
+
tmpEmailAttachmentPath,
|
|
301
|
+
mailConfig,
|
|
302
|
+
reportData.client
|
|
303
|
+
);
|
|
304
|
+
}
|
|
283
305
|
|
|
306
|
+
return base64data;
|
|
307
|
+
} finally {
|
|
308
|
+
await Promise.all([
|
|
309
|
+
safeUnlink(tmpInputPath),
|
|
310
|
+
safeUnlink(tmpEncryptedPath),
|
|
311
|
+
safeUnlink(tmpEmailAttachmentPath),
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
284
314
|
},
|
|
285
315
|
|
|
286
316
|
generateSanuxPDF: async (data, isDebugging, isDownloadable, shouldSendEmail) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const crypto = require("crypto");
|
|
3
4
|
const { promisify } = require('util');
|
|
4
5
|
const { JSDOM } = require("jsdom");
|
|
5
6
|
const path = require("path");
|
|
@@ -1001,8 +1002,12 @@ let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language
|
|
|
1001
1002
|
// Setup
|
|
1002
1003
|
debug = isDebugging;
|
|
1003
1004
|
const client = clientName || 'default';
|
|
1004
|
-
|
|
1005
|
-
|
|
1005
|
+
// Per-call unique paths so concurrent callers never share a file on disk.
|
|
1006
|
+
// The puppeteer `goto(file:///…)` step is what previously read another
|
|
1007
|
+
// user's HTML when two requests overlapped, producing a swapped PDF body.
|
|
1008
|
+
const callId = crypto.randomUUID();
|
|
1009
|
+
const callOutFile = path.resolve(__dirname + `/../output/${client}-pdf-${callId}.html`);
|
|
1010
|
+
const callPdfFile = `/output/${client}-pdf-${callId}.html`;
|
|
1006
1011
|
|
|
1007
1012
|
// Language configuration
|
|
1008
1013
|
language = empty(language) ? 'en' : language;
|
|
@@ -1113,11 +1118,12 @@ let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language
|
|
|
1113
1118
|
}
|
|
1114
1119
|
|
|
1115
1120
|
// Write file
|
|
1116
|
-
await promisify(fs.writeFile)(
|
|
1121
|
+
await promisify(fs.writeFile)(callOutFile, finalHtml);
|
|
1117
1122
|
|
|
1118
1123
|
return {
|
|
1119
1124
|
html: finalHtml,
|
|
1120
|
-
outputFile:
|
|
1125
|
+
outputFile: callOutFile,
|
|
1126
|
+
pdfFile: callPdfFile,
|
|
1121
1127
|
header: renderedHeader,
|
|
1122
1128
|
footer: footerHtml,
|
|
1123
1129
|
historyData
|
|
@@ -1138,8 +1144,12 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
|
|
|
1138
1144
|
// Setup
|
|
1139
1145
|
debug = isDebugging;
|
|
1140
1146
|
const client = clientName || 'default';
|
|
1141
|
-
|
|
1142
|
-
|
|
1147
|
+
// Per-call unique paths so concurrent callers never share a file on disk.
|
|
1148
|
+
// The puppeteer `goto(file:///…)` step is what previously read another
|
|
1149
|
+
// user's HTML when two requests overlapped, producing a swapped PDF body.
|
|
1150
|
+
const callId = crypto.randomUUID();
|
|
1151
|
+
const callOutFile = path.resolve(__dirname + `/../output/${client}-pdf-${callId}.html`);
|
|
1152
|
+
const callPdfFile = `/output/${client}-pdf-${callId}.html`;
|
|
1143
1153
|
|
|
1144
1154
|
// Extract data
|
|
1145
1155
|
const wellbeingData = data.wellbeing || {};
|
|
@@ -1485,11 +1495,12 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
|
|
|
1485
1495
|
}
|
|
1486
1496
|
|
|
1487
1497
|
// Write file
|
|
1488
|
-
await promisify(fs.writeFile)(
|
|
1498
|
+
await promisify(fs.writeFile)(callOutFile, finalHtml);
|
|
1489
1499
|
|
|
1490
1500
|
return {
|
|
1491
1501
|
html: finalHtml,
|
|
1492
|
-
outputFile:
|
|
1502
|
+
outputFile: callOutFile,
|
|
1503
|
+
pdfFile: callPdfFile,
|
|
1493
1504
|
header: renderedHeader,
|
|
1494
1505
|
footer: templates.footer,
|
|
1495
1506
|
historyData,
|
|
@@ -1545,15 +1556,16 @@ const setupPageContent = async (page, contentHtml, filePath) => {
|
|
|
1545
1556
|
let generatePDFWellbeingReport = async (data) => {
|
|
1546
1557
|
try {
|
|
1547
1558
|
const contentHtml = data.html;
|
|
1548
|
-
OUT_FILE = data.outputFile;
|
|
1549
1559
|
|
|
1550
1560
|
// Launch browser and setup page
|
|
1551
1561
|
const browser = await launchBrowser();
|
|
1552
1562
|
const page = await browser.newPage();
|
|
1553
1563
|
debugLog("page launched!");
|
|
1554
1564
|
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1565
|
+
// Use the per-call relative path produced by generateHTMLWellbeingReport.
|
|
1566
|
+
// Reading module-level state here was the root cause of cross-user PDF
|
|
1567
|
+
// body swapping under concurrent requests.
|
|
1568
|
+
await setupPageContent(page, contentHtml, data.pdfFile);
|
|
1557
1569
|
debugLog("page opened!");
|
|
1558
1570
|
|
|
1559
1571
|
// Generate PDF
|
|
@@ -1789,15 +1801,17 @@ const setupExtendedPageContent = async (page, contentHtml, filePath) => {
|
|
|
1789
1801
|
const generatePDFReport = async (data, hideWellbeingUI = false) => {
|
|
1790
1802
|
try {
|
|
1791
1803
|
const contentHtml = data.html;
|
|
1792
|
-
const OUT_FILE = data.outputFile;
|
|
1793
1804
|
const footerTemplate = getPdfFooterTemplate(data.client, data.selectedLabs, data.footer);
|
|
1794
1805
|
|
|
1795
1806
|
// Launch browser and setup page
|
|
1796
1807
|
const browser = await launchExtendedBrowser();
|
|
1797
1808
|
const page = await browser.newPage();
|
|
1798
1809
|
|
|
1799
|
-
//
|
|
1800
|
-
|
|
1810
|
+
// Use the per-call relative path produced by
|
|
1811
|
+
// generateHTMLWellbeingReportWithSmartReport. Reading module-level
|
|
1812
|
+
// state here was the root cause of cross-user PDF body swapping
|
|
1813
|
+
// under concurrent requests.
|
|
1814
|
+
await setupExtendedPageContent(page, contentHtml, data.pdfFile);
|
|
1801
1815
|
|
|
1802
1816
|
// Generate PDF with header and footer (PHA needs larger bottom margin to prevent content overlapping footer)
|
|
1803
1817
|
const bottomMargin = (data.client || '').toLowerCase() === 'pha' ? '60px' : '50px';
|