mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-20 06:35:54 +02:00
v2.12.0
feat: create duplicate template functionality feat: add support of csv and xlsx for bulk contacts import feat: add info text for forms and dashboard button feat: implement functionality to delete a PDF page while editing the document feat: show completed documents in which signers included but not owner in completed reports feat: add delete folder button in opensign drive feat: introduce Bcc email support for sending completed documents feat: allow merging multiple pdf while drafting document and template feat: add 'My Initials' tab for auto-signing in request-sign flow feat: add support for redirect URL to navigate after document completion feat: introduce privacy policy and digital signature terms before signing documents. feat: add feature to allow adding new pages to existing document feat: add save signature, initials, stamp functionality in sign pad for logged in user feat: add preferences menu feat: add save custom email template, notifyonsignature, set timezone, allow signature types in preferences feat: secure local url feat: implement Italian language translation feat: implement German language translation feat: update menu name from report to documents and shift contactbook in main menu feat: provide edit contact functionality fix: unable to delete folder when all its documents are deleted fix: fields.push is not function fix: document loading issue in opensign drive fix: adjust the guest signature flow to display the document in full screen, eliminating any blank space which is displayed below the place holder in mobile view fix: resolve issue of instance of pdfdict or pdfstream but got undefined build(deps): update dependencies refactor: change note text from add contact form
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import fs from 'node:fs';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { formatTimeInTimezone } from '../../../Utils.js';
|
||||
|
||||
export default async function GenerateCertificate(docDetails) {
|
||||
const timezone = docDetails?.ExtUserPtr?.Timezone || '';
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
// `fontBytes` is used to embed custom font in pdf
|
||||
const fontBytes = fs.readFileSync('./font/times.ttf'); //
|
||||
@@ -23,19 +25,23 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const timeText = 11;
|
||||
const textKeyColor = rgb(0.12, 0.12, 0.12);
|
||||
const textValueColor = rgb(0.3, 0.3, 0.3);
|
||||
const completedAt = new Date();
|
||||
const completedUTCtime = completedAt.toUTCString();
|
||||
const completedAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
const completedAtperTimezone = formatTimeInTimezone(completedAt, timezone);
|
||||
const completedUTCtime = completedAtperTimezone;
|
||||
const signersCount = docDetails?.Signers?.length || 1;
|
||||
const generateAt = new Date();
|
||||
const generatedUTCTime = generateAt.toUTCString();
|
||||
const generateAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
const generatedAtperTimezone = formatTimeInTimezone(generateAt, timezone);
|
||||
const generatedUTCTime = generatedAtperTimezone;
|
||||
const generatedOn = 'Generated On ' + generatedUTCTime;
|
||||
const OriginIp = docDetails?.OriginIp || '';
|
||||
const company = docDetails?.ExtUserPtr?.Company || '';
|
||||
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
|
||||
const createdAtperTimezone = formatTimeInTimezone(createdAt, timezone);
|
||||
const IsEnableOTP = docDetails?.IsEnableOTP || false;
|
||||
const filteredaudit = docDetails?.AuditTrail?.filter(x => x?.UserPtr?.objectId);
|
||||
const auditTrail =
|
||||
docDetails?.Signers?.length > 0
|
||||
? docDetails.AuditTrail.map(x => {
|
||||
? filteredaudit?.map(x => {
|
||||
const data = docDetails.Signers.find(y => y.objectId === x.UserPtr.objectId);
|
||||
return {
|
||||
...data,
|
||||
@@ -48,16 +54,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
: [
|
||||
{
|
||||
...docDetails.ExtUserPtr,
|
||||
ipAddress: docDetails?.AuditTrail[0].ipAddress,
|
||||
SignedOn: docDetails?.AuditTrail[0]?.SignedOn || generatedUTCTime,
|
||||
ViewedOn:
|
||||
docDetails?.AuditTrail[0]?.ViewedOn ||
|
||||
docDetails?.AuditTrail[0]?.SignedOn ||
|
||||
generatedUTCTime,
|
||||
Signature: docDetails?.AuditTrail[0]?.Signature || '',
|
||||
ipAddress: filteredaudit[0].ipAddress,
|
||||
SignedOn: filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
ViewedOn: filteredaudit[0]?.ViewedOn || filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
Signature: filteredaudit[0]?.Signature || '',
|
||||
},
|
||||
];
|
||||
|
||||
const ownerName = docDetails?.SenderName || docDetails.ExtUserPtr?.Name || 'n/a';
|
||||
const ownerEmail = docDetails?.SenderMail || docDetails.ExtUserPtr?.Email || 'n/a';
|
||||
const half = width / 2;
|
||||
// Draw a border
|
||||
page.drawRectangle({
|
||||
@@ -131,10 +136,10 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
page.drawText(docDetails.Name, {
|
||||
page.drawText(docDetails?.Name, {
|
||||
x: 140,
|
||||
y: 665,
|
||||
size: text,
|
||||
size: docDetails?.Name?.length >= 78 ? 12 : text,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
@@ -162,7 +167,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
page.drawText(`${new Date(createdAt).toUTCString()}`, {
|
||||
page.drawText(`${createdAtperTimezone}`, {
|
||||
x: 105,
|
||||
y: 625,
|
||||
size: text,
|
||||
@@ -213,7 +218,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText(`${docDetails.ExtUserPtr.Name}`, {
|
||||
page.drawText(ownerName, {
|
||||
x: 105,
|
||||
y: 545,
|
||||
size: text,
|
||||
@@ -227,7 +232,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText(`${docDetails.ExtUserPtr.Email}`, {
|
||||
page.drawText(ownerEmail, {
|
||||
x: 105,
|
||||
y: 525,
|
||||
size: text,
|
||||
@@ -287,15 +292,16 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
page.drawText('Viewed on :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
page.drawText(`${new Date(x.ViewedOn).toUTCString()}`, {
|
||||
x: half + 112,
|
||||
//new Date(x.ViewedOn).toUTCString()
|
||||
page.drawText(`${formatTimeInTimezone(x.ViewedOn, timezone)}`, {
|
||||
x: half + 102,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
@@ -319,15 +325,16 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
page.drawText('Signed on :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition3 + 5,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
page.drawText(`${new Date(x.SignedOn).toUTCString()}`, {
|
||||
x: half + 108,
|
||||
// new Date(x.SignedOn).toUTCString()
|
||||
page.drawText(`${formatTimeInTimezone(x.SignedOn, timezone)}`, {
|
||||
x: half + 98,
|
||||
y: yPosition3 + 5,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
@@ -351,14 +358,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
if (IsEnableOTP) {
|
||||
page.drawText('Security level :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition4 + 10,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
page.drawText('Email, OTP Auth', {
|
||||
x: half + 125,
|
||||
x: half + 115,
|
||||
y: yPosition4 + 10,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
@@ -458,15 +465,16 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
currentPage.drawText('Viewed on :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${new Date(x.ViewedOn).toUTCString()}`, {
|
||||
x: half + 112,
|
||||
// new Date(x.ViewedOn).toUTCString()
|
||||
currentPage.drawText(`${formatTimeInTimezone(x.ViewedOn, timezone)}`, {
|
||||
x: half + 102,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
@@ -490,15 +498,16 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
currentPage.drawText('Signed on :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition3 + 5,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${new Date(x.SignedOn).toUTCString()}`, {
|
||||
x: half + 108,
|
||||
// new Date(x.SignedOn).toUTCString()
|
||||
currentPage.drawText(`${formatTimeInTimezone(x.SignedOn, timezone)}`, {
|
||||
x: half + 98,
|
||||
y: yPosition3 + 5,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
@@ -523,14 +532,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
|
||||
if (IsEnableOTP) {
|
||||
currentPage.drawText('Security level :', {
|
||||
x: half + 55,
|
||||
x: half + 45,
|
||||
y: yPosition4 + 10,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(`Email, OTP Auth`, {
|
||||
x: half + 125,
|
||||
x: half + 115,
|
||||
y: yPosition4 + 10,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
|
||||
@@ -4,37 +4,31 @@ import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { cloudServerUrl, replaceMailVaribles, saveFileUsage } from '../../../Utils.js';
|
||||
import {
|
||||
cloudServerUrl,
|
||||
replaceMailVaribles,
|
||||
saveFileUsage,
|
||||
getSecureUrl,
|
||||
} from '../../../Utils.js';
|
||||
import GenerateCertificate from './GenerateCertificate.js';
|
||||
import uploadFileToS3 from '../uploadFiletoS3.js';
|
||||
import { Placeholder } from './Placeholder.js';
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = process.env.APP_ID;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
const eSignName = 'opensign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
// `updateDoc` is used to create url in from pdfFile
|
||||
async function uploadFile(pdfName, filepath, adapter) {
|
||||
async function uploadFile(
|
||||
pdfName,
|
||||
filepath,
|
||||
) {
|
||||
try {
|
||||
const filedata = fs.readFileSync(filepath);
|
||||
let fileUrl;
|
||||
if (adapter?.bucketName) {
|
||||
const adapterConfig = {
|
||||
id: adapter?.id,
|
||||
fileAdapter: adapter?.fileAdapter,
|
||||
bucketName: adapter?.bucketName,
|
||||
region: adapter?.region,
|
||||
endpoint: adapter?.endpoint,
|
||||
accessKeyId: adapter?.accessKeyId,
|
||||
secretAccessKey: adapter?.secretAccessKey,
|
||||
baseUrl: adapter?.baseUrl,
|
||||
};
|
||||
// `uploadFileToS3` is used to save document in user's file storage
|
||||
fileUrl = await uploadFileToS3(filedata, pdfName, 'application/pdf', adapterConfig);
|
||||
} else {
|
||||
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
await file.save({ useMasterKey: true });
|
||||
fileUrl = file.url();
|
||||
}
|
||||
const fileRes = getSecureUrl(file.url());
|
||||
fileUrl = fileRes.url;
|
||||
|
||||
return { imageUrl: fileUrl };
|
||||
} catch (err) {
|
||||
@@ -161,67 +155,80 @@ async function sendCompletedMail(obj) {
|
||||
const recipient = signersMail;
|
||||
let subject = `Document "${pdfName}" has been signed by all parties`;
|
||||
let 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='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'> <div><img src=" +
|
||||
mailLogo +
|
||||
" height='50' style='padding:20px'/> </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 signed successfully</p></div><div><p style='padding:20px;font-family:system-ui;font-size:14px'>All parties have successfully signed the document " +
|
||||
`<b>"${pdfName}"</b>` +
|
||||
'. Kindly download the document from the attachment.</p></div> </div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender ' +
|
||||
sender.Email +
|
||||
' directly. If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href=www.opensignlabs.com target=_blank>here</a>.</p></div></div></body></html>';
|
||||
"<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><img src=${mailLogo} height='50' style='padding:20px'/></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 signed successfully</p></div><div>` +
|
||||
`<p style='padding:20px;font-family:system-ui;font-size:14px;'>All parties have successfully signed the document <b>"${pdfName}"</b>. Kindly download the document from the attachment.</p>` +
|
||||
`</div> </div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender ${sender.Email} directly.` +
|
||||
'If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href=www.opensignlabs.com target=_blank>here</a>.</p></div></div></body></html>';
|
||||
|
||||
if (obj?.isCustomMail) {
|
||||
try {
|
||||
const tenantCreditsQuery = new Parse.Query('partners_Tenant');
|
||||
tenantCreditsQuery.equalTo('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: sender.UserId.objectId,
|
||||
});
|
||||
const res = await tenantCreditsQuery.first();
|
||||
if (res) {
|
||||
const _res = JSON.parse(JSON.stringify(res));
|
||||
if (_res?.CompletionSubject) {
|
||||
subject = _res?.CompletionSubject;
|
||||
const tenant = sender?.TenantId;
|
||||
if (tenant) {
|
||||
subject = tenant?.CompletionSubject || '';
|
||||
body = tenant?.CompletionBody || '';
|
||||
} else {
|
||||
const userId = sender?.CreatedBy?.objectId || sender?.UserId?.objectId;
|
||||
if (userId) {
|
||||
try {
|
||||
const tenantQuery = new Parse.Query('partners_Tenant');
|
||||
tenantQuery.equalTo('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: userId,
|
||||
});
|
||||
const tenantRes = await tenantQuery.first();
|
||||
if (tenantRes) {
|
||||
const _tenantRes = JSON.parse(JSON.stringify(tenantRes));
|
||||
subject = _tenantRes?.CompletionSubject || '';
|
||||
body = _tenantRes?.CompletionBody || '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error in fetch tenant in signpdf', err.message);
|
||||
}
|
||||
if (_res?.CompletionBody) {
|
||||
body = _res?.CompletionBody;
|
||||
}
|
||||
const expireDate = doc.ExpiryDate.iso;
|
||||
const newDate = new Date(expireDate);
|
||||
const localExpireDate = newDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const variables = {
|
||||
document_title: pdfName,
|
||||
sender_name: sender.Name,
|
||||
sender_mail: sender.Email,
|
||||
sender_phone: sender?.Phone || '',
|
||||
receiver_name: sender.Name,
|
||||
receiver_email: sender.Email,
|
||||
receiver_phone: sender?.Phone || '',
|
||||
expiry_date: localExpireDate,
|
||||
company_name: sender.Company,
|
||||
};
|
||||
const replaceVar = replaceMailVaribles(subject, body, variables);
|
||||
subject = replaceVar.subject;
|
||||
body = replaceVar.body;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error in fetch tenant in signpdf', err.message);
|
||||
}
|
||||
const expireDate = doc.ExpiryDate.iso;
|
||||
const newDate = new Date(expireDate);
|
||||
const localExpireDate = newDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const variables = {
|
||||
document_title: pdfName,
|
||||
sender_name:
|
||||
sender.Name,
|
||||
sender_mail: doc?.SenderMail || sender.Email,
|
||||
sender_phone: sender?.Phone || '',
|
||||
receiver_name: sender.Name,
|
||||
receiver_email: sender.Email,
|
||||
receiver_phone: sender?.Phone || '',
|
||||
expiry_date: localExpireDate,
|
||||
company_name: sender.Company,
|
||||
};
|
||||
const replaceVar = replaceMailVaribles(subject, body, variables);
|
||||
subject = replaceVar.subject;
|
||||
body = replaceVar.body;
|
||||
}
|
||||
const Bcc = doc?.Bcc?.length > 0 ? doc.Bcc.map(x => x.Email) : '';
|
||||
const params = {
|
||||
extUserId: sender.objectId,
|
||||
url: url,
|
||||
from: 'OpenSign™',
|
||||
from:
|
||||
'OpenSign™',
|
||||
replyto:
|
||||
doc?.ExtUserPtr?.Email ||
|
||||
'',
|
||||
recipient: recipient,
|
||||
subject: subject,
|
||||
pdfName: pdfName,
|
||||
html: body,
|
||||
mailProvider: obj.mailProvider,
|
||||
bcc: Bcc,
|
||||
certificatePath: `./exports/certificate_${doc.objectId}.pdf`,
|
||||
filename: obj?.filename,
|
||||
};
|
||||
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, {
|
||||
headers: {
|
||||
@@ -232,72 +239,15 @@ async function sendCompletedMail(obj) {
|
||||
});
|
||||
}
|
||||
|
||||
// `sendDoctoWebhook` is used to send res data of document on webhook
|
||||
async function sendDoctoWebhook(doc, Url, event, signUser, certificateUrl) {
|
||||
let signers = [];
|
||||
if (signUser) {
|
||||
signers = { name: signUser?.Name, email: signUser?.Email, phone: signUser?.Phone };
|
||||
} else {
|
||||
signers = doc?.Signers?.map(x => ({ name: x.Name, email: x.Email, phone: x.Phone })) || [
|
||||
{ name: doc?.ExtUserPtr?.Name, email: doc?.ExtUserPtr?.Email, phone: doc?.ExtUserPtr?.Phone },
|
||||
];
|
||||
}
|
||||
|
||||
if (doc.ExtUserPtr?.Webhook) {
|
||||
const time =
|
||||
event === 'signed'
|
||||
? { signer: signers, signedAt: new Date() }
|
||||
: { signers: signers, completedAt: new Date() };
|
||||
const certificate = certificateUrl ? { certificate: certificateUrl } : {};
|
||||
const params = {
|
||||
event: event,
|
||||
objectId: doc?.objectId,
|
||||
file: Url || '',
|
||||
...certificate,
|
||||
name: doc?.Name,
|
||||
note: doc?.Note || '',
|
||||
description: doc?.Description || '',
|
||||
...time,
|
||||
createdAt: doc?.createdAt,
|
||||
};
|
||||
axios
|
||||
.post(doc?.ExtUserPtr?.Webhook, params, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then(res => {
|
||||
try {
|
||||
const webhook = new Parse.Object('contracts_Webhook');
|
||||
webhook.set('Log', res?.status);
|
||||
webhook.set('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: doc.ExtUserPtr.UserId.objectId,
|
||||
});
|
||||
webhook.save(null, { useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.log('err save in contracts_Webhook', err.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Err send data to webhook', err.message);
|
||||
try {
|
||||
const webhook = new Parse.Object('contracts_Webhook');
|
||||
webhook.set('Log', err?.status);
|
||||
webhook.set('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: doc.ExtUserPtr.UserId.objectId,
|
||||
});
|
||||
webhook.save(null, { useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.log('err save in contracts_Webhook', err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// `sendMailsaveCertifcate` is used send completion mail and update complete status of document
|
||||
async function sendMailsaveCertifcate(doc, P12Buffer, isCustomMail, mailProvider, adapterConfig) {
|
||||
async function sendMailsaveCertifcate(
|
||||
doc,
|
||||
P12Buffer,
|
||||
isCustomMail,
|
||||
mailProvider,
|
||||
filename
|
||||
) {
|
||||
const certificate = await GenerateCertificate(doc);
|
||||
const certificatePdf = await PDFDocument.load(certificate);
|
||||
let passphrase = process.env.PASS_PHRASE;
|
||||
@@ -320,10 +270,13 @@ async function sendMailsaveCertifcate(doc, P12Buffer, isCustomMail, mailProvider
|
||||
const certificateOBJ = new SignPdf();
|
||||
// `signedCertificate` is used to sign certificate digitally
|
||||
const signedCertificate = await certificateOBJ.sign(CertificateBuffer, p12);
|
||||
|
||||
const certificatePath = `./exports/certificate_${doc.objectId}.pdf`;
|
||||
//below is used to save signed certificate in exports folder
|
||||
fs.writeFileSync('./exports/certificate.pdf', signedCertificate);
|
||||
const file = await uploadFile('certificate.pdf', './exports/certificate.pdf', adapterConfig);
|
||||
fs.writeFileSync(certificatePath, signedCertificate);
|
||||
const file = await uploadFile(
|
||||
'certificate.pdf',
|
||||
certificatePath,
|
||||
);
|
||||
const body = { CertificateUrl: file.imageUrl };
|
||||
await axios.put(serverUrl + '/classes/contracts_Document/' + doc.objectId, body, {
|
||||
headers: {
|
||||
@@ -336,10 +289,9 @@ async function sendMailsaveCertifcate(doc, P12Buffer, isCustomMail, mailProvider
|
||||
if (doc.IsSendMail === false) {
|
||||
console.log("don't send mail");
|
||||
} else {
|
||||
sendCompletedMail({ isCustomMail, doc, mailProvider });
|
||||
sendCompletedMail({ isCustomMail, doc, mailProvider, filename });
|
||||
}
|
||||
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
|
||||
sendDoctoWebhook(doc, doc?.SignedUrl, 'completed', '', file.imageUrl);
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -357,7 +309,7 @@ async function PDF(req) {
|
||||
const sign = req.params.signature || '';
|
||||
// below bode is used to get info of docId
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
const resDoc = await docQuery.first({ useMasterKey: true });
|
||||
if (!resDoc) {
|
||||
@@ -371,17 +323,6 @@ async function PDF(req) {
|
||||
}
|
||||
}
|
||||
const _resDoc = resDoc?.toJSON();
|
||||
// `fileAdapterId` is used check document uploaded in custom file adapter and get customFileAdapter id
|
||||
const fileAdapterId = _resDoc?.FileAdapterId || '';
|
||||
let adapterConfig = {};
|
||||
if (fileAdapterId) {
|
||||
// `FileAdapter` is used to credintials of file adapter
|
||||
const FileAdapter =
|
||||
_resDoc?.ExtUserPtr?.TenantId?.FileAdapters?.find(x => x.id === fileAdapterId) || {};
|
||||
if (FileAdapter) {
|
||||
adapterConfig = FileAdapter;
|
||||
}
|
||||
}
|
||||
let signUser;
|
||||
let className;
|
||||
// `reqUserId` is send throught pdfrequest signing flow
|
||||
@@ -433,38 +374,32 @@ async function PDF(req) {
|
||||
const randomNumber = Math.floor(Math.random() * 5000);
|
||||
// below regex is used to replace all word with "_" except A to Z, a to z, numbers
|
||||
const docName = _resDoc?.Name?.replace(/[^a-zA-Z0-9._-]/g, '_')?.toLowerCase();
|
||||
const name = `signed_${docName}_${randomNumber}.pdf`;
|
||||
const filename = docName?.length > 100 ? docName?.slice(0, 100) : docName;
|
||||
const name = `signed_${filename}_${randomNumber}.pdf`;
|
||||
const filePath = `./exports/${name}`;
|
||||
let pdfSize = PdfBuffer.length;
|
||||
if (isCompleted) {
|
||||
const signersName = _resDoc.Signers?.map(x => x.Name + ' <' + x.Email + '>');
|
||||
if (signersName && signersName.length > 0) {
|
||||
// `pdflibAddPlaceholder` is used to add code of only digitial sign without widget
|
||||
const pdfDoc = await PDFDocument.load(PdfBuffer);
|
||||
pdflibAddPlaceholder({
|
||||
pdfDoc: pdfDoc,
|
||||
reason: 'Digitally signed by OpenSign for ' + signersName?.join(', '),
|
||||
location: 'n/a',
|
||||
name: eSignName,
|
||||
contactInfo: eSigncontact,
|
||||
signatureLength: 15000,
|
||||
});
|
||||
const pdfWithPlaceholderBytes = await pdfDoc.save();
|
||||
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
|
||||
} else {
|
||||
// `pdflibAddPlaceholder` is used to add code of only digitial sign without widget (signyourself)
|
||||
const pdfDoc = await PDFDocument.load(PdfBuffer);
|
||||
pdflibAddPlaceholder({
|
||||
pdfDoc: pdfDoc,
|
||||
reason: 'Digitally signed by OpenSign for ' + username + ' <' + userEmail + '>',
|
||||
location: 'n/a',
|
||||
name: eSignName,
|
||||
contactInfo: eSigncontact,
|
||||
signatureLength: 15000,
|
||||
});
|
||||
const pdfWithPlaceholderBytes = await pdfDoc.save();
|
||||
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
|
||||
}
|
||||
const reason =
|
||||
signersName && signersName.length > 0
|
||||
? signersName?.join(', ')
|
||||
: username + ' <' + userEmail + '>';
|
||||
const pdfDoc = await PDFDocument.load(PdfBuffer);
|
||||
const form = pdfDoc.getForm();
|
||||
// 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();
|
||||
Placeholder({
|
||||
pdfDoc: pdfDoc,
|
||||
reason: 'Digitally signed by OpenSign for ' + reason,
|
||||
location: 'n/a',
|
||||
name: eSignName,
|
||||
contactInfo: eSigncontact,
|
||||
signatureLength: 15000,
|
||||
});
|
||||
const pdfWithPlaceholderBytes = await pdfDoc.save();
|
||||
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
|
||||
//`new signPDF` create new instance of pdfBuffer and p12Buffer
|
||||
const OBJ = new SignPdf();
|
||||
// `signedDocs` is used to signpdf digitally
|
||||
@@ -480,7 +415,10 @@ async function PDF(req) {
|
||||
}
|
||||
|
||||
// `uploadFile` is used to upload pdf to aws s3 and get it's url
|
||||
const data = await uploadFile(name, filePath, adapterConfig);
|
||||
const data = await uploadFile(
|
||||
name,
|
||||
filePath,
|
||||
);
|
||||
|
||||
if (data && data.imageUrl) {
|
||||
// `axios` is used to update signed pdf url in contracts_Document classes for given DocId
|
||||
@@ -493,12 +431,17 @@ async function PDF(req) {
|
||||
className, // className based on flow
|
||||
sign // sign base64
|
||||
);
|
||||
sendDoctoWebhook(_resDoc, data.imageUrl, 'signed', signUser);
|
||||
sendNotifyMail(_resDoc, signUser, mailProvider);
|
||||
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
|
||||
if (updatedDoc && updatedDoc.isCompleted) {
|
||||
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
|
||||
sendMailsaveCertifcate(doc, P12Buffer, isCustomMail, mailProvider, adapterConfig);
|
||||
sendMailsaveCertifcate(
|
||||
doc,
|
||||
P12Buffer,
|
||||
isCustomMail,
|
||||
mailProvider,
|
||||
name
|
||||
);
|
||||
}
|
||||
// `fs.unlinkSync` is used to remove exported signed pdf file from exports folder
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { PDFArray, CharCodes } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Extends PDFArray class in order to make ByteRange look like this:
|
||||
* /ByteRange [0 /********** /********** /**********]
|
||||
* Not this:
|
||||
* /ByteRange [ 0 /********** /********** /********** ]
|
||||
*/
|
||||
export default class PDFArrayCustom extends PDFArray {
|
||||
static withContext(context) {
|
||||
return new PDFArrayCustom(context);
|
||||
}
|
||||
|
||||
clone(context) {
|
||||
const clone = PDFArrayCustom.withContext(context || this.context);
|
||||
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||
clone.push(this.array[idx]);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let arrayString = '[';
|
||||
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||
arrayString += this.get(idx).toString();
|
||||
if (idx < len - 1) arrayString += ' ';
|
||||
}
|
||||
arrayString += ']';
|
||||
return arrayString;
|
||||
}
|
||||
|
||||
sizeInBytes() {
|
||||
let size = 2;
|
||||
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||
size += this.get(idx).sizeInBytes();
|
||||
if (idx < len - 1) size += 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
copyBytesInto(buffer, offset) {
|
||||
const initialOffset = offset;
|
||||
|
||||
buffer[offset++] = CharCodes.LeftSquareBracket;
|
||||
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||
offset += this.get(idx).copyBytesInto(buffer, offset);
|
||||
if (idx < len - 1) buffer[offset++] = CharCodes.Space;
|
||||
}
|
||||
buffer[offset++] = CharCodes.RightSquareBracket;
|
||||
|
||||
return offset - initialOffset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
DEFAULT_SIGNATURE_LENGTH,
|
||||
DEFAULT_BYTE_RANGE_PLACEHOLDER,
|
||||
SUBFILTER_ADOBE_PKCS7_DETACHED,
|
||||
ANNOTATION_FLAGS,
|
||||
SIG_FLAGS,
|
||||
SignPdfError,
|
||||
} from '@signpdf/utils';
|
||||
import {
|
||||
PDFArray,
|
||||
PDFNumber,
|
||||
PDFName,
|
||||
PDFHexString,
|
||||
PDFString,
|
||||
PDFInvalidObject,
|
||||
PDFDict,
|
||||
} from 'pdf-lib';
|
||||
|
||||
export const Placeholder = ({
|
||||
pdfDoc,
|
||||
pdfPage,
|
||||
reason,
|
||||
contactInfo,
|
||||
name,
|
||||
location,
|
||||
signingTime = new Date(),
|
||||
signatureLength = DEFAULT_SIGNATURE_LENGTH,
|
||||
byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER,
|
||||
subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED,
|
||||
widgetRect = [0, 0, 0, 0],
|
||||
appName,
|
||||
}) => {
|
||||
if (!pdfDoc && !pdfPage) {
|
||||
throw new SignPdfError('PDFDoc or PDFPage must be set.', SignPdfError.TYPE_INPUT);
|
||||
}
|
||||
|
||||
const doc = pdfDoc || pdfPage.doc;
|
||||
const page = pdfPage || doc.getPages()[0];
|
||||
|
||||
const byteRange = PDFArray.withContext(doc.context);
|
||||
byteRange.push(PDFNumber.of(0));
|
||||
byteRange.push(PDFName.of(byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(byteRangePlaceholder));
|
||||
|
||||
const placeholder = PDFHexString.of(String.fromCharCode(0).repeat(signatureLength));
|
||||
|
||||
const appBuild = appName ? { App: { Name: appName } } : {};
|
||||
const signatureDict = doc.context.obj({
|
||||
Type: 'Sig',
|
||||
Filter: 'Adobe.PPKLite',
|
||||
SubFilter: subFilter,
|
||||
ByteRange: byteRange,
|
||||
Contents: placeholder,
|
||||
Reason: PDFString.of(reason),
|
||||
M: PDFString.fromDate(signingTime),
|
||||
ContactInfo: PDFString.of(contactInfo),
|
||||
Name: PDFString.of(name),
|
||||
Location: PDFString.of(location),
|
||||
Prop_Build: {
|
||||
Filter: { Name: 'Adobe.PPKLite' },
|
||||
...appBuild,
|
||||
},
|
||||
});
|
||||
|
||||
const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes());
|
||||
signatureDict.copyBytesInto(signatureBuffer, 0);
|
||||
const signatureObj = PDFInvalidObject.of(signatureBuffer);
|
||||
const signatureDictRef = doc.context.register(signatureObj);
|
||||
|
||||
const rect = PDFArray.withContext(doc.context);
|
||||
widgetRect.forEach(c => rect.push(PDFNumber.of(c)));
|
||||
const apStream = doc.context.formXObject([], {
|
||||
BBox: widgetRect,
|
||||
Resources: {},
|
||||
});
|
||||
|
||||
const widgetDict = doc.context.obj({
|
||||
Type: 'Annot',
|
||||
Subtype: 'Widget',
|
||||
FT: 'Sig',
|
||||
Rect: rect,
|
||||
V: signatureDictRef,
|
||||
T: PDFString.of('Signature1'),
|
||||
F: ANNOTATION_FLAGS.PRINT,
|
||||
P: page.ref,
|
||||
AP: { N: doc.context.register(apStream) },
|
||||
});
|
||||
|
||||
const widgetDictRef = doc.context.register(widgetDict);
|
||||
|
||||
let annotations = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
|
||||
if (!annotations) {
|
||||
annotations = doc.context.obj([]);
|
||||
}
|
||||
annotations.push(widgetDictRef);
|
||||
page.node.set(PDFName.of('Annots'), annotations);
|
||||
|
||||
let acroForm = doc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict);
|
||||
if (!acroForm) {
|
||||
acroForm = doc.context.obj({ Fields: [] });
|
||||
const acroFormRef = doc.context.register(acroForm);
|
||||
doc.catalog.set(PDFName.of('AcroForm'), acroFormRef);
|
||||
}
|
||||
|
||||
let sigFlags = acroForm.has(PDFName.of('SigFlags'))
|
||||
? acroForm.get(PDFName.of('SigFlags'))
|
||||
: PDFNumber.of(0);
|
||||
|
||||
const updatedFlags = PDFNumber.of(
|
||||
sigFlags.asNumber() | SIG_FLAGS.SIGNATURES_EXIST | SIG_FLAGS.APPEND_ONLY
|
||||
);
|
||||
acroForm.set(PDFName.of('SigFlags'), updatedFlags);
|
||||
|
||||
let fields = acroForm.get(PDFName.of('Fields'));
|
||||
if (fields instanceof PDFArray) {
|
||||
fields.push(widgetDictRef);
|
||||
}
|
||||
// else if (fields) {
|
||||
// const newFields = PDFArray.withContext(doc.context);
|
||||
// fields.asArray().forEach(field => newFields.push(field));
|
||||
// newFields.push(widgetDictRef);
|
||||
// acroForm.set(PDFName.of('Fields'), newFields);
|
||||
// }
|
||||
else {
|
||||
const newFields = PDFArray.withContext(doc.context);
|
||||
newFields.push(widgetDictRef);
|
||||
acroForm.set(PDFName.of('Fields'), newFields);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user