mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-21 23:22:35 +02:00
Fix: Document title changes are not reflected when the document is sent to signers.
Merge pull request #1498 from nxglabs/staging
This commit is contained in:
@@ -49,8 +49,8 @@ async function sendMail(document, publicUrl) {
|
||||
let signPdf = `${hostUrl}/login/${encodeBase64}`;
|
||||
const orgName = document.ExtUserPtr.Company ? document.ExtUserPtr.Company : '';
|
||||
const senderObj = document?.ExtUserPtr;
|
||||
const mailBody = document?.ExtUserPtr?.TenantId?.RequestBody || '';
|
||||
const mailSubject = document?.ExtUserPtr?.TenantId?.RequestSubject || '';
|
||||
let mailBody = senderObj?.TenantId?.RequestBody || '';
|
||||
let mailSubject = senderObj?.TenantId?.RequestSubject || '';
|
||||
let replaceVar;
|
||||
if (mailBody && mailSubject) {
|
||||
const replacedRequestBody = mailBody.replace(/"/g, "'");
|
||||
@@ -126,8 +126,8 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || '';
|
||||
const mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || '';
|
||||
let mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || '';
|
||||
let mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || '';
|
||||
return {
|
||||
method: 'POST',
|
||||
path: '/app/classes/contracts_Document',
|
||||
|
||||
@@ -1,26 +1,78 @@
|
||||
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 {
|
||||
const TenantAppName = appName;
|
||||
const logo =
|
||||
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
|
||||
const opurl = ` <a href=www.opensignlabs.com target=_blank>here</a>`;
|
||||
const removePrefill =
|
||||
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
const signUser =
|
||||
removePrefill?.length > 0 &&
|
||||
removePrefill?.find(x => x?.signerPtr?.UserId?.objectId === userId);
|
||||
|
||||
const sender = doc.ExtUserPtr;
|
||||
const pdfName = doc.Name;
|
||||
const creatorName = doc.ExtUserPtr.Name;
|
||||
const creatorEmail = doc.ExtUserPtr.Email;
|
||||
const signerName = signUser?.signerPtr?.Name || '';
|
||||
const signerEmail = signUser?.signerPtr?.Email || signUser?.email || '';
|
||||
const viewDocUrl = `${publicUrl}/recipientSignPdf/${doc.objectId}`;
|
||||
const subject = `Document "${pdfName}" has been declined by ${signerName}`;
|
||||
const body =
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'/></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='background-color:white'>" +
|
||||
`<div>${logo}</div><div style='padding:2px;font-family:system-ui;background-color:#47a3ad'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Document declined by ${signerName}</p>` +
|
||||
`</div><div style='padding:20px;font-family:system-ui;font-size:14px'><p>Dear ${creatorName},</p>` +
|
||||
`<p>${pdfName} has been declined by ${signerName} "${signerEmail}" on ${new Date().toLocaleDateString()}.</p>` +
|
||||
`<p>Decline Reason: ${reason || 'Not specified'}</p>` +
|
||||
`<p><a href=${viewDocUrl} target=_blank>View Document</a></p></div></div><div><p>This is an automated email from ${TenantAppName}. For any queries regarding this email, ` +
|
||||
`please contact the sender ${creatorEmail} directly. If you think this email is inappropriate or spam, you may file a complaint with ${TenantAppName}${opurl}.</p></div></div></body></html>`;
|
||||
|
||||
const params = {
|
||||
extUserId: sender.objectId,
|
||||
from: TenantAppName,
|
||||
recipient: creatorEmail,
|
||||
subject: subject,
|
||||
pdfName: pdfName,
|
||||
html: body,
|
||||
};
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
} catch (err) {
|
||||
console.log('err in sendnotifymail', err);
|
||||
}
|
||||
}
|
||||
export default async function declinedocument(request) {
|
||||
const docId = request.params.docId;
|
||||
const reason = request.params?.reason || '';
|
||||
const declineBy = {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: request.params?.userId,
|
||||
};
|
||||
|
||||
const userId = request.params.userId;
|
||||
const declineBy = { __type: 'Pointer', className: '_User', objectId: userId };
|
||||
const publicUrl = request.headers.public_url;
|
||||
if (!docId) {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'missing parameter docId.');
|
||||
}
|
||||
try {
|
||||
const docCls = new Parse.Query('contracts_Document');
|
||||
docCls.include('ExtUserPtr.TenantId');
|
||||
docCls.include('ExtUserPtr.TenantId,Placeholders.signerPtr,Signers');
|
||||
const updateDoc = await docCls.get(docId, { useMasterKey: true });
|
||||
if (updateDoc) {
|
||||
const _doc = JSON.parse(JSON.stringify(updateDoc));
|
||||
const isEnableOTP = updateDoc?.get('IsEnableOTP') || false;
|
||||
if (!isEnableOTP) {
|
||||
updateDoc.set('IsDeclined', true);
|
||||
updateDoc.set('DeclineReason', reason);
|
||||
updateDoc.set('DeclineBy', declineBy);
|
||||
await updateDoc.save(null, { useMasterKey: true });
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
return 'document declined';
|
||||
} else {
|
||||
if (!request?.user) {
|
||||
@@ -30,6 +82,7 @@ export default async function declinedocument(request) {
|
||||
updateDoc.set('DeclineReason', reason);
|
||||
updateDoc.set('DeclineBy', declineBy);
|
||||
await updateDoc.save(null, { useMasterKey: true });
|
||||
sendDeclineMail(_doc, publicUrl, userId, reason);
|
||||
return 'document declined';
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -41,6 +41,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const maxX = width - margin - textWidth; // Ensures text stays inside the border with 30px margin
|
||||
const OriginIp = docDetails?.OriginIp || '';
|
||||
const company = docDetails?.ExtUserPtr?.Company || '';
|
||||
const documentHash = docDetails?.DocumentHash || '';
|
||||
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
|
||||
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
|
||||
const IsEnableOTP = docDetails?.IsEnableOTP || false;
|
||||
@@ -150,9 +151,36 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
if (documentHash) {
|
||||
page.drawText('Document hash (sha256) :', {
|
||||
x: 30,
|
||||
y: 670,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
page.drawText(documentHash, {
|
||||
x: 170,
|
||||
y: 670,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
}
|
||||
|
||||
const organizationY = documentHash ? 650 : 670;
|
||||
const createdOnY = organizationY - 20;
|
||||
const completedOnY = createdOnY - 20;
|
||||
const signersY = completedOnY - 20;
|
||||
const originatorHeaderY = signersY - 20;
|
||||
const nameY = originatorHeaderY - 17;
|
||||
const emailY = nameY - 20;
|
||||
const ipY = emailY - 20;
|
||||
|
||||
page.drawText('Organization :', {
|
||||
x: 30,
|
||||
y: 670,
|
||||
y: organizationY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
@@ -160,14 +188,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
|
||||
page.drawText(company, {
|
||||
x: 110,
|
||||
y: 670,
|
||||
y: organizationY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('Created on :', {
|
||||
x: 30,
|
||||
y: 650,
|
||||
y: createdOnY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
@@ -175,14 +203,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
|
||||
page.drawText(`${createdAtperTimezone}`, {
|
||||
x: 97,
|
||||
y: 650,
|
||||
y: createdOnY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('Completed on :', {
|
||||
x: 30,
|
||||
y: 630,
|
||||
y: completedOnY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
@@ -190,14 +218,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
|
||||
page.drawText(`${completedUTCtime}`, {
|
||||
x: 115,
|
||||
y: 630,
|
||||
y: completedOnY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('Signers :', {
|
||||
x: 30,
|
||||
y: 610,
|
||||
y: signersY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
@@ -205,75 +233,75 @@ export default async function GenerateCertificate(docDetails) {
|
||||
|
||||
page.drawText(`${signersCount}`, {
|
||||
x: 80,
|
||||
y: 610,
|
||||
y: signersY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('Document originator', {
|
||||
x: 30,
|
||||
y: 590,
|
||||
y: originatorHeaderY,
|
||||
size: 17,
|
||||
font: timesRomanFont,
|
||||
color: titleColor,
|
||||
});
|
||||
page.drawText('Name :', {
|
||||
x: 60,
|
||||
y: 573,
|
||||
y: nameY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText(ownerName, {
|
||||
x: 105,
|
||||
y: 573,
|
||||
y: nameY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('Email :', {
|
||||
x: 60,
|
||||
y: 553,
|
||||
y: emailY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText(ownerEmail, {
|
||||
x: 105,
|
||||
y: 553,
|
||||
y: emailY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
page.drawText('IP address :', {
|
||||
x: 60,
|
||||
y: 533,
|
||||
y: ipY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText(`${OriginIp}`, {
|
||||
x: 125,
|
||||
y: 533,
|
||||
y: ipY,
|
||||
size: text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
page.drawLine({
|
||||
start: { x: 30, y: 527 },
|
||||
end: { x: width - 30, y: 527 },
|
||||
start: { x: 30, y: ipY - 6 },
|
||||
end: { x: width - 30, y: ipY - 6 },
|
||||
color: rgb(0.12, 0.12, 0.12),
|
||||
thickness: 0.5,
|
||||
});
|
||||
let yPosition1 = 512;
|
||||
let yPosition2 = 498;
|
||||
let yPosition3 = 478;
|
||||
let yPosition4 = 458;
|
||||
let yPosition5 = 438;
|
||||
let yPosition6 = 418;
|
||||
let yPosition7 = 398;
|
||||
let yPosition8 = 363;
|
||||
let yPosition1 = ipY - 21;
|
||||
let yPosition2 = yPosition1 - 14;
|
||||
let yPosition3 = yPosition2 - 20;
|
||||
let yPosition4 = yPosition3 - 20;
|
||||
let yPosition5 = yPosition4 - 20;
|
||||
let yPosition6 = yPosition5 - 20;
|
||||
let yPosition7 = yPosition6 - 20;
|
||||
let yPosition8 = yPosition7 - 35;
|
||||
|
||||
auditTrail.slice(0, 3).forEach(async (x, i) => {
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import axios from 'axios';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import {
|
||||
@@ -28,6 +29,10 @@ const headers = {
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
};
|
||||
|
||||
function generateDocumentHash(buffer) {
|
||||
return createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
async function unlinkFile(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
try {
|
||||
@@ -44,11 +49,6 @@ 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;
|
||||
|
||||
@@ -61,7 +61,7 @@ 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) {
|
||||
async function updateDoc(docId, url, userId, ipAddress, data, className, sign, documentHash) {
|
||||
try {
|
||||
const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
|
||||
const obj = {
|
||||
@@ -100,8 +100,16 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
|
||||
isCompleted = true;
|
||||
}
|
||||
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
|
||||
if (documentHash && isCompleted) {
|
||||
body.DocumentHash = documentHash;
|
||||
}
|
||||
const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers });
|
||||
return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail };
|
||||
return {
|
||||
isCompleted: isCompleted,
|
||||
message: 'success',
|
||||
AuditTrail: updateAuditTrail,
|
||||
DocumentHash: documentHash && isCompleted ? documentHash : undefined,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log('update doc err ', err);
|
||||
return 'err';
|
||||
@@ -183,8 +191,8 @@ async function sendCompletedMail(obj) {
|
||||
if (obj?.isCustomMail) {
|
||||
const tenant = sender?.TenantId;
|
||||
if (tenant) {
|
||||
subject = tenant?.CompletionSubject || '';
|
||||
body = tenant?.CompletionBody || '';
|
||||
subject = tenant?.CompletionSubject ? tenant?.CompletionSubject : subject;
|
||||
body = tenant?.CompletionBody ? tenant?.CompletionBody : body;
|
||||
} else {
|
||||
const userId = sender?.CreatedBy?.objectId || sender?.UserId?.objectId;
|
||||
if (userId) {
|
||||
@@ -198,8 +206,8 @@ async function sendCompletedMail(obj) {
|
||||
const tenantRes = await tenantQuery.first({ useMasterKey: true });
|
||||
if (tenantRes) {
|
||||
const _tenantRes = JSON.parse(JSON.stringify(tenantRes));
|
||||
subject = _tenantRes?.CompletionSubject || '';
|
||||
body = _tenantRes?.CompletionBody || '';
|
||||
subject = _tenantRes?.CompletionSubject ? tenant?.CompletionSubject : subject;
|
||||
body = _tenantRes?.CompletionBody ? tenant?.CompletionBody : body;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error in fetch tenant in signpdf', err.message);
|
||||
@@ -427,6 +435,7 @@ async function PDF(req) {
|
||||
let filePath = `./exports/${name}`;
|
||||
let signedFilePath = `./exports/signed_${name}`;
|
||||
let pdfSize = PdfBuffer.length;
|
||||
let documentHash;
|
||||
if (isCompleted) {
|
||||
const signersName = _resDoc.Signers?.map(x => x.Name + ' <' + x.Email + '>');
|
||||
const reason =
|
||||
@@ -444,6 +453,7 @@ async function PDF(req) {
|
||||
//`saveUrl` is used to save signed pdf in exports folder
|
||||
fs.writeFileSync(signedFilePath, signedDocs);
|
||||
pdfSize = signedDocs.length;
|
||||
documentHash = generateDocumentHash(signedDocs);
|
||||
console.log(`✅ PDF digitally signed created: ${signedFilePath} \n`);
|
||||
} else {
|
||||
//`saveUrl` is used to save signed pdf in exports folder
|
||||
@@ -464,12 +474,17 @@ async function PDF(req) {
|
||||
userIP, // client ipAddress,
|
||||
_resDoc, // auditTrail, signers, etc data
|
||||
className, // className based on flow
|
||||
sign // sign base64
|
||||
sign, // sign base64
|
||||
isCompleted ? documentHash : undefined
|
||||
);
|
||||
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
|
||||
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
|
||||
if (updatedDoc && updatedDoc.isCompleted) {
|
||||
const hashForDoc = documentHash || updatedDoc?.DocumentHash;
|
||||
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
|
||||
if (hashForDoc) {
|
||||
doc.DocumentHash = hashForDoc;
|
||||
}
|
||||
sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`);
|
||||
} else {
|
||||
unlinkFile(pfxname);
|
||||
|
||||
@@ -159,7 +159,8 @@ export default async function sendMailGmailProvider(_extRes, template) {
|
||||
template;
|
||||
|
||||
if (_extRes) {
|
||||
const refresh_token = _extRes.google_refresh_token;
|
||||
let refresh_token = '';
|
||||
refresh_token = _extRes?.TenantId?.google_refresh_token;
|
||||
// generate access token
|
||||
const access_token = await refreshAccessToken(refresh_token);
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export default async function updateEmailTemplates(request) {
|
||||
const { tenantId, details } = request.params;
|
||||
|
||||
if (!tenantId || !details) {
|
||||
throw new Parse.Error(400, 'Missing tenantId or details.');
|
||||
}
|
||||
|
||||
if (!request.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'unauthorized');
|
||||
}
|
||||
|
||||
try {
|
||||
const extUser = new Parse.Query('contracts_Users');
|
||||
extUser.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: request.user.id });
|
||||
extUser.include('TenantId');
|
||||
const extUserRes = await extUser.first({ useMasterKey: true });
|
||||
const mailKeys = ['CompletionBody', 'CompletionSubject', 'RequestBody', 'RequestSubject'];
|
||||
Object.keys(details).forEach(key => {
|
||||
if (mailKeys.includes(key)) {
|
||||
if (details?.[key]) {
|
||||
extUserRes.set(key, details?.[key]);
|
||||
} else {
|
||||
extUserRes.unset(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
const updateExtRes = await extUserRes.save(null, { useMasterKey: true });
|
||||
if (updateExtRes) {
|
||||
const res = JSON.parse(JSON.stringify(updateExtRes));
|
||||
return res;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error while updating email templates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,11 @@ export default async function updateTenant(request) {
|
||||
// Update tenant details
|
||||
Object.keys(details).forEach(key => {
|
||||
if (validKeys.includes(key)) {
|
||||
tenant.set(key, details?.[key]);
|
||||
if (details?.[key] !== undefined) {
|
||||
tenant.set(key, details?.[key]);
|
||||
} else {
|
||||
tenant.unset(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user