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:
prafull-opensignlabs
2025-02-10 14:41:16 +00:00
parent b55aff20d3
commit e299891174
260 changed files with 22438 additions and 25112 deletions
@@ -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,
+121 -178
View File
@@ -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);
}
};