import fs from 'node:fs'; import axios from 'axios'; 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, getSecureUrl, } from '../../../Utils.js'; import GenerateCertificate from './GenerateCertificate.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, ) { try { 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; return { imageUrl: fileUrl }; } catch (err) { console.log('Err ', err); // `fs.unlinkSync` is used to remove exported signed pdf file from exports folder fs.unlinkSync(filepath); } } // `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document async function updateDoc(docId, url, userId, ipAddress, data, className, sign) { try { const UserPtr = { __type: 'Pointer', className: className, objectId: userId }; const obj = { UserPtr: UserPtr, SignedUrl: url, Activity: 'Signed', ipAddress: ipAddress, SignedOn: new Date(), Signature: sign, }; let updateAuditTrail; if (data.AuditTrail && data.AuditTrail.length > 0) { const AuditTrail = JSON.parse(JSON.stringify(data.AuditTrail)); const existingIndex = AuditTrail.findIndex( entry => entry.UserPtr.objectId === userId && entry.Activity !== 'Created' ); existingIndex !== -1 ? (AuditTrail[existingIndex] = { ...AuditTrail[existingIndex], ...obj }) : AuditTrail.push(obj); updateAuditTrail = AuditTrail; } else { updateAuditTrail = [obj]; } const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed'); let isCompleted = false; if (data.Signers && data.Signers.length > 0) { if (auditTrail.length === data.Placeholders.length) { isCompleted = true; } } else { isCompleted = true; } const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted }; const signedRes = await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, { headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY, }, }); return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail }; } catch (err) { console.log('update doc err ', err); return 'err'; } } // `sendNotifyMail` is used to send notification mail of signer signed the document async function sendNotifyMail(doc, signUser, mailProvider) { try { const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0; const signersCount = doc?.Placeholders?.length; const remaingsign = signersCount - auditTrailCount; if (remaingsign > 1 && doc?.NotifyOnSignatures) { const sender = doc.ExtUserPtr; const pdfName = doc.Name; const creatorName = doc.ExtUserPtr.Name; const creatorEmail = doc.ExtUserPtr.Email; const signerName = signUser.Name; const signerEmail = signUser.Email; const mailLogo = 'https://qikinnovation.ams3.digitaloceanspaces.com/logo.png'; const viewDocUrl = `${process.env.PUBLIC_URL}/recipientSignPdf/${doc.objectId}`; const subject = `Document "${pdfName}" has been signed by ${signerName}`; const body = "
" + `
` + `

Document signed by ${signerName}

` + `

Dear ${creatorName},

` + `

${pdfName} has been signed by ${signerName} "${signerEmail}" successfully

` + `

View Document

` + `

This is an automated email from OpenSign™. 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 OpenSign™ here.

`; const params = { extUserId: sender.objectId, from: 'OpenSign™', recipient: creatorEmail, subject: subject, pdfName: pdfName, html: body, mailProvider: mailProvider, }; await axios.post(serverUrl + '/functions/sendmailv3', params, { headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY, }, }); } } catch (err) { console.log('err in sendnotifymail', err); } } // `sendCompletedMail` is used to send copy of completed document mail async function sendCompletedMail(obj) { const url = obj.doc?.SignedUrl; const doc = obj.doc; const sender = obj.doc.ExtUserPtr; const pdfName = doc.Name; const mailLogo = 'https://qikinnovation.ams3.digitaloceanspaces.com/logo.png'; let signersMail; if (doc?.Signers?.length > 0) { const isOwnerExistsinSigners = doc?.Signers?.find(x => x.Email === sender.Email); signersMail = isOwnerExistsinSigners ? doc?.Signers?.map(x => x?.Email)?.join(',') : [...doc?.Signers?.map(x => x?.Email), sender.Email]?.join(','); } else { signersMail = sender.Email; } const recipient = signersMail; let subject = `Document "${pdfName}" has been signed by all parties`; let body = "
" + `
` + `

Document signed successfully

` + `

All parties have successfully signed the document "${pdfName}". Kindly download the document from the attachment.

` + `

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™ here.

'; if (obj?.isCustomMail) { 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); } } } 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™', 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: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY, }, }); } // `sendMailsaveCertifcate` is used send completion mail and update complete status of document 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; if (doc?.ExtUserPtr?.TenantId?.PfxFile?.password) { passphrase = doc?.ExtUserPtr?.TenantId?.PfxFile?.password; } const p12 = new P12Signer(P12Buffer, { passphrase: passphrase || null }); // `pdflibAddPlaceholder` is used to add code of only digitial sign in certificate pdflibAddPlaceholder({ pdfDoc: certificatePdf, reason: 'Digitally signed by OpenSign.', location: 'n/a', name: eSignName, contactInfo: eSigncontact, signatureLength: 15000, }); const pdfWithPlaceholderBytes = await certificatePdf.save(); const CertificateBuffer = Buffer.from(pdfWithPlaceholderBytes); //`new signPDF` create new instance of CertificateBuffer and p12Buffer 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(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: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY, }, }); // used in API only if (doc.IsSendMail === false) { console.log("don't send mail"); } else { sendCompletedMail({ isCustomMail, doc, mailProvider, filename }); } saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId); } /** * * @param docId Id of Document in which user is signing * @param pdfFile base64 of pdfFile which you want sign * @returns if success {status, data} else {status, message} */ async function PDF(req) { try { const userIP = req.headers['x-real-ip']; // client IPaddress const docId = req.params.docId; const reqUserId = req.params.userId; const isCustomMail = req.params.isCustomCompletionMail || false; const mailProvider = req.params.mailProvider || ''; 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,Bcc'); docQuery.equalTo('objectId', docId); const resDoc = await docQuery.first({ useMasterKey: true }); if (!resDoc) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.'); } const IsEnableOTP = resDoc?.get('IsEnableOTP') || false; // if `IsEnableOTP` is false then we don't have to check authentication if (IsEnableOTP) { if (!req?.user) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.'); } } const _resDoc = resDoc?.toJSON(); let signUser; let className; // `reqUserId` is send throught pdfrequest signing flow if (reqUserId) { // to get contracts_Contactbook details for currentuser from reqUserId const _contractUser = _resDoc.Signers.find(x => x.objectId === reqUserId); if (_contractUser) { signUser = _contractUser; className = 'contracts_Contactbook'; } } else { className = 'contracts_Users'; signUser = _resDoc.ExtUserPtr; } const username = signUser.Name; const userEmail = signUser.Email; if (req.params.pdfFile) { // `PdfBuffer` used to create buffer from pdf file let PdfBuffer = Buffer.from(req.params.pdfFile, 'base64'); // `P12Buffer` used to create buffer from p12 certificate let pfxFile = process.env.PFX_BASE64; let passphrase = process.env.PASS_PHRASE; if (_resDoc?.ExtUserPtr?.TenantId?.PfxFile?.base64) { pfxFile = _resDoc?.ExtUserPtr?.TenantId?.PfxFile?.base64; passphrase = _resDoc?.ExtUserPtr?.TenantId?.PfxFile?.password; } // const P12Buffer = fs.readFileSync(); const P12Buffer = Buffer.from(pfxFile, 'base64'); const p12Cert = new P12Signer(P12Buffer, { passphrase: passphrase || null }); const UserPtr = { __type: 'Pointer', className: className, objectId: signUser.objectId }; const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: 'Signed', ipAddress: userIP }; let updateAuditTrail; if (_resDoc.AuditTrail && _resDoc.AuditTrail.length > 0) { updateAuditTrail = [..._resDoc.AuditTrail, obj]; } else { updateAuditTrail = [obj]; } const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed'); let isCompleted = false; if (_resDoc.Signers && _resDoc.Signers.length > 0) { if (auditTrail.length === _resDoc.Signers.length) { isCompleted = true; } } else { isCompleted = true; } 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 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 + '>'); 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 const signedDocs = await OBJ.sign(PdfBuffer, p12Cert); //`saveUrl` is used to save signed pdf in exports folder fs.writeFileSync(filePath, signedDocs); pdfSize = signedDocs.length; } else { //`saveUrl` is used to save signed pdf in exports folder fs.writeFileSync(filePath, PdfBuffer); pdfSize = PdfBuffer.length; } // `uploadFile` is used to upload pdf to aws s3 and get it's url 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 const updatedDoc = await updateDoc( req.params.docId, //docId data.imageUrl, // SignedUrl signUser.objectId, // userID userIP, // client ipAddress, _resDoc, // auditTrail, signers, etc data className, // className based on flow sign // sign base64 ); 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, name, ); } // `fs.unlinkSync` is used to remove exported signed pdf file from exports folder fs.unlinkSync(filePath); console.log(`New Signed PDF created called: ${filePath}`); if (updatedDoc.message === 'success') { return { status: 'success', data: data.imageUrl }; } else { const error = new Error('Please provide required parameters!'); error.code = 400; // Set the error code (e.g., 400 for bad request) throw error; } } } else { const error = new Error('Pdf file not present!'); error.code = 400; // Set the error code (e.g., 400 for bad request) throw error; } } catch (err) { console.log('Err in signpdf', err); throw err; } } export default PDF;