mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-26 17:42:33 +02:00
+33
-492
@@ -2,15 +2,7 @@ import dotenv from 'dotenv';
|
||||
import { format, toZonedTime } from 'date-fns-tz';
|
||||
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
|
||||
import crypto from 'node:crypto';
|
||||
import {
|
||||
PDFDocument,
|
||||
PDFName,
|
||||
rgb,
|
||||
degrees,
|
||||
// StandardFonts,
|
||||
PDFArray,
|
||||
PDFDict,
|
||||
} from 'pdf-lib';
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import { parseUploadFile } from './utils/fileUtils.js';
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
@@ -157,484 +149,41 @@ export function generateId(length) {
|
||||
}
|
||||
|
||||
/**
|
||||
* FlattenPdf renders field values as static content and removes the interactive
|
||||
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
|
||||
* comments, stamps) are preserved.
|
||||
* FlattenPdf is used to remove existing widgets if present any and flatten pdf.
|
||||
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
|
||||
* @returns {Promise<Uint8Array>} flatPdf - pdf file in Uint8Array
|
||||
* @returns {Promise<Uint8Array>} flatPdf - pdf file in unit8arry
|
||||
*/
|
||||
export const flattenPdf = async pdfFile => {
|
||||
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
const acroFormEntry = pdfDoc.catalog.get(PDFName.of('AcroForm'));
|
||||
const acroForm = pdfDoc.context.lookupMaybe
|
||||
? pdfDoc.context.lookupMaybe(acroFormEntry)
|
||||
: pdfDoc.context.lookup(acroFormEntry);
|
||||
|
||||
if (acroForm && typeof acroForm.set === 'function') {
|
||||
// Avoid pdf-lib form APIs here; some malformed PDFs crash while
|
||||
// iterating/removing fields. Clearing /Fields directly is safer.
|
||||
acroForm.set(PDFName.of('Fields'), pdfDoc.context.obj([]));
|
||||
acroForm.delete(PDFName.of('XFA'));
|
||||
acroForm.delete(PDFName.of('SigFlags'));
|
||||
}
|
||||
} catch {
|
||||
// If AcroForm is malformed, continue with page annotation cleanup.
|
||||
}
|
||||
|
||||
for (const page of pdfDoc.getPages()) {
|
||||
try {
|
||||
const annotationsRef = page.node.get(PDFName.of('Annots'));
|
||||
if (!annotationsRef) continue;
|
||||
|
||||
const annotations = pdfDoc.context.lookup(annotationsRef);
|
||||
if (!annotations || !annotations.asArray) continue;
|
||||
|
||||
const filtered = annotations.asArray().filter(annotRef => {
|
||||
try {
|
||||
const annot = pdfDoc.context.lookup(annotRef);
|
||||
const subtype = annot?.get(PDFName.of('Subtype'));
|
||||
return subtype?.toString() !== '/Widget';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
} else {
|
||||
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
|
||||
}
|
||||
} catch {
|
||||
// best effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
|
||||
} catch {
|
||||
// best effort cleanup
|
||||
}
|
||||
|
||||
return await pdfDoc.save({ useObjectStreams: false });
|
||||
};
|
||||
|
||||
/* ---- flattenPdf private helpers ---- */
|
||||
|
||||
function _safeGetWidgetsServer(field) {
|
||||
try {
|
||||
return field.acroField?.getWidgets?.() || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function _getWidgetRectServer(widget, pdfDoc) {
|
||||
try {
|
||||
const r = widget.getRectangle?.();
|
||||
if (r && isFinite(r.x) && isFinite(r.y) && isFinite(r.width) && isFinite(r.height)) {
|
||||
return r;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to manual extraction */
|
||||
}
|
||||
|
||||
try {
|
||||
const rectArr = widget.dict?.lookup?.(PDFName.of('Rect'));
|
||||
if (!rectArr || typeof rectArr.size !== 'function' || rectArr.size() !== 4) return null;
|
||||
|
||||
const x1 = _numberFromPdfObjectServer(rectArr.get(0));
|
||||
const y1 = _numberFromPdfObjectServer(rectArr.get(1));
|
||||
const x2 = _numberFromPdfObjectServer(rectArr.get(2));
|
||||
const y2 = _numberFromPdfObjectServer(rectArr.get(3));
|
||||
|
||||
if ([x1, y1, x2, y2].some(v => !isFinite(v))) return null;
|
||||
|
||||
return {
|
||||
x: Math.min(x1, x2),
|
||||
y: Math.min(y1, y2),
|
||||
width: Math.abs(x2 - x1),
|
||||
height: Math.abs(y2 - y1),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _numberFromPdfObjectServer(obj) {
|
||||
if (!obj) return NaN;
|
||||
if (typeof obj.asNumber === 'function') return obj.asNumber();
|
||||
if (typeof obj.numberValue === 'function') return obj.numberValue();
|
||||
return Number(obj?.value ?? obj);
|
||||
}
|
||||
|
||||
function _getWidgetPageServer(pdfDoc, pages, widget) {
|
||||
try {
|
||||
const pRef = widget.P?.();
|
||||
if (pRef) {
|
||||
for (const page of pages) {
|
||||
if (page.ref === pRef) return page;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
|
||||
try {
|
||||
const pRef = widget.dict?.get?.(PDFName.of('P'));
|
||||
if (pRef) {
|
||||
for (const page of pages) {
|
||||
if (page.ref === pRef) return page;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
|
||||
// Fallback: search page annots
|
||||
try {
|
||||
for (const page of pages) {
|
||||
const annots = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
|
||||
if (!annots) continue;
|
||||
|
||||
for (let i = 0; i < annots.size(); i++) {
|
||||
const ref = annots.get(i);
|
||||
if (ref === widget.ref) return page;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _drawWidgetBoxServer(page, rect) {
|
||||
try {
|
||||
page.drawRectangle({
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
borderWidth: 0.6,
|
||||
borderColor: rgb(0.65, 0.65, 0.65),
|
||||
});
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
function _drawTextFieldServer(page, field, rect, font) {
|
||||
let text = '';
|
||||
try {
|
||||
text = field.getText?.() ?? '';
|
||||
} catch {
|
||||
text = '';
|
||||
}
|
||||
text = String(text ?? '');
|
||||
|
||||
if (!text) return;
|
||||
|
||||
let multiline = false;
|
||||
try {
|
||||
multiline = field.isMultiline?.() ?? false;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
let comb = false;
|
||||
try {
|
||||
comb = field.isCombed?.() ?? false;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (comb) {
|
||||
_drawCombTextServer(page, text, rect, font);
|
||||
return;
|
||||
}
|
||||
|
||||
if (multiline || text.includes('\n')) {
|
||||
_drawMultilineTextServer(page, text, rect, font);
|
||||
return;
|
||||
}
|
||||
|
||||
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
|
||||
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
|
||||
|
||||
page.drawText(text, {
|
||||
x: rect.x + 2,
|
||||
y: baselineY,
|
||||
size: fontSize,
|
||||
font,
|
||||
color: rgb(0, 0, 0),
|
||||
maxWidth: Math.max(1, rect.width - 4),
|
||||
});
|
||||
}
|
||||
|
||||
function _drawMultilineTextServer(page, text, rect, font) {
|
||||
const lines = String(text).replace(/\r/g, '').split('\n');
|
||||
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
|
||||
const lineHeight = fontSize + 1.5;
|
||||
|
||||
let y = rect.y + rect.height - fontSize - 2;
|
||||
|
||||
for (const line of lines) {
|
||||
if (y < rect.y + 1) break;
|
||||
|
||||
page.drawText(line, {
|
||||
x: rect.x + 2,
|
||||
y,
|
||||
size: fontSize,
|
||||
font,
|
||||
color: rgb(0, 0, 0),
|
||||
maxWidth: Math.max(1, rect.width - 4),
|
||||
lineHeight,
|
||||
});
|
||||
|
||||
y -= lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function _drawCombTextServer(page, text, rect, font) {
|
||||
const chars = String(text).split('');
|
||||
const count = Math.max(chars.length, 1);
|
||||
const cellWidth = rect.width / count;
|
||||
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
|
||||
|
||||
chars.forEach((ch, i) => {
|
||||
const textWidth = font.widthOfTextAtSize(ch, fontSize);
|
||||
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
|
||||
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
|
||||
|
||||
page.drawText(ch, {
|
||||
x,
|
||||
y,
|
||||
size: fontSize,
|
||||
font,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
|
||||
if (i < count - 1) {
|
||||
const pdfDoc = await PDFDocument.load(pdfFile);
|
||||
// Get the form
|
||||
const form = pdfDoc.getForm();
|
||||
// fetch form fields
|
||||
const fields = form.getFields();
|
||||
// remove form all existing fields and their widgets
|
||||
if (fields && fields?.length > 0) {
|
||||
try {
|
||||
page.drawLine({
|
||||
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
|
||||
end: { x: rect.x + (i + 1) * cellWidth, y: rect.y + rect.height },
|
||||
thickness: 0.4,
|
||||
color: rgb(0.75, 0.75, 0.75),
|
||||
});
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _fitSingleLineFontSizeServer(text, rect, font) {
|
||||
let size = Math.min(12, rect.height - 4);
|
||||
size = Math.max(size, 6);
|
||||
|
||||
while (size > 6) {
|
||||
const width = font.widthOfTextAtSize(text, size);
|
||||
if (width <= rect.width - 4) return size;
|
||||
size -= 0.5;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
|
||||
function _drawCheckBoxServer(page, field, rect, zapf) {
|
||||
let checked = false;
|
||||
try {
|
||||
checked = field.isChecked();
|
||||
} catch {
|
||||
checked = false;
|
||||
}
|
||||
|
||||
if (!checked) return;
|
||||
|
||||
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
|
||||
|
||||
page.drawText('\u2714', {
|
||||
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
|
||||
y: rect.y + Math.max(1, (rect.height - size) / 2),
|
||||
size,
|
||||
font: zapf,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
function _drawRadioGroupServer(page, field, widget, rect) {
|
||||
let selected = null;
|
||||
try {
|
||||
selected = field.getSelected();
|
||||
} catch {
|
||||
selected = null;
|
||||
}
|
||||
|
||||
if (!selected) return;
|
||||
|
||||
let widgetOnValue = null;
|
||||
try {
|
||||
widgetOnValue = widget.getOnValue?.();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (!widgetOnValue) {
|
||||
try {
|
||||
const ap = widget.dict?.lookupMaybe?.(PDFName.of('AP'), PDFDict);
|
||||
const n = ap?.lookupMaybe?.(PDFName.of('N'), PDFDict);
|
||||
if (n) {
|
||||
const keys = n.keys();
|
||||
for (const k of keys) {
|
||||
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
|
||||
if (name !== '/Off' && name !== 'Off') {
|
||||
widgetOnValue = name.replace(/^\//, '');
|
||||
break;
|
||||
for (const field of fields) {
|
||||
while (field.acroField.getWidgets().length) {
|
||||
field.acroField.removeWidget(0);
|
||||
}
|
||||
form.removeField(field);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err while removing field from pdf', err);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
// Updates the field appearances to ensure visual changes are reflected.
|
||||
form.updateFieldAppearances();
|
||||
// Flattens the form, converting all form fields into non-editable, static content
|
||||
form.flatten();
|
||||
const flatPdf = await pdfDoc.save({ useObjectStreams: false });
|
||||
return flatPdf;
|
||||
} catch (err) {
|
||||
console.log('err ', err);
|
||||
throw new Error('error in pdf');
|
||||
}
|
||||
|
||||
const selectedStr = String(selected).replace(/^\//, '');
|
||||
const onStr = String(widgetOnValue ?? '').replace(/^\//, '');
|
||||
|
||||
if (!onStr || selectedStr !== onStr) return;
|
||||
|
||||
// Circle outline
|
||||
try {
|
||||
page.drawEllipse({
|
||||
x: rect.x + rect.width / 2,
|
||||
y: rect.y + rect.height / 2,
|
||||
xScale: rect.width / 2 - 1,
|
||||
yScale: rect.height / 2 - 1,
|
||||
borderWidth: 0.8,
|
||||
borderColor: rgb(0, 0, 0),
|
||||
});
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
|
||||
// Inner filled dot
|
||||
try {
|
||||
const r = Math.min(rect.width, rect.height) / 4;
|
||||
page.drawEllipse({
|
||||
x: rect.x + rect.width / 2,
|
||||
y: rect.y + rect.height / 2,
|
||||
xScale: r,
|
||||
yScale: r,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
function _drawDropdownServer(page, field, rect, font) {
|
||||
let text = '';
|
||||
try {
|
||||
const selected = field.getSelected?.();
|
||||
if (Array.isArray(selected)) {
|
||||
text = selected.join(', ');
|
||||
} else {
|
||||
text = selected ?? '';
|
||||
}
|
||||
} catch {
|
||||
text = '';
|
||||
}
|
||||
|
||||
text = String(text ?? '');
|
||||
if (!text) return;
|
||||
|
||||
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
|
||||
|
||||
page.drawText(text, {
|
||||
x: rect.x + 2,
|
||||
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
|
||||
size: fontSize,
|
||||
font,
|
||||
color: rgb(0, 0, 0),
|
||||
maxWidth: Math.max(1, rect.width - 12),
|
||||
});
|
||||
}
|
||||
|
||||
function _drawOptionListServer(page, field, rect, font) {
|
||||
let selected = [];
|
||||
try {
|
||||
selected = field.getSelected?.() || [];
|
||||
} catch {
|
||||
selected = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(selected)) {
|
||||
selected = [selected].filter(Boolean);
|
||||
}
|
||||
|
||||
if (!selected.length) return;
|
||||
|
||||
const lines = selected.map(v => String(v));
|
||||
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
|
||||
const lineHeight = fontSize + 1.5;
|
||||
|
||||
let y = rect.y + rect.height - fontSize - 2;
|
||||
|
||||
for (const line of lines) {
|
||||
if (y < rect.y + 1) break;
|
||||
|
||||
page.drawText(line, {
|
||||
x: rect.x + 2,
|
||||
y,
|
||||
size: fontSize,
|
||||
font,
|
||||
color: rgb(0, 0, 0),
|
||||
maxWidth: Math.max(1, rect.width - 4),
|
||||
});
|
||||
|
||||
y -= lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
|
||||
function _removeWidgetAnnotationsServer(pdfDoc) {
|
||||
for (const page of pdfDoc.getPages()) {
|
||||
try {
|
||||
const annotationsRef = page.node.get(PDFName.of('Annots'));
|
||||
if (!annotationsRef) continue;
|
||||
|
||||
const annotations = pdfDoc.context.lookup(annotationsRef);
|
||||
if (!annotations || !annotations.asArray) continue;
|
||||
|
||||
const filtered = annotations.asArray().filter(annotRef => {
|
||||
try {
|
||||
const annot = pdfDoc.context.lookup(annotRef);
|
||||
const subtype = annot?.get(PDFName.of('Subtype'));
|
||||
return subtype?.toString() !== '/Widget';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
} else {
|
||||
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
|
||||
}
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Format date and time for the selected timezone
|
||||
export const formatTimeInTimezone = (date, timezone) => {
|
||||
@@ -647,7 +196,7 @@ export const formatTimeInTimezone = (date, timezone) => {
|
||||
|
||||
// `getSecureUrl` is used to return local secure url if local files
|
||||
export const getSecureUrl = url => {
|
||||
const fileUrl = new URL(url)?.pathname?.includes('/files/');
|
||||
const fileUrl = new URL(url)?.pathname?.includes('files');
|
||||
if (fileUrl) {
|
||||
try {
|
||||
const file = getSignedLocalUrl(url);
|
||||
@@ -739,21 +288,13 @@ export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
|
||||
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
|
||||
: formatTimeInTimezone(date, timeZone);
|
||||
}
|
||||
|
||||
export const randomId = (digit = 8) => {
|
||||
// 1. Grab a cryptographically-secure 32-bit random value
|
||||
// Use crypto for stronger randomness
|
||||
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
|
||||
const raw = randomBytes[0]; // 0 … 4,294,967,295
|
||||
|
||||
// Calculate the min and max for the given digit length
|
||||
const min = Math.pow(10, digit - 1); // e.g., digit=3 → 100
|
||||
const max = Math.pow(10, digit) - 1; // e.g., digit=3 → 999
|
||||
const range = max - min + 1;
|
||||
|
||||
// Collapse random value into the range and shift
|
||||
return min + (raw % range);
|
||||
export const randomId = () => {
|
||||
const randomBytes = crypto.getRandomValues(new Uint16Array(1));
|
||||
const randomValue = randomBytes[0];
|
||||
const randomDigit = 1000 + (randomValue % 9000);
|
||||
return randomDigit;
|
||||
};
|
||||
|
||||
export const handleValidImage = async Placeholder => {
|
||||
const updatedPlaceholders = [];
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async function deleteLocalFile(fileUrl) {
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
const filePath = decodeURIComponent(url.pathname);
|
||||
if (!filePath.includes('/files/')) return;
|
||||
if (!filePath.includes('files')) return;
|
||||
|
||||
const localPath = url?.pathname?.split(`/files/${serverAppId}/`)?.pop();
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { appName } from '../../../Utils.js';
|
||||
import sendSystemMail from '../../parsefunction/sendSystemMail.js';
|
||||
import axios from 'axios';
|
||||
import { appName, cloudServerUrl, serverAppId } from '../../../Utils.js';
|
||||
|
||||
const serverUrl = cloudServerUrl;
|
||||
const appId = serverAppId;
|
||||
const masterKey = process.env.MASTER_KEY;
|
||||
|
||||
// Constants (adjust to your preference)
|
||||
export const OTP_LENGTH = 6;
|
||||
@@ -56,7 +60,12 @@ export async function sendDeleteOtpEmail(extUser, otp) {
|
||||
</html>
|
||||
`,
|
||||
};
|
||||
return sendSystemMail({ params });
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Master-Key': masterKey,
|
||||
};
|
||||
return axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
}
|
||||
|
||||
export function msUntil(nowMs, futureMs) {
|
||||
|
||||
@@ -126,7 +126,6 @@ export default async function docxtopdf(req, res) {
|
||||
try {
|
||||
// ---- Auth: current user ----
|
||||
const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
|
||||
const uploadedSizeBytes = req.file.size ?? req.file.buffer.length;
|
||||
|
||||
// ---- contracts_Users ----
|
||||
const whereUser = JSON.stringify({
|
||||
|
||||
@@ -63,7 +63,6 @@ import getSignature from './parsefunction/getSignature.js';
|
||||
import updateEmailTemplates from './parsefunction/updateEmailTemplates.js';
|
||||
import triggerEvent from './parsefunction/triggerEvent.js';
|
||||
import setWidgetPreferences from './parsefunction/setWidgetPreferences.js';
|
||||
import createDocumentFromApp from './parsefunction/createDocumentFromApp.js';
|
||||
|
||||
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
|
||||
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
|
||||
@@ -137,4 +136,3 @@ Parse.Cloud.define('getdefaultsignature', getSignature);
|
||||
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
|
||||
Parse.Cloud.define('triggerevent', triggerEvent);
|
||||
Parse.Cloud.define('setwidgetpreferences', setWidgetPreferences);
|
||||
Parse.Cloud.define('createdocumentfromapp', createDocumentFromApp);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
async function DocumentAftersave(request) {
|
||||
try {
|
||||
if (!request.original) {
|
||||
console.log('new entry is insert in contracts_Document ', request?.object?.id);
|
||||
console.log('new entry is insert in contracts_Document');
|
||||
const obj = request.object;
|
||||
const objId = obj?.id;
|
||||
const createdAt = obj?.get?.('createdAt');
|
||||
|
||||
@@ -26,8 +26,6 @@ export default async function GetTemplate(request) {
|
||||
template.include('CreatedBy');
|
||||
template.include('ExtUserPtr.TenantId');
|
||||
template.include('Bcc');
|
||||
template.include('Cc');
|
||||
|
||||
const extUserQuery = new Parse.Query('contracts_Users');
|
||||
extUserQuery.equalTo('Email', userEmail);
|
||||
extUserQuery.include('TeamIds');
|
||||
@@ -64,7 +62,6 @@ export default async function GetTemplate(request) {
|
||||
template.include('ExtUserPtr.TenantId');
|
||||
template.include('Placeholders.signerPtr');
|
||||
template.include('Bcc');
|
||||
template.include('Cc');
|
||||
}
|
||||
}
|
||||
const res = await template.first({ useMasterKey: true });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default async function TemplateAfterSave(request) {
|
||||
try {
|
||||
if (!request.original) {
|
||||
console.log('new entry is insert in contracts_Template', request?.object?.id);
|
||||
console.log('new entry is insert in contracts_Template');
|
||||
const obj = request.object;
|
||||
const objId = obj?.id;
|
||||
const ip = request?.headers?.['x-real-ip'] || '';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { cloudServerUrl, mailTemplate, replaceMailVaribles, serverAppId } from '
|
||||
import { setDocumentCount } from '../../utils/CountUtils.js';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import sendSystemMail from './sendSystemMail.js';
|
||||
|
||||
function chunkArray(arr, size) {
|
||||
const out = [];
|
||||
@@ -51,6 +50,9 @@ async function sendOwnerSummaryEmail({
|
||||
failedList,
|
||||
}) {
|
||||
try {
|
||||
const url = `${serverUrl}/functions/sendmailv3`;
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId };
|
||||
|
||||
const subject = `Bulk send finished: ${failed} of ${total} failed to create`;
|
||||
|
||||
const failureHtml = failedList?.length
|
||||
@@ -81,7 +83,7 @@ async function sendOwnerSummaryEmail({
|
||||
html,
|
||||
};
|
||||
|
||||
await sendSystemMail({ params });
|
||||
await axios.post(url, params, { headers });
|
||||
} catch (e) {
|
||||
console.log('batchdoc Failed to send owner summary email:', e?.message || e);
|
||||
}
|
||||
@@ -90,7 +92,7 @@ async function sendOwnerSummaryEmail({
|
||||
async function deductcount(docsCount, extUserId) {
|
||||
try {
|
||||
if (extUserId) {
|
||||
setDocumentCount(extUserId, docsCount);
|
||||
setDocumentCount(extUserId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('batchdoc deductcount error: ', err);
|
||||
@@ -228,7 +230,6 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
|
||||
Description: x.Description,
|
||||
CreatedBy: x.CreatedBy,
|
||||
SendinOrder: x.SendinOrder || true,
|
||||
SendInOrderStrict: x.SendInOrderStrict || false,
|
||||
ExtUserPtr: {
|
||||
__type: 'Pointer',
|
||||
className: x.ExtUserPtr.className,
|
||||
@@ -271,7 +272,6 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
|
||||
...(x?.SignatureType ? { SignatureType: x?.SignatureType } : {}),
|
||||
...(x?.NotifyOnSignatures ? { NotifyOnSignatures: x?.NotifyOnSignatures } : {}),
|
||||
...(x?.Bcc?.length > 0 ? { Bcc: x?.Bcc } : {}),
|
||||
...(x?.Cc?.length > 0 ? { Cc: x?.Cc } : {}),
|
||||
...(x?.RedirectUrl ? { RedirectUrl: x?.RedirectUrl } : {}),
|
||||
...(mailBody ? { RequestBody: mailBody } : {}),
|
||||
...(mailSubject ? { RequestSubject: mailSubject } : {}),
|
||||
@@ -303,6 +303,7 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
|
||||
createdAt: response.data[0]?.success?.createdAt,
|
||||
};
|
||||
deductcount(response.data.length, resExt.id);
|
||||
console.log('here');
|
||||
sendMail(updateDocuments, publicUrl); //sessionToken
|
||||
return { total: 1, created: 1, failed: 0 };
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { setDocumentCount } from '../../utils/CountUtils.js';
|
||||
|
||||
export default async function createDocumentFromApp(request) {
|
||||
const doc = request.params?.document;
|
||||
|
||||
if (!doc) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'Missing document payload.');
|
||||
}
|
||||
|
||||
if (!request.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
}
|
||||
|
||||
const SentToOthers = doc.SentToOthers !== undefined ? doc.SentToOthers : false;
|
||||
const SendinOrder = doc.SendinOrder !== undefined ? doc.SendinOrder : false;
|
||||
const SendInOrderStrict = doc.SendInOrderStrict !== undefined ? !!doc.SendInOrderStrict : false;
|
||||
const IsEnableOTP = doc?.IsEnableOTP !== undefined ? doc?.IsEnableOTP : false;
|
||||
const IsTourEnabled = doc?.IsTourEnabled !== undefined ? doc?.IsTourEnabled : false;
|
||||
const AllowModifications =
|
||||
doc?.AllowModifications !== undefined ? doc?.AllowModifications : false;
|
||||
const AutomaticReminders =
|
||||
doc?.AutomaticReminders !== undefined ? doc?.AutomaticReminders : false;
|
||||
const NotifyOnSignatures = doc.NotifyOnSignatures !== undefined ? doc.NotifyOnSignatures : false;
|
||||
|
||||
try {
|
||||
const docCls = new Parse.Object('contracts_Document');
|
||||
|
||||
docCls.set('Name', doc?.Name || 'untitled document');
|
||||
docCls.set('URL', doc?.URL);
|
||||
docCls.set('ExtUserPtr', doc.ExtUserPtr);
|
||||
docCls.set('CreatedBy', doc.CreatedBy);
|
||||
|
||||
if (doc.Description) {
|
||||
docCls.set('Description', doc.Description);
|
||||
}
|
||||
if (doc.Note) {
|
||||
docCls.set('Note', doc.Note);
|
||||
}
|
||||
if (doc.SignedUrl) {
|
||||
docCls.set('SignedUrl', doc.SignedUrl);
|
||||
}
|
||||
|
||||
docCls.set('SentToOthers', SentToOthers);
|
||||
docCls.set('SendinOrder', SendinOrder);
|
||||
docCls.set('SendInOrderStrict', SendInOrderStrict);
|
||||
docCls.set('IsEnableOTP', IsEnableOTP);
|
||||
docCls.set('IsTourEnabled', IsTourEnabled);
|
||||
docCls.set('AllowModifications', AllowModifications);
|
||||
docCls.set('AutomaticReminders', AutomaticReminders);
|
||||
docCls.set('NotifyOnSignatures', NotifyOnSignatures);
|
||||
|
||||
if (doc.TimeToCompleteDays !== undefined) {
|
||||
docCls.set('TimeToCompleteDays', Number(doc.TimeToCompleteDays));
|
||||
}
|
||||
if (doc.RemindOnceInEvery !== undefined) {
|
||||
docCls.set('RemindOnceInEvery', Number(doc.RemindOnceInEvery));
|
||||
}
|
||||
|
||||
if (doc?.DocSentAt?.iso) {
|
||||
docCls.set('DocSentAt', new Date(doc?.DocSentAt?.iso));
|
||||
}
|
||||
|
||||
if (doc.RedirectUrl) {
|
||||
docCls.set('RedirectUrl', doc.RedirectUrl);
|
||||
}
|
||||
if (doc.TemplateId) {
|
||||
docCls.set('TemplateId', doc.TemplateId);
|
||||
}
|
||||
|
||||
if (Array.isArray(doc.Signers) && doc?.Signers?.length > 0) {
|
||||
docCls.set('Signers', doc.Signers);
|
||||
}
|
||||
if (Array.isArray(doc.Placeholders) && doc?.Placeholders?.length > 0) {
|
||||
docCls.set('Placeholders', doc.Placeholders);
|
||||
}
|
||||
if (Array.isArray(doc.SignatureType) && doc.SignatureType.length > 0) {
|
||||
docCls.set('SignatureType', doc.SignatureType);
|
||||
}
|
||||
if (Array.isArray(doc.Bcc) && doc?.Bcc?.length > 0) {
|
||||
docCls.set('Bcc', doc.Bcc);
|
||||
}
|
||||
if (Array.isArray(doc.Cc) && doc?.Cc?.length > 0) {
|
||||
docCls.set('Cc', doc.Cc);
|
||||
}
|
||||
if (Array.isArray(doc?.PenColors) && doc?.PenColors?.length > 0) {
|
||||
docCls.set('PenColors', doc.PenColors);
|
||||
}
|
||||
|
||||
const docRes = await docCls.save(null, { useMasterKey: true });
|
||||
|
||||
// update documentCount in Users and tenant account
|
||||
setDocumentCount(doc?.ExtUserPtr?.id);
|
||||
|
||||
return docRes;
|
||||
} catch (error) {
|
||||
console.log('error in create document from app: ', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ export default async function createDuplicate(request) {
|
||||
newTemplate.set('SignedUrl', _templateRes.SignedUrl);
|
||||
newTemplate.set('SentToOthers', _templateRes?.SentToOthers || false);
|
||||
newTemplate.set('SendinOrder', _templateRes?.SendinOrder || false);
|
||||
newTemplate.set('SendInOrderStrict', _templateRes?.SendInOrderStrict || false);
|
||||
newTemplate.set('AutomaticReminders', _templateRes?.AutomaticReminders || false);
|
||||
newTemplate.set('RemindOnceInEvery', _templateRes?.RemindOnceInEvery || 5);
|
||||
newTemplate.set('IsEnableOTP', _templateRes?.IsEnableOTP || false);
|
||||
@@ -78,9 +77,6 @@ export default async function createDuplicate(request) {
|
||||
if (_templateRes?.Bcc?.length) {
|
||||
newTemplate.set('Bcc', _templateRes?.Bcc);
|
||||
}
|
||||
if (_templateRes?.Cc?.length) {
|
||||
newTemplate.set('Cc', _templateRes?.Cc);
|
||||
}
|
||||
const OriginIp = _templateRes?.OriginIp || request?.headers?.['x-real-ip'] || '';
|
||||
|
||||
if (OriginIp) {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { appName } from '../../Utils.js';
|
||||
import sendSystemMail from './sendSystemMail.js';
|
||||
import axios from 'axios';
|
||||
import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
const serverUrl = cloudServerUrl;
|
||||
const APPID = serverAppId;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
};
|
||||
|
||||
async function sendDeclineMail(doc, publicUrl, userId, reason) {
|
||||
try {
|
||||
@@ -38,7 +46,7 @@ async function sendDeclineMail(doc, publicUrl, userId, reason) {
|
||||
pdfName: pdfName,
|
||||
html: body,
|
||||
};
|
||||
await sendSystemMail({ params });
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
} catch (err) {
|
||||
console.log('err in sendnotifymail', err);
|
||||
}
|
||||
@@ -55,21 +63,16 @@ export default async function declinedocument(request) {
|
||||
try {
|
||||
const docCls = new Parse.Query('contracts_Document');
|
||||
docCls.include('ExtUserPtr.TenantId,Placeholders.signerPtr,Signers');
|
||||
docCls.notEqualTo('IsCompleted', true);
|
||||
docCls.notEqualTo('IsArchive', true);
|
||||
const updateDoc = await docCls.get(docId, { useMasterKey: true });
|
||||
if (updateDoc) {
|
||||
const _doc = JSON.parse(JSON.stringify(updateDoc));
|
||||
const isEnableOTP = updateDoc?.get('IsEnableOTP') || false;
|
||||
const isCreator = _doc?.CreatedBy?.objectId === userId;
|
||||
if (!isEnableOTP) {
|
||||
updateDoc.set('IsDeclined', true);
|
||||
updateDoc.set('DeclineReason', reason);
|
||||
updateDoc.set('DeclineBy', declineBy);
|
||||
await updateDoc.save(null, { useMasterKey: true });
|
||||
if (!isCreator) {
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
}
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
return 'document declined';
|
||||
} else {
|
||||
if (!request?.user) {
|
||||
@@ -79,10 +82,7 @@ export default async function declinedocument(request) {
|
||||
updateDoc.set('DeclineReason', reason);
|
||||
updateDoc.set('DeclineBy', declineBy);
|
||||
await updateDoc.save(null, { useMasterKey: true });
|
||||
const isCreator = _doc?.CreatedBy?.objectId === request?.user?.id;
|
||||
if (!isCreator) {
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
}
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
return 'document declined';
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,11 @@ async function uploadFile(pdfName, filepath) {
|
||||
const filedata = fs.readFileSync(filepath);
|
||||
let fileUrl;
|
||||
|
||||
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
// await file.save({ useMasterKey: true });
|
||||
// const fileRes = getSecureUrl(file.url());
|
||||
// fileUrl = fileRes.url;
|
||||
|
||||
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
|
||||
fileUrl = getSecureUrl(fileRes?.url)?.url;
|
||||
return { imageUrl: fileUrl };
|
||||
|
||||
@@ -43,7 +43,7 @@ function makeS3Client() {
|
||||
}
|
||||
|
||||
export default async function getPresignedUrl(url) {
|
||||
if (url?.includes('/files/')) {
|
||||
if (url?.includes('files')) {
|
||||
return presignedlocalUrl(url);
|
||||
} else {
|
||||
const client = makeS3Client();
|
||||
@@ -70,7 +70,7 @@ export async function getSignedUrl(request) {
|
||||
|
||||
if (docId || templateId) {
|
||||
try {
|
||||
if (url?.includes('/files/')) {
|
||||
if (url?.includes('files')) {
|
||||
return presignedlocalUrl(url);
|
||||
} else if (useLocal !== 'true') {
|
||||
const query = new Parse.Query(docId ? 'contracts_Document' : 'contracts_Template');
|
||||
@@ -106,7 +106,7 @@ export async function getSignedUrl(request) {
|
||||
if (!isAuth) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
|
||||
} else {
|
||||
if (url?.includes('/files/')) {
|
||||
if (url?.includes('files')) {
|
||||
return presignedlocalUrl(url);
|
||||
} else if (useLocal !== 'true') {
|
||||
const presignedUrl = await getPresignedUrl(url);
|
||||
@@ -147,7 +147,7 @@ export function getSignedLocalUrl(fileUrl, expirationTimeInSeconds) {
|
||||
}
|
||||
|
||||
export function presignedlocalUrl(signedUrl, expirationTimeInSeconds) {
|
||||
if (signedUrl?.includes('/files/')) {
|
||||
if (signedUrl?.includes('files')) {
|
||||
const fileUrl = signedUrl.split('?')?.[0];
|
||||
const secretKey = process.env.MASTER_KEY;
|
||||
const exp = expirationTimeInSeconds || 200;
|
||||
|
||||
@@ -3,13 +3,6 @@ import fs from 'node:fs';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { formatDateTime } from '../../../Utils.js';
|
||||
|
||||
const formatDateStr = (dateStr, DateFormat, timezone, Is12Hr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
return formatDateTime(date, DateFormat, timezone, Is12Hr);
|
||||
};
|
||||
|
||||
export default async function GenerateCertificate(docDetails) {
|
||||
const timezone = docDetails?.ExtUserPtr?.Timezone || '';
|
||||
const Is12Hr = docDetails?.ExtUserPtr?.Is12HourTime || false;
|
||||
@@ -19,9 +12,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const fontBytes = fs.readFileSync('./font/times.ttf'); //
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
const timesRomanFont = await pdfDoc.embedFont(fontBytes, { subset: true });
|
||||
const pngUrl = fs.readFileSync('./images/logo.png').buffer;
|
||||
const naSignUrl = fs.readFileSync('./images/na_sign.png').buffer;
|
||||
const nasign = await pdfDoc.embedPng(naSignUrl);
|
||||
const pngUrl = fs.readFileSync('./logo.png').buffer;
|
||||
const pngImage = await pdfDoc.embedPng(pngUrl);
|
||||
const page = pdfDoc.addPage();
|
||||
const { width, height } = page.getSize();
|
||||
@@ -38,7 +29,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const textKeyColor = rgb(0.12, 0.12, 0.12);
|
||||
const textValueColor = rgb(0.3, 0.3, 0.3);
|
||||
const completedAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
const completedAtperTimezone = formatDateStr(completedAt, DateFormat, timezone, Is12Hr);
|
||||
const completedAtperTimezone = formatDateTime(completedAt, DateFormat, timezone, Is12Hr);
|
||||
const completedUTCtime = completedAtperTimezone;
|
||||
const signersCount = docDetails?.Signers?.length || 1;
|
||||
const generateAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
@@ -52,19 +43,9 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const company = docDetails?.ExtUserPtr?.Company || '';
|
||||
const documentHash = docDetails?.DocumentHash || '';
|
||||
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
|
||||
const createdAtperTimezone = formatDateStr(createdAt, DateFormat, timezone, Is12Hr);
|
||||
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
|
||||
const IsEnableOTP = docDetails?.IsEnableOTP || false;
|
||||
const placeholders = Array.isArray(docDetails?.Placeholders) ? docDetails.Placeholders : [];
|
||||
const filteredaudit = docDetails?.AuditTrail?.filter(x => {
|
||||
if (!x?.UserPtr?.objectId) return false;
|
||||
return true;
|
||||
});
|
||||
const toTs = v => {
|
||||
if (!v) return 0;
|
||||
if (typeof v === 'object' && v?.iso) return new Date(v.iso).getTime() || 0;
|
||||
const t = new Date(v).getTime();
|
||||
return Number.isFinite(t) ? t : 0;
|
||||
};
|
||||
const filteredaudit = docDetails?.AuditTrail?.filter(x => x?.UserPtr?.objectId);
|
||||
const auditTrail =
|
||||
docDetails?.Signers?.length > 0
|
||||
? filteredaudit?.map(x => {
|
||||
@@ -75,7 +56,6 @@ export default async function GenerateCertificate(docDetails) {
|
||||
SignedOn: x?.SignedOn || generatedUTCTime,
|
||||
ViewedOn: x?.ViewedOn || x?.SignedOn || generatedUTCTime,
|
||||
Signature: x?.Signature || '',
|
||||
_signedOnTs: toTs(x?.SignedOn),
|
||||
};
|
||||
})
|
||||
: [
|
||||
@@ -85,12 +65,8 @@ export default async function GenerateCertificate(docDetails) {
|
||||
SignedOn: filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
ViewedOn: filteredaudit[0]?.ViewedOn || filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
Signature: filteredaudit[0]?.Signature || '',
|
||||
_signedOnTs: toTs(filteredaudit[0]?.SignedOn),
|
||||
},
|
||||
];
|
||||
if (Array.isArray(auditTrail)) {
|
||||
auditTrail.sort((a, b) => (a?._signedOnTs || 0) - (b?._signedOnTs || 0));
|
||||
}
|
||||
|
||||
const ownerName = docDetails?.SenderName || docDetails.ExtUserPtr?.Name || 'n/a';
|
||||
const ownerEmail = docDetails?.SenderMail || docDetails.ExtUserPtr?.Email || 'n/a';
|
||||
@@ -327,63 +303,24 @@ export default async function GenerateCertificate(docDetails) {
|
||||
let yPosition7 = yPosition6 - 20;
|
||||
let yPosition8 = yPosition7 - 35;
|
||||
|
||||
// A signer/approver block spans from yPosition1 down to yPosition8 (the
|
||||
// separator line at the bottom of the block). The signature image's
|
||||
// bottom edge sits at yPosition7 - 30 and must remain inside the page
|
||||
// border (whose bottom edge is at startY). Use the lowest of those two
|
||||
// values when deciding whether the next block fits on the current page.
|
||||
const minY = startY + 5;
|
||||
const blockBottom = () => Math.min(yPosition7 - 30, yPosition8);
|
||||
|
||||
// Helper that resets the y-positions to the top of a freshly added page so
|
||||
// the next block starts cleanly under the border.
|
||||
const startNewPage = () => {
|
||||
const newPage = pdfDoc.addPage();
|
||||
newPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: width - 2 * startX,
|
||||
height: height - 2 * startY,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1,
|
||||
});
|
||||
yPosition1 = newPage.getHeight() - 40;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition7 - 35;
|
||||
return newPage;
|
||||
};
|
||||
|
||||
let currentPage = page;
|
||||
for (let i = 0; i < auditTrail.length; i++) {
|
||||
const x = auditTrail[i];
|
||||
// If the next block would overflow the bottom border, move to a new page.
|
||||
if (blockBottom() < minY) {
|
||||
currentPage = startNewPage();
|
||||
}
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : nasign;
|
||||
const headerLabel = `${i + 1}. ${'Signer'}`;
|
||||
const signedOnLabel = 'Signed on :';
|
||||
|
||||
currentPage.drawText(headerLabel, {
|
||||
auditTrail.slice(0, 3).forEach(async (x, i) => {
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
|
||||
page.drawText(`Signer ${i + 1}`, {
|
||||
x: 30,
|
||||
y: yPosition1,
|
||||
size: subtitle,
|
||||
font: timesRomanFont,
|
||||
color: titleColor,
|
||||
});
|
||||
currentPage.drawText('Name :', {
|
||||
page.drawText('Name :', {
|
||||
x: 30,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.Name || '', {
|
||||
|
||||
page.drawText(x?.Name, {
|
||||
x: 75,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
@@ -392,14 +329,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
if (IsEnableOTP) {
|
||||
currentPage.drawText('Security level :', {
|
||||
page.drawText('Security level :', {
|
||||
x: half + 120,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText('Email, OTP Auth', {
|
||||
page.drawText('Email, OTP Auth', {
|
||||
x: half + 190,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
@@ -408,14 +345,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawText('Email :', {
|
||||
page.drawText('Email :', {
|
||||
x: 30,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.Email || '', {
|
||||
|
||||
page.drawText(x?.Email, {
|
||||
x: 75,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
@@ -423,14 +361,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Viewed on :', {
|
||||
page.drawText('Viewed on :', {
|
||||
x: 30,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(`${formatDateStr(x?.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
|
||||
page.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 97,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
@@ -438,30 +377,31 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(signedOnLabel, {
|
||||
page.drawText('Signed on :', {
|
||||
x: 30,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
const signedOnValueX = 30 + timesRomanFont.widthOfTextAtSize(signedOnLabel, signertext) + 5;
|
||||
currentPage.drawText(`${formatDateStr(x?.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: signedOnValueX,
|
||||
|
||||
page.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 95,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('IP address :', {
|
||||
page.drawText('IP address :', {
|
||||
x: 30,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.ipAddress || '', {
|
||||
|
||||
page.drawText(x?.ipAddress, {
|
||||
x: 95,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
@@ -469,14 +409,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Signature :', {
|
||||
page.drawText('Signature :', {
|
||||
x: 30,
|
||||
y: yPosition7,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawRectangle({
|
||||
|
||||
page.drawRectangle({
|
||||
x: 98,
|
||||
y: yPosition7 - 30,
|
||||
width: 104,
|
||||
@@ -485,14 +426,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
borderWidth: 1,
|
||||
});
|
||||
if (embedPng) {
|
||||
currentPage.drawImage(embedPng, {
|
||||
page.drawImage(embedPng, {
|
||||
x: 100,
|
||||
y: yPosition7 - 27,
|
||||
width: 100,
|
||||
height: 40,
|
||||
});
|
||||
}
|
||||
currentPage.drawLine({
|
||||
page.drawLine({
|
||||
start: { x: 30, y: yPosition8 },
|
||||
end: { x: width - 30, y: yPosition8 },
|
||||
color: rgb(0.12, 0.12, 0.12),
|
||||
@@ -507,6 +448,185 @@ export default async function GenerateCertificate(docDetails) {
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition8 - 174;
|
||||
});
|
||||
|
||||
if (auditTrail.length > 3) {
|
||||
let currentPageIndex = 1;
|
||||
let currentPage = page;
|
||||
auditTrail.slice(3).forEach(async (x, i) => {
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
|
||||
|
||||
// Calculate remaining space on current page
|
||||
const remainingSpace = yPosition8;
|
||||
|
||||
// If there's not enough space for the next entry, create a new page
|
||||
if (remainingSpace < 90) {
|
||||
// Adjust the value as needed
|
||||
currentPageIndex++;
|
||||
currentPage = pdfDoc.addPage();
|
||||
currentPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: width - 2 * startX,
|
||||
height: height - 2 * startY,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1,
|
||||
});
|
||||
yPosition1 = currentPage.getHeight() - 40;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = currentPage.getHeight() - 190;
|
||||
}
|
||||
|
||||
currentPage.drawText(`Signer ${4 + i}`, {
|
||||
x: 30,
|
||||
y: yPosition1,
|
||||
size: subtitle,
|
||||
font: timesRomanFont,
|
||||
color: titleColor,
|
||||
});
|
||||
currentPage.drawText('Name :', {
|
||||
x: 30,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.Name, {
|
||||
x: 75,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
if (IsEnableOTP) {
|
||||
currentPage.drawText('Security level :', {
|
||||
x: half + 120,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(`Email, OTP Auth`, {
|
||||
x: half + 190,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawText('Email :', {
|
||||
x: 30,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.Email, {
|
||||
x: 75,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Viewed on :', {
|
||||
x: 30,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 97,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
currentPage.drawText('Signed on :', {
|
||||
x: 30,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 95,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('IP address :', {
|
||||
x: 30,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.ipAddress, {
|
||||
x: 100,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Signature :', {
|
||||
x: 30,
|
||||
y: yPosition7,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawRectangle({
|
||||
x: 98,
|
||||
y: yPosition7 - 27,
|
||||
width: 104,
|
||||
height: 44,
|
||||
borderColor: rgb(0.22, 0.18, 0.47),
|
||||
borderWidth: 1,
|
||||
});
|
||||
if (embedPng) {
|
||||
currentPage.drawImage(embedPng, {
|
||||
x: 100,
|
||||
y: yPosition7 - 25,
|
||||
width: 100,
|
||||
height: 40,
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawLine({
|
||||
start: { x: 30, y: yPosition8 },
|
||||
end: { x: width - 30, y: yPosition8 },
|
||||
color: rgb(0.12, 0.12, 0.12),
|
||||
thickness: 0.5,
|
||||
});
|
||||
|
||||
// Update y positions for the next entry
|
||||
yPosition1 = yPosition8 - 20;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition8 - 174;
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
|
||||
@@ -17,22 +17,6 @@ import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
|
||||
import sendMailWithAttachment from '../sendMailWithAttachment.js';
|
||||
import sendSystemMail from '../sendSystemMail.js';
|
||||
import {
|
||||
COMPLETION_ACTIVITIES,
|
||||
findPlaceholderIndex,
|
||||
findPendingPriorSigner,
|
||||
isCompletionRelevant,
|
||||
} from '../../../utils/workflowUtils.js';
|
||||
|
||||
// Audit-trail activities that count toward document completion. The free
|
||||
// build only counts 'Signed'; EE additionally counts 'Approved'.
|
||||
|
||||
// A placeholder participates in completion unless it is a prefill entry.
|
||||
// EE additionally excludes viewers (who never act on the document).
|
||||
|
||||
// Strict-order gating: returns the signerObjId of the prior placeholder
|
||||
// still pending, or null when the strict-order requirement is satisfied.
|
||||
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
@@ -78,24 +62,13 @@ async function uploadFile(pdfName, filepath) {
|
||||
}
|
||||
|
||||
// `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document
|
||||
async function updateDoc(
|
||||
docId,
|
||||
url,
|
||||
userId,
|
||||
ipAddress,
|
||||
data,
|
||||
className,
|
||||
sign,
|
||||
documentHash,
|
||||
activity
|
||||
) {
|
||||
async function updateDoc(docId, url, userId, ipAddress, data, className, sign, documentHash) {
|
||||
try {
|
||||
const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
|
||||
const auditActivity = 'Signed';
|
||||
const obj = {
|
||||
UserPtr: UserPtr,
|
||||
SignedUrl: url,
|
||||
Activity: auditActivity,
|
||||
Activity: 'Signed',
|
||||
ipAddress: ipAddress,
|
||||
SignedOn: new Date(),
|
||||
Signature: sign,
|
||||
@@ -115,14 +88,13 @@ async function updateDoc(
|
||||
updateAuditTrail = [obj];
|
||||
}
|
||||
|
||||
// Count both Signed and Approved entries; only signer/approver
|
||||
// placeholders count toward completion (viewers and prefill excluded).
|
||||
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (data.Signers && data.Signers.length > 0) {
|
||||
const completionRelevant =
|
||||
data.Placeholders?.length > 0 ? data.Placeholders.filter(isCompletionRelevant) : [];
|
||||
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
|
||||
//'removePrefill' is used to remove prefill role from placeholders filed then compare length to change status of document
|
||||
const removePrefill =
|
||||
data.Placeholders.length > 0 && data.Placeholders.filter(x => x.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -152,11 +124,10 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
const logo =
|
||||
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
|
||||
|
||||
const auditTrailCount =
|
||||
doc?.AuditTrail?.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity))?.length || 0;
|
||||
const completionRelevant =
|
||||
doc?.Placeholders?.length > 0 ? doc.Placeholders.filter(isCompletionRelevant) : [];
|
||||
const signersCount = completionRelevant?.length;
|
||||
const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0;
|
||||
const removePrefill =
|
||||
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
const signersCount = removePrefill?.length;
|
||||
const remainingSign = signersCount - auditTrailCount;
|
||||
if (remainingSign > 1 && doc?.NotifyOnSignatures) {
|
||||
const sender = doc.ExtUserPtr;
|
||||
@@ -183,7 +154,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
html: body,
|
||||
mailProvider: mailProvider,
|
||||
};
|
||||
await sendSystemMail({ params });
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in sendnotifymail', err);
|
||||
@@ -268,7 +239,6 @@ async function sendCompletedMail(obj) {
|
||||
body = replaceVar.body;
|
||||
}
|
||||
const Bcc = doc?.Bcc?.length > 0 ? doc.Bcc.map(x => x.Email) : [];
|
||||
const Cc = doc?.Cc?.length > 0 ? doc.Cc.map(x => x.Email) : [];
|
||||
const updatedBcc = doc?.SenderMail ? [...Bcc, doc?.SenderMail] : Bcc;
|
||||
const formatId = doc?.ExtUserPtr?.DownloadFilenameFormat;
|
||||
const filename = pdfName?.length > 100 ? pdfName?.slice(0, 100) : pdfName;
|
||||
@@ -288,7 +258,6 @@ async function sendCompletedMail(obj) {
|
||||
html: body,
|
||||
mailProvider: obj.mailProvider,
|
||||
bcc: updatedBcc?.length > 0 ? updatedBcc : '',
|
||||
cc: Cc?.length > 0 ? Cc : '',
|
||||
certificatePath: `./exports/signed_certificate_${doc.objectId}.pdf`,
|
||||
filename: docName,
|
||||
};
|
||||
@@ -391,14 +360,11 @@ async function PDF(req) {
|
||||
const isCustomMail = req.params.isCustomCompletionMail || false;
|
||||
const mailProvider = req.params.mailProvider || '';
|
||||
const sign = req.params.signature || '';
|
||||
const auditActivity = 'Signed';
|
||||
const publicUrl = req.headers.public_url;
|
||||
// below bode is used to get info of docId
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,Cc,CreatedBy');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
docQuery.notEqualTo('IsDeclined', true);
|
||||
docQuery.notEqualTo('IsArchive', true);
|
||||
const resDoc = await docQuery.first({ useMasterKey: true });
|
||||
if (!resDoc) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.');
|
||||
@@ -425,27 +391,7 @@ async function PDF(req) {
|
||||
className = 'contracts_Users';
|
||||
signUser = _resDoc.ExtUserPtr;
|
||||
}
|
||||
// Strict-order gating: when both `SendinOrder` and `SendInOrderStrict`
|
||||
// are enabled the document creator wants the signing flow locked to a
|
||||
// strict sequence — a signer/approver may only act once every previous
|
||||
// signer/approver placeholder has a Signed/Approved audit entry. We
|
||||
// skip this check entirely for the document owner (className=Users)
|
||||
// because owners never sign through this path.
|
||||
if (reqUserId && _resDoc?.SendinOrder === true && _resDoc?.SendInOrderStrict === true) {
|
||||
const placeholders = Array.isArray(_resDoc?.Placeholders)
|
||||
? _resDoc.Placeholders.filter(p => p?.Role !== 'prefill')
|
||||
: [];
|
||||
const myIdx = findPlaceholderIndex(placeholders, reqUserId);
|
||||
if (myIdx > 0) {
|
||||
const pendingId = findPendingPriorSigner(placeholders, myIdx, _resDoc?.AuditTrail);
|
||||
if (pendingId) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Strict signing order is enabled — please wait for the previous signers to complete their action before signing.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const username = signUser.Name;
|
||||
const userEmail = signUser.Email;
|
||||
if (req.params.pdfFile) {
|
||||
@@ -462,7 +408,7 @@ async function PDF(req) {
|
||||
const P12Buffer = Buffer.from(pfxFile, 'base64');
|
||||
fs.writeFileSync(pfxname, P12Buffer);
|
||||
const UserPtr = { __type: 'Pointer', className: className, objectId: signUser.objectId };
|
||||
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: auditActivity, ipAddress: userIP };
|
||||
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: 'Signed', ipAddress: userIP };
|
||||
let updateAuditTrail;
|
||||
if (_resDoc.AuditTrail && _resDoc.AuditTrail.length > 0) {
|
||||
updateAuditTrail = [..._resDoc.AuditTrail, obj];
|
||||
@@ -470,17 +416,14 @@ async function PDF(req) {
|
||||
updateAuditTrail = [obj];
|
||||
}
|
||||
|
||||
// Both Signed and Approved entries count toward completion. Only
|
||||
// signer/approver placeholders are counted; viewers and prefill are
|
||||
// excluded.
|
||||
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (_resDoc.Signers && _resDoc.Signers.length > 0) {
|
||||
const completionRelevant =
|
||||
_resDoc?.Placeholders?.length > 0
|
||||
? _resDoc.Placeholders.filter(isCompletionRelevant)
|
||||
: [];
|
||||
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
|
||||
const removePrefill =
|
||||
_resDoc?.Placeholders?.length > 0 &&
|
||||
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
// if (auditTrail.length === _resDoc.Signers.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -533,8 +476,7 @@ async function PDF(req) {
|
||||
_resDoc, // auditTrail, signers, etc data
|
||||
className, // className based on flow
|
||||
sign, // sign base64
|
||||
isCompleted ? documentHash : undefined,
|
||||
auditActivity
|
||||
isCompleted ? documentHash : undefined
|
||||
);
|
||||
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
|
||||
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { setDocumentCount } from '../../utils/CountUtils.js';
|
||||
|
||||
export default async function recreateDocument(request) {
|
||||
const { docId } = request.params;
|
||||
if (!docId) {
|
||||
@@ -12,16 +10,6 @@ export default async function recreateDocument(request) {
|
||||
try {
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
docQuery.notEqualTo('IsArchive', true);
|
||||
docQuery.exclude([
|
||||
'DocSentAt',
|
||||
'SignedUrl',
|
||||
'AuditTrail',
|
||||
'DeclineBy',
|
||||
'DeclineReason',
|
||||
'DocumentHash',
|
||||
'CertificateUrl',
|
||||
]);
|
||||
const doc = await docQuery.first({ useMasterKey: true });
|
||||
if (!doc) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found');
|
||||
@@ -30,27 +18,11 @@ export default async function recreateDocument(request) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Signyourself Document not allowed');
|
||||
}
|
||||
const _docRes = doc?.toJSON();
|
||||
const { ACL, objectId, ...docRes } = _docRes;
|
||||
const { objectId, SignedUrl, AuditTrail, ACL, DeclineBy, DeclineReason, ...docRes } = _docRes;
|
||||
const createDoc = new Parse.Object('contracts_Document');
|
||||
Object.entries(docRes).forEach(([key, value]) => {
|
||||
if (key === 'IsDeclined' || key === 'IsCompleted') {
|
||||
createDoc.set(key, false);
|
||||
} else if (key === 'Placeholders') {
|
||||
const placeHolders = value.map(signer => ({
|
||||
...signer,
|
||||
placeHolder: (signer.placeHolder || []).map(page => ({
|
||||
...page,
|
||||
pos: (page.pos || []).map(widget => {
|
||||
const { SignUrl, ...rest } = widget;
|
||||
return {
|
||||
...rest,
|
||||
type: widget.type === 'text' ? 'text input' : widget.type,
|
||||
options: { ...widget.options, response: '', defaultValue: '' },
|
||||
};
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
createDoc.set(key, placeHolders);
|
||||
} else {
|
||||
createDoc.set(key, value);
|
||||
}
|
||||
@@ -59,7 +31,6 @@ export default async function recreateDocument(request) {
|
||||
const createDocRes = await createDoc.save(null, { useMasterKey: true });
|
||||
// console.log('createDocRes', createDocRes);
|
||||
const newDoc = JSON.parse(JSON.stringify(createDocRes));
|
||||
setDocumentCount(_docRes?.ExtUserPtr?.objectId);
|
||||
return { objectId: newDoc.objectId, createdAt: newDoc.createdAt, updatedAt: newDoc.updatedAt };
|
||||
} catch (err) {
|
||||
console.log('err in recreate document', err);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { randomId } from '../../Utils.js';
|
||||
|
||||
const randomId = () => Math.floor(1000 + Math.random() * 9000);
|
||||
export default async function saveAsTemplate(request) {
|
||||
const docId = request.params.docId;
|
||||
const Ip = request?.headers?.['x-real-ip'] || '';
|
||||
@@ -24,7 +23,6 @@ export default async function saveAsTemplate(request) {
|
||||
templateCls.set('Description', _docRes?.Description);
|
||||
templateCls.set('OriginIp', Ip);
|
||||
templateCls.set('SendinOrder', _docRes?.SendinOrder || false);
|
||||
templateCls.set('SendInOrderStrict', _docRes?.SendInOrderStrict || false);
|
||||
templateCls.set('AutomaticReminders', _docRes?.AutomaticReminders || false);
|
||||
templateCls.set('ExtUserPtr', _docRes?.ExtUserPtr);
|
||||
templateCls.set('CreatedBy', _docRes?.CreatedBy);
|
||||
@@ -93,27 +91,18 @@ export default async function saveAsTemplate(request) {
|
||||
pos: (page.pos || []).map(widget => {
|
||||
// if there is a defaultValue in options, zero it out
|
||||
if (widget.options && widget.options.defaultValue !== undefined) {
|
||||
const { SignUrl, ...rest } = widget;
|
||||
return {
|
||||
...rest,
|
||||
type: widget.type === 'text' ? 'text input' : widget.type,
|
||||
...(widget.type === 'signature' ? { signatureType: '' } : {}),
|
||||
...widget,
|
||||
signatureType: '',
|
||||
options: {
|
||||
...widget.options,
|
||||
defaultValue: '',
|
||||
response: '',
|
||||
...(widget?.options?.isReadOnly ? { isReadOnly: false } : {}),
|
||||
},
|
||||
}; // reset only the value
|
||||
}
|
||||
// otherwise, return the widget unchanged
|
||||
const { SignUrl, ...rest } = widget;
|
||||
return {
|
||||
...rest,
|
||||
type: widget.type === 'text' ? 'text input' : widget.type,
|
||||
...(widget.type === 'signature' ? { signatureType: '' } : {}),
|
||||
options: { ...widget.options, response: '' },
|
||||
};
|
||||
return widget;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
@@ -127,9 +116,6 @@ export default async function saveAsTemplate(request) {
|
||||
if (_docRes?.Bcc?.length > 0) {
|
||||
templateCls.set('Bcc', _docRes?.Bcc);
|
||||
}
|
||||
if (_docRes?.Cc?.length > 0) {
|
||||
templateCls.set('Cc', _docRes?.Cc);
|
||||
}
|
||||
if (_docRes?.PenColors?.length > 0) {
|
||||
templateCls.set('PenColors', _docRes?.PenColors);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ const makeEmail = async (
|
||||
url,
|
||||
pdfName,
|
||||
bcc,
|
||||
cc,
|
||||
filename,
|
||||
certificatePath,
|
||||
replyto,
|
||||
@@ -55,7 +54,6 @@ const makeEmail = async (
|
||||
const htmlContent = html;
|
||||
const boundary = 'boundary_' + Date.now().toString(16);
|
||||
const bccHeader = bcc && bcc.length > 0 ? `BCC: ${bcc.join(',')}\n` : ''; // Construct BCC header if provided
|
||||
const ccHeader = cc && cc.length > 0 ? `CC: ${cc.join(',')}\n` : ''; // Construct CC header if provided
|
||||
const replyToHeader = replyto ? `Reply-To: ${replyto}\n` : ''; // Construct Reply-To header if provided
|
||||
|
||||
let str;
|
||||
@@ -139,7 +137,6 @@ const makeEmail = async (
|
||||
`To: ${to}\n`,
|
||||
`From: ${from}\n`,
|
||||
bccHeader,
|
||||
ccHeader,
|
||||
replyToHeader,
|
||||
`Subject: ${subject}\n\n`,
|
||||
'--' + boundary + '\n',
|
||||
@@ -157,7 +154,6 @@ const makeEmail = async (
|
||||
`To: ${to}\n`,
|
||||
`From: ${from}\n`,
|
||||
bccHeader,
|
||||
ccHeader,
|
||||
replyToHeader,
|
||||
`Subject: ${subject}\n\n`,
|
||||
'--' + boundary + '\n',
|
||||
@@ -172,19 +168,8 @@ const makeEmail = async (
|
||||
return encodedMail;
|
||||
};
|
||||
export default async function sendMailGmailProvider(_extRes, template) {
|
||||
const {
|
||||
sender,
|
||||
receiver,
|
||||
subject,
|
||||
html,
|
||||
url,
|
||||
pdfName,
|
||||
bcc,
|
||||
cc,
|
||||
filename,
|
||||
certificatePath,
|
||||
replyto,
|
||||
} = template;
|
||||
const { sender, receiver, subject, html, url, pdfName, bcc, filename, certificatePath, replyto } =
|
||||
template;
|
||||
|
||||
if (_extRes) {
|
||||
let refresh_token = '';
|
||||
@@ -207,7 +192,6 @@ export default async function sendMailGmailProvider(_extRes, template) {
|
||||
url,
|
||||
pdfName,
|
||||
bcc,
|
||||
cc,
|
||||
filename,
|
||||
certificatePath,
|
||||
replyto,
|
||||
@@ -238,8 +222,7 @@ export default async function sendMailGmailProvider(_extRes, template) {
|
||||
}
|
||||
return { code: 200, message: 'Email sent successfully' };
|
||||
} catch (error) {
|
||||
const message = error?.response?.data || error?.message || 'Unknown error';
|
||||
console.error('Error sending email:', message);
|
||||
console.error('Error sending email:', error);
|
||||
return { code: 500, message: 'Failed to send email ' + error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ async function sendMailProvider(params) {
|
||||
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
|
||||
|
||||
const mailgunApiKey = process.env.MAILGUN_API_KEY;
|
||||
let transporterSMTP;
|
||||
try {
|
||||
let transporterSMTP;
|
||||
let mailgunClient;
|
||||
let mailgunDomain;
|
||||
if (smtpenable) {
|
||||
@@ -143,7 +143,6 @@ async function sendMailProvider(params) {
|
||||
attachments: smtpenable ? attachment : undefined,
|
||||
attachment: smtpenable ? undefined : attachment,
|
||||
bcc: params.bcc ? params.bcc : undefined,
|
||||
cc: params.cc ? params.cc : undefined,
|
||||
replyTo: replyto ? replyto : undefined,
|
||||
};
|
||||
const cleanupPaths = [
|
||||
@@ -194,7 +193,6 @@ async function sendMailProvider(params) {
|
||||
text: params.text || 'mail',
|
||||
html: params?.html ? params.html + reportMsg : '',
|
||||
bcc: params.bcc ? params.bcc : undefined,
|
||||
cc: params.cc ? params.cc : undefined,
|
||||
replyTo: replyto ? replyto : undefined,
|
||||
};
|
||||
|
||||
@@ -227,10 +225,6 @@ async function sendMailProvider(params) {
|
||||
if (err) {
|
||||
return { status: 'error' };
|
||||
}
|
||||
} finally {
|
||||
if (transporterSMTP) {
|
||||
transporterSMTP?.close?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import https from 'https';
|
||||
import formData from 'form-data';
|
||||
import Mailgun from 'mailgun.js';
|
||||
import { appName, smtpenable, smtpsecure, updateMailCount } from '../../Utils.js';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import axios from 'axios';
|
||||
async function sendMailProvider(req) {
|
||||
const app = appName;
|
||||
const extUserId = req.params?.extUserId || '';
|
||||
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
|
||||
|
||||
const mailgunApiKey = process.env.MAILGUN_API_KEY;
|
||||
let transporterSMTP;
|
||||
try {
|
||||
let transporterSMTP;
|
||||
let mailgunClient;
|
||||
let mailgunDomain;
|
||||
if (smtpenable) {
|
||||
@@ -48,7 +51,6 @@ async function sendMailProvider(req) {
|
||||
text: req.params.text || 'mail',
|
||||
html: req.params?.html ? req.params.html + reportMsg : '',
|
||||
bcc: req.params.bcc ? req.params.bcc : undefined,
|
||||
cc: req.params.cc ? req.params.cc : undefined,
|
||||
replyTo: replyto ? replyto : undefined,
|
||||
};
|
||||
|
||||
@@ -80,10 +82,6 @@ async function sendMailProvider(req) {
|
||||
if (err) {
|
||||
return { status: 'error' };
|
||||
}
|
||||
} finally {
|
||||
if (transporterSMTP) {
|
||||
transporterSMTP?.close?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import formData from 'form-data';
|
||||
import Mailgun from 'mailgun.js';
|
||||
import { appName, smtpenable, smtpsecure, updateMailCount } from '../../Utils.js';
|
||||
import { createTransport } from 'nodemailer';
|
||||
async function sendMailProvider(req) {
|
||||
const app = appName;
|
||||
const extUserId = req.params?.extUserId || '';
|
||||
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
|
||||
|
||||
const mailgunApiKey = process.env.MAILGUN_API_KEY;
|
||||
let transporterSMTP;
|
||||
try {
|
||||
let mailgunClient;
|
||||
let mailgunDomain;
|
||||
if (smtpenable) {
|
||||
let transporterConfig = {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: process.env.SMTP_PORT || 465,
|
||||
secure: smtpsecure,
|
||||
};
|
||||
|
||||
// ✅ Add auth only if BOTH username & password exist
|
||||
const smtpUser = process.env.SMTP_USERNAME;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
|
||||
if (smtpUser && smtpPass) {
|
||||
transporterConfig.auth = {
|
||||
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
|
||||
pass: smtpPass,
|
||||
};
|
||||
}
|
||||
transporterSMTP = createTransport(transporterConfig);
|
||||
} else {
|
||||
if (mailgunApiKey) {
|
||||
const mailgun = new Mailgun(formData);
|
||||
mailgunClient = mailgun.client({ username: 'api', key: mailgunApiKey });
|
||||
mailgunDomain = process.env.MAILGUN_DOMAIN;
|
||||
}
|
||||
}
|
||||
|
||||
const from = req.params.from || '';
|
||||
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
|
||||
const replyto = req.params?.replyto || '';
|
||||
const messageParams = {
|
||||
from: from + ' <' + mailsender + '>',
|
||||
to: req.params.recipient,
|
||||
subject: req.params.subject,
|
||||
text: req.params.text || 'mail',
|
||||
html: req.params?.html ? req.params.html + reportMsg : '',
|
||||
bcc: req.params.bcc ? req.params.bcc : undefined,
|
||||
replyTo: replyto ? replyto : undefined,
|
||||
};
|
||||
|
||||
if (transporterSMTP) {
|
||||
const res = await transporterSMTP.sendMail(messageParams);
|
||||
console.log('smtp transporter res: ', res?.response);
|
||||
if (!res.err) {
|
||||
if (extUserId) {
|
||||
await updateMailCount(extUserId);
|
||||
}
|
||||
return { status: 'success' };
|
||||
}
|
||||
} else {
|
||||
if (mailgunApiKey) {
|
||||
const res = await mailgunClient.messages.create(mailgunDomain, messageParams);
|
||||
console.log('mailgun res: ', res?.status);
|
||||
if (res.status === 200) {
|
||||
if (extUserId) {
|
||||
await updateMailCount(extUserId);
|
||||
}
|
||||
return { status: 'success' };
|
||||
}
|
||||
} else {
|
||||
return { status: 'error' };
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`sendSystemMail Error: ${err}`);
|
||||
if (err) {
|
||||
return { status: 'error' };
|
||||
}
|
||||
} finally {
|
||||
if (transporterSMTP) {
|
||||
transporterSMTP?.close?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSystemMail(req) {
|
||||
const nonCustomMail = await sendMailProvider(req);
|
||||
return nonCustomMail;
|
||||
}
|
||||
|
||||
export default sendSystemMail;
|
||||
@@ -5,7 +5,6 @@ const APPID = serverAppId;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
|
||||
async function saveUser(userDetails) {
|
||||
const normalizedEmail = normalizeEmail(userDetails.email.toLowerCase().replace(/\s/g, ''));
|
||||
const userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.equalTo('username', userDetails.email);
|
||||
const userRes = await userQuery.first({ useMasterKey: true });
|
||||
@@ -32,8 +31,6 @@ async function saveUser(userDetails) {
|
||||
user.set('username', userDetails.email);
|
||||
user.set('password', userDetails.password);
|
||||
user.set('email', userDetails?.email?.toLowerCase()?.replace(/\s/g, ''));
|
||||
user.set('normalizedEmail', normalizedEmail);
|
||||
|
||||
if (userDetails?.phone) {
|
||||
user.set('phone', userDetails.phone);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const docSchema = new Parse.Schema('contracts_Document');
|
||||
docSchema.addArray('Cc');
|
||||
await docSchema.update();
|
||||
|
||||
const templateSchema = new Parse.Schema('contracts_Template');
|
||||
templateSchema.addArray('Cc');
|
||||
await templateSchema.update();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const docSchema = new Parse.Schema('contracts_Document');
|
||||
docSchema.deleteField('Cc');
|
||||
await docSchema.update();
|
||||
|
||||
const templateSchema = new Parse.Schema('contracts_Template');
|
||||
templateSchema.deleteField('Cc');
|
||||
await templateSchema.update();
|
||||
};
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const className = 'contracts_templateLinks';
|
||||
const schema = new Parse.Schema(className);
|
||||
|
||||
schema.addString('Type');
|
||||
schema.addPointer('TemplatePtr', 'contracts_Template');
|
||||
schema.addArray('Placeholders');
|
||||
|
||||
return schema.save();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const className = 'contracts_templateLinks';
|
||||
const schema = new Parse.Schema(className);
|
||||
return schema.purge().then(() => schema.delete());
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const schema = new Parse.Schema("_User");
|
||||
schema.addString('normalizedEmail');
|
||||
return schema.update();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const schema = new Parse.Schema("_User");
|
||||
schema.deleteField('normalizedEmail');
|
||||
return schema.update();
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const docSchema = new Parse.Schema('contracts_Document');
|
||||
docSchema.addBoolean('SendInOrderStrict');
|
||||
await docSchema.update();
|
||||
|
||||
const templateSchema = new Parse.Schema('contracts_Template');
|
||||
templateSchema.addBoolean('SendInOrderStrict');
|
||||
await templateSchema.update();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const docSchema = new Parse.Schema('contracts_Document');
|
||||
docSchema.deleteField('SendInOrderStrict');
|
||||
await docSchema.update();
|
||||
|
||||
const templateSchema = new Parse.Schema('contracts_Template');
|
||||
templateSchema.deleteField('SendInOrderStrict');
|
||||
await templateSchema.update();
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -190,7 +190,7 @@ function getUserIP(request) {
|
||||
}
|
||||
|
||||
app.use(async function (req, res, next) {
|
||||
const isFilePath = req.path?.includes('/files/') || false;
|
||||
const isFilePath = req.path.includes('files') || false;
|
||||
if (isFilePath && req.method.toLowerCase() === 'get') {
|
||||
const serverUrl = new URL(process.env.SERVER_URL);
|
||||
const origin = serverUrl.pathname === '/api/app' ? serverUrl.origin + '/api' : serverUrl.origin;
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,46 +0,0 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import { generateId } from '../Utils.js';
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
export default async function createNormalizedEmailUnique() {
|
||||
// Provide the complete MongoDB connection URL with the database name
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/dev'; // Replace with your MongoDB URI
|
||||
const client = new MongoClient(uri);
|
||||
try {
|
||||
await client.connect();
|
||||
const database = client.db();
|
||||
|
||||
const migrationCollection = database.collection('Migrationdb');
|
||||
const migrationName = 'normalizedEmailUnique_1';
|
||||
|
||||
// Check if the migration has already been executed
|
||||
const migrationExists = await migrationCollection.findOne({ name: migrationName });
|
||||
|
||||
if (migrationExists) {
|
||||
console.log(' INFO The unqiue index for normalizedEmail is already present.');
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = database.collection('_User');
|
||||
|
||||
// Create the unique index, but only on documents where NormalizedEmail exists
|
||||
await collection.createIndex({ normalizedEmail: 1 }, { unique: true, sparse: true });
|
||||
|
||||
// Save the migration record in the migrationdb collection
|
||||
await migrationCollection.insertOne({
|
||||
_id: generateId(10),
|
||||
name: migrationName,
|
||||
_created_at: new Date(),
|
||||
_updated_at: new Date(),
|
||||
executedAt: new Date(),
|
||||
details: 'Created unique index on NormalizedEmail',
|
||||
});
|
||||
|
||||
console.log(' SUCCESS The unqiue index for normalizedEmail is already created.');
|
||||
} catch (error) {
|
||||
console.log(' ERROR Running unqiue index for normalizedEmail migration:', error);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import createContactIndex from './createContactIndex.js';
|
||||
import createDocumentIndex from './createDocumentIndex.js';
|
||||
import createNormalizedEmailUnique from './createNormalizedEmailUnqiue.js';
|
||||
|
||||
export default async function runDbMigrations() {
|
||||
await createContactIndex();
|
||||
await createDocumentIndex();
|
||||
await createNormalizedEmailUnique();
|
||||
}
|
||||
|
||||
Generated
+1135
-1365
File diff suppressed because it is too large
Load Diff
@@ -18,36 +18,36 @@
|
||||
"watch": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1038.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1038.0",
|
||||
"@aws-sdk/client-s3": "^3.1008.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1008.0",
|
||||
"@parse/fs-files-adapter": "^3.0.0",
|
||||
"@parse/push-adapter": "^8.5.0",
|
||||
"@parse/s3-files-adapter": "^5.3.1",
|
||||
"@parse/push-adapter": "^8.3.1",
|
||||
"@parse/s3-files-adapter": "5.0.0",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@signpdf/placeholder-pdf-lib": "^3.3.0",
|
||||
"@signpdf/signer-p12": "^3.3.0",
|
||||
"@signpdf/signpdf": "^3.3.0",
|
||||
"axios": "^1.15.2",
|
||||
"axios": "^1.13.6",
|
||||
"coherentpdf": "^2.5.5",
|
||||
"cors": "^2.8.6",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"form-data": "^4.0.5",
|
||||
"generate-api-key": "^1.0.2",
|
||||
"googleapis": "^171.4.0",
|
||||
"libreoffice-convert": "^1.8.1",
|
||||
"mailgun.js": "^13.0.0",
|
||||
"mailgun.js": "^12.7.1",
|
||||
"moment": "^2.30.1",
|
||||
"mongodb": "^7.2.0",
|
||||
"mongodb": "^7.1.0",
|
||||
"multer": "^2.1.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"node-forge": "^1.3.3",
|
||||
"nodemailer": "^8.0.7",
|
||||
"nodemailer": "^8.0.2",
|
||||
"p-limit": "^7.3.0",
|
||||
"parse": "^8.1.0",
|
||||
"parse-dbtool": "^1.2.0",
|
||||
"parse-server": "^8.6.76",
|
||||
"parse-server": "^8.6.40",
|
||||
"parse-server-api-mail-adapter": "^5.0.5",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"posthog-node": "^5.21.2",
|
||||
@@ -55,17 +55,17 @@
|
||||
"rate-limiter-flexible": "^9.1.1",
|
||||
"sharp": "^0.34.5",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ws": "^8.20.0"
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.28.6",
|
||||
"eslint": "^9.39.4",
|
||||
"jasmine": "^6.2.0",
|
||||
"mongodb-runner": "^6.7.8",
|
||||
"jasmine": "^6.1.0",
|
||||
"mongodb-runner": "^6.7.1",
|
||||
"nodemon": "^3.1.14",
|
||||
"nyc": "^18.0.0",
|
||||
"prettier": "^3.8.3"
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "^3.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
"ws": "$ws",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const setDocumentCount = async (extUserId, docsCount) => {
|
||||
export const setDocumentCount = async extUserId => {
|
||||
if (extUserId) {
|
||||
try {
|
||||
// Update count in contracts_Users class
|
||||
@@ -6,14 +6,7 @@ export const setDocumentCount = async (extUserId, docsCount) => {
|
||||
extQuery.equalTo('objectId', extUserId);
|
||||
const contractUser = await extQuery.first({ useMasterKey: true });
|
||||
if (contractUser) {
|
||||
if (docsCount) {
|
||||
const count = contractUser.get('DocumentCount')
|
||||
? contractUser.get('DocumentCount') + Number(docsCount)
|
||||
: 0 + Number(docsCount);
|
||||
contractUser.set('DocumentCount', count);
|
||||
} else {
|
||||
contractUser.increment('DocumentCount', 1);
|
||||
}
|
||||
contractUser.increment('DocumentCount', 1);
|
||||
await contractUser.save(null, { useMasterKey: true });
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Workflow helpers for placeholder/audit-trail processing. These helpers
|
||||
// are intentionally agnostic of any role classification — they treat every
|
||||
// non-prefill placeholder as a participant, and only the 'Signed' activity
|
||||
// counts toward completion. Builds with role-aware behaviour layer extra
|
||||
// filtering on top.
|
||||
|
||||
// Audit-trail activities that mark a placeholder as having fulfilled its
|
||||
// completion duty.
|
||||
export const COMPLETION_ACTIVITIES = ['Signed'];
|
||||
|
||||
// A placeholder participates in completion unless it is a prefill entry
|
||||
// (which only pre-populates field values without acting on the document).
|
||||
export function isParticipantBasic(placeholder) {
|
||||
return placeholder?.Role !== 'prefill';
|
||||
}
|
||||
|
||||
// A placeholder participates in completion unless it is a prefill entry or a viewer.
|
||||
export function isCompletionRelevant(placeholder) {
|
||||
if (!isParticipantBasic(placeholder)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Locate a placeholder index by signerObjId. Prefill placeholders are
|
||||
// excluded so caller indices stay aligned with the participant list.
|
||||
export function findPlaceholderIndex(placeholders, signerObjId) {
|
||||
if (!Array.isArray(placeholders) || !signerObjId) return -1;
|
||||
return placeholders.findIndex(
|
||||
p => (p?.signerObjId || p?.signerPtr?.objectId) === signerObjId && p?.Role !== 'prefill'
|
||||
);
|
||||
}
|
||||
|
||||
// Strict-order gating: returns the signerObjId of the prior placeholder
|
||||
// still pending, or null when the strict-order requirement is satisfied.
|
||||
export function findPendingPriorSigner(placeholders, idx, auditTrail) {
|
||||
if (!Array.isArray(placeholders) || idx <= 0) return null;
|
||||
const trail = Array.isArray(auditTrail) ? auditTrail : [];
|
||||
for (let i = 0; i < idx; i++) {
|
||||
const ph = placeholders[i];
|
||||
if (!isCompletionRelevant(ph)) continue;
|
||||
const signerObjId = ph?.signerObjId || ph?.signerPtr?.objectId;
|
||||
if (!signerObjId) continue;
|
||||
const acted = trail.some(
|
||||
a => a?.UserPtr?.objectId === signerObjId && COMPLETION_ACTIVITIES.includes(a?.Activity)
|
||||
);
|
||||
if (!acted) return signerObjId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user