Merged in feature/IO-2433-esignature (pull request #3133)

Feature/IO-2433 esignature
This commit is contained in:
Dave Richer
2026-03-14 01:17:41 +00:00
21 changed files with 1792 additions and 13 deletions

304
server/esign/esign-new.js Normal file
View File

@@ -0,0 +1,304 @@
const { Documenso } = require("@documenso/sdk-typescript");
const axios = require("axios");
const { jsrAuthString } = require("../utils/utils");
const logger = require("../utils/logger");
const DOCUMENSO_API_KEY = "api_asojim0czruv13ud";//Done on a by team basis,
const documenso = new Documenso({
apiKey: DOCUMENSO_API_KEY,//Done on a by team basis,
serverURL: "https://stg-app.documenso.com/api/v2",
});
const JSR_SERVER = "https://reports.test.imex.online";
const jsreport = require("@jsreport/nodejs-client");
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
async function distributeDocument(req, res) {
try {
const client = req.userGraphQLClient;
const { documentId } = req.body;
const distributeResult = await documenso.documents.distribute({
documentId,
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
jobid: req.body.jobid,
bodyshopid: req.body.bodyshopid,
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
useremail: req.user?.email,
type: 'esig-distribute'
}
})
res.json({ success: true, distributeResult });
} catch (error) {
console.error("Error distributing document:", error?.data);
logger.log(`esig-distribute-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while distributing the document." });
}
}
async function deleteDocument(req, res) {
try {
const { documentId } = req.body;
//TODO: This needs to be hardened to prevent deleting other people's documents, completed ones, etc.
const deleteResult = await documenso.documents.delete({
documentId
});
res.json({ success: true, deleteResult });
} catch (error) {
console.error("Error deleting document:", error?.data);
logger.log(`esig-delete-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while deleting the document." });
}
}
async function newEsignDocument(req, res) {
try {
const client = req.userGraphQLClient;
const { bodyshop } = req.body
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
//Get the Job data.
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid: req.body.jobid });
const createDocumentResponse = await documenso.documents.create({
payload: {
title: esigData?.title,
externalId: `${req.body.jobid}|${req.user?.email}`, //Have to pass the uploaded by later on. Limited to 255 chars.
recipients: [
{
email: "patrick@imexsystems.ca",//jobData.ownr_ea,
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
role: "SIGNER",
}
],
meta: {
timezone: bodyshop.timezone,
dateFormat: "MM/dd/yyyy hh:mm a",
language: "en",
subject: esigData?.subject,
message: esigData?.message,
}
},
file: fileBlob
});
const documentResult = await documenso.documents.get({
documentId: createDocumentResponse.id,
});
if (esigData?.fields && esigData.fields.length > 0) {
try {
await documenso.envelopes.fields.createMany({
envelopeId: createDocumentResponse.envelopeId,
data: esigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
});
} catch (error) {
logger.log(`esig-new-fields-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
}
}
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
//add to job audit trail.
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
jobid: req.body.jobid,
bodyshopid: bodyshop.id,
operation: `Esignature document created. Subject: ${esigData?.subject || "No subject"}, Message: ${esigData?.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
useremail: req.user?.email,
type: 'esig-create'
}
})
res.json({ token: presignToken.token, documentId: createDocumentResponse.id, envelopeId: createDocumentResponse.envelopeId });
}
catch (error) {
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while creating the e-sign document." });
}
}
async function RenderTemplate({ req }) {
//TODO Refactor to pull
const jsrAuth = jsrAuthString()
const jsreportClient = new jsreport("https://reports.test.imex.online", process.env.JSR_USER, process.env.JSR_PASSWORD);
const { templateObject, bodyshop } = req.body;
let { contextData, useShopSpecificTemplate, shopSpecificFolder, esigData } = await fetchContextData({ templateObject, jsrAuth, req });
const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name];
let reportRequest = {
template: {
name: useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${templateObject.name}` : `/${templateObject.name}`,
recipe: "chrome-pdf",
...(!ignoreCustomMargins && {
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px"
}
}),
},
data: {
...contextData,
...templateObject.variables,
...templateObject.context,
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(),
defaultSorters: templateObject?.defaultSorters
}
};
const render = await jsreportClient.render(reportRequest);
//Check render object and download. It should be the PDF?
const pdfBuffer = await render.body()
return { pdf: pdfBuffer, esigData }
}
const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
const { bodyshop } = req.body
const folders = await axios.get(`${JSR_SERVER}/odata/folders`, {
headers: { Authorization: jsrAuth }
});
const shopSpecificFolder = folders.data.value.find((f) => f.name === bodyshop.imexshopid);
const jsReportQueries = await axios.get(
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{ headers: { Authorization: jsrAuth } }
);
const jsReportEsig = await axios.get(
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
{ headers: { Authorization: jsrAuth } }
);
let templateQueryToExecute;
let esigData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportQueries.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
templateQueryToExecute = atob(shopSpecificTemplate.content);
}
let shopSpecificEsig = jsReportEsig.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificEsig) {
esigData = (atob(shopSpecificEsig.content));
}
}
if (!templateQueryToExecute) {
const generalTemplate = jsReportQueries.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
templateQueryToExecute = atob(generalTemplate.content);
}
if (!esigData) {
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate && generalTemplate.content) {
esigData = atob(generalTemplate?.content);
}
}
const client = req.userGraphQLClient;
// In the print center, we will never have sorters or filters.
// We have no template filters or sorters, so we can just execute the query and return the data
// if (!hasFilters && !hasSorters && !hasDefaultSorters) {
let contextData = {};
if (templateQueryToExecute) {
const data = await client.request(
templateQueryToExecute,
templateObject.variables,
);
contextData = data;
}
let parsedEsigData
try {
parsedEsigData = esigData ? JSON.parse(esigData) : null;
} catch (error) {
console.log("Error parsing esig data", error);
parsedEsigData = {}
}
return {
contextData,
useShopSpecificTemplate,
shopSpecificFolder,
esigData: parsedEsigData
};
// }
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
};
module.exports = {
newEsignDocument,
distributeDocument,
deleteDocument
}
// const sample_esig_for_jsr = {
// "fields": [
// {
// "placeholder": "[[signature]]",
// "type": "SIGNATURE"
// },
// {
// "placeholder": "[[date]]",
// "type": "DATE"
// }
// ],
// "subject": "CASL Auth Set in JSR",
// "message": "CASL Message set in JSR",
// "title": "CASL Title set in JSR"
// }

393
server/esign/webhook.js Normal file
View File

@@ -0,0 +1,393 @@
const { Documenso } = require("@documenso/sdk-typescript");
const fs = require("fs");
const path = require("path");
const logger = require("../utils/logger");
const { QUERY_META_FOR_ESIG_COMPLETION, INSERT_ESIGNATURE_DOCUMENT, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
const { uploadFileBuffer } = require("../media/imgproxy-media");
const client = require('../graphql-client/graphql-client').client;
const documenso = new Documenso({
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
serverURL: "https://stg-app.documenso.com/api/v2",
});
const webhookTypeEnums = {
DOCUMENT_CREATED: "DOCUMENT_CREATED",
DOCUMENT_SENT: "DOCUMENT_SENT",
DOCUMENT_COMPLETED: "DOCUMENT_COMPLETED",
DOCUMENT_REJECTED: "DOCUMENT_REJECTED",
DOCUMENT_CANCELLED: "DOCUMENT_CANCELLED",
DOCUMENT_OPENED: "DOCUMENT_OPENED",
DOCUMENT_SIGNED: "DOCUMENT_SIGNED",
}
async function esignWebhook(req, res) {
console.log("Esign Webhook Received:", req.body);
try {
const message = req.body
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
event: message.event,
body: message
});
switch (message.event) {
case webhookTypeEnums.DOCUMENT_CREATED:
//This is largely a throwaway event we know it was created.
console.log("Document created event received. Document ID:", message.payload.documentId);
// Here you can add any additional processing you want to do when a document is created
break;
case webhookTypeEnums.DOCUMENT_COMPLETED:
console.log("Document completed event received. Document ID:", message.payload.documentId);
await handleDocumentCompleted(message.payload);
// Here you can add any additional processing you want to do when a document is completed
break;
case webhookTypeEnums.DOCUMENT_SIGNED:
console.log("Document signed event received. Document ID:", message.payload.documentId);
// Here you can add any additional processing you want to do when a document is signed
break;
default:
console.log(`Unhandled event type: ${message.event}`);
}
// const result = await documenso.documents.download({
// documentId: req.body.payload.id,
// });
// result.resultingBuffer = Buffer.from(result.resultingArrayBuffer);
// // Save the document to a file for testing purposes
// const downloadsDir = path.join(__dirname, '../downloads');
// if (!fs.existsSync(downloadsDir)) {
// fs.mkdirSync(downloadsDir, { recursive: true });
// }
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
// fs.writeFileSync(filePath, result.resultingBuffer);
// console.log(result)
res.sendStatus(200)
} catch (error) {
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
message: error.message, stack: error.stack,
body: req.body
});
// const downloadsDir = path.join(__dirname, '../downloads');
// if (!fs.existsSync(downloadsDir)) {
// fs.mkdirSync(downloadsDir, { recursive: true });
// }
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
// fs.writeFileSync(filePath, Buffer.from(err.body));
// console.error("Error handling esign webhook:", err);
res.sendStatus(500)
}
}
async function handleDocumentCompleted(payload = sampleComplete) {
//Check if the bodyshop is on image proxy or not
try {
//Split the external id to get the uploaded user.
const [jobid, uploaded_by] = payload.externalId.split("|");
if (!jobid || !uploaded_by) {
throw new Error(`Invalid externalId format. Expected "jobid|uploaded_by", got "${payload.externalId}"`);
}
const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, {
jobid
});
const document = await documenso.document.documentDownload({
documentId: payload.id,
});
const response = await fetch(document.downloadUrl);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`;
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
//LMS not yet implemented.
} else {
//S3 Upload
const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" });
if (!uploadResult.success) {
logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", {
message: uploadResult.message,
stack: uploadResult.stack,
jobid: jobid,
documentId: payload.id
});
} else {
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
jobid: jobid,
documentId: payload.id,
s3Key: key,
bucket: uploadResult.bucket
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
jobid: jobs_by_pk.id,
bodyshopid: jobs_by_pk.bodyshop.id,
operation: `Esignature document with title ${payload.title} (ID: ${payload.documentMeta.id}) has been completed.`,
useremail: uploaded_by,
type: 'esig-complete'
}
})
//insert the document record with the s3 key and bucket info.
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
docInput: {
jobid: jobs_by_pk.id,
uploaded_by: uploaded_by,
key,
type: "application/pdf",
extension: "pdf",
bodyshopid: jobs_by_pk.bodyshop.id,
size: buffer.length,
takenat: new Date().toISOString(),
}
})
}
}
} catch (error) {
logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", {
message: error.message, stack: error.stack,
payload
});
}
}
module.exports = {
esignWebhook
}
const sampleComplete = {
"id": 10929,
"title": "CASL Title set in JSR",
"source": "DOCUMENT",
"status": "COMPLETED",
"teamId": 742,
"userId": 654,
"Recipient": [
{
"id": 24997,
"name": "James Tschetter",
"role": "SIGNER",
"email": "patrick@imexsystems.ca",
"token": "uMom0GwL29NBqMfohGpUE",
"signedAt": "2026-02-27T22:11:52.835Z",
"expiresAt": "2026-05-28T22:10:48.991Z",
"documentId": 10929,
"readStatus": "OPENED",
"sendStatus": "SENT",
"templateId": null,
"authOptions": {
"accessAuth": [],
"actionAuth": []
},
"signingOrder": null,
"signingStatus": "SIGNED",
"rejectionReason": null,
"documentDeletedAt": null,
"expirationNotifiedAt": null
}
],
"createdAt": "2026-02-27T22:10:10.580Z",
"deletedAt": null,
"updatedAt": "2026-02-27T22:11:57.753Z",
"externalId": null,
"formValues": null,
"recipients": [
{
"id": 24997,
"name": "James Tschetter",
"role": "SIGNER",
"email": "patrick@imexsystems.ca",
"token": "uMom0GwL29NBqMfohGpUE",
"signedAt": "2026-02-27T22:11:52.835Z",
"expiresAt": "2026-05-28T22:10:48.991Z",
"documentId": 10929,
"readStatus": "OPENED",
"sendStatus": "SENT",
"templateId": null,
"authOptions": {
"accessAuth": [],
"actionAuth": []
},
"signingOrder": null,
"signingStatus": "SIGNED",
"rejectionReason": null,
"documentDeletedAt": null,
"expirationNotifiedAt": null
}
],
"templateId": null,
"visibility": "EVERYONE",
"authOptions": {
"globalAccessAuth": [],
"globalActionAuth": []
},
"completedAt": "2026-02-27T22:11:57.752Z",
"documentMeta": {
"id": "cmm5g3y7u00ecad1sv3ague1w",
"message": "CASL Message set in JSR",
"subject": "CASL Auth Set in JSR",
"language": "en",
"timezone": "Etc/UTC",
"dateFormat": "yyyy-MM-dd hh:mm a",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"emailSettings": {
"documentDeleted": true,
"documentPending": true,
"recipientSigned": true,
"recipientRemoved": true,
"documentCompleted": true,
"ownerDocumentCompleted": true,
"recipientSigningRequest": true
},
"distributionMethod": "EMAIL",
"drawSignatureEnabled": true,
"typedSignatureEnabled": true,
"allowDictateNextSigner": false,
"uploadSignatureEnabled": true
}
}
// const sampleBody = {
// event: "DOCUMENT_COMPLETED",
// payload: {
// Recipient: [
// {
// authOptions: {
// accessAuth: [
// ],
// actionAuth: [
// ],
// },
// documentDeletedAt: null,
// documentId: 9827,
// email: "patrick@imexsystems.ca",
// expired: null,
// id: 13311,
// name: "Customer Fullname",
// readStatus: "OPENED",
// rejectionReason: null,
// role: "SIGNER",
// sendStatus: "SENT",
// signedAt: "2026-01-30T18:29:12.648Z",
// signingOrder: null,
// signingStatus: "SIGNED",
// templateId: null,
// token: "uiEWIsXUPTbWHd7QedVgt",
// },
// ],
// authOptions: {
// globalAccessAuth: [
// ],
// globalActionAuth: [
// ],
// },
// completedAt: "2026-01-30T18:29:16.279Z",
// createdAt: "2026-01-30T18:28:48.861Z",
// deletedAt: null,
// documentMeta: {
// allowDictateNextSigner: false,
// dateFormat: "yyyy-MM-dd hh:mm a",
// distributionMethod: "EMAIL",
// drawSignatureEnabled: true,
// emailSettings: {
// documentCompleted: true,
// documentDeleted: true,
// documentPending: true,
// ownerDocumentCompleted: true,
// recipientRemoved: false,
// recipientSigned: true,
// recipientSigningRequest: true,
// },
// id: "cml17vfb200qjad1t2spxnc1n",
// language: "en",
// message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ",
// redirectUrl: null,
// signingOrder: "PARALLEL",
// subject: "Repair Authorization for ABC Collision",
// timezone: "Etc/UTC",
// typedSignatureEnabled: true,
// uploadSignatureEnabled: true,
// },
// externalId: null,
// formValues: null,
// id: 9827,
// recipients: [
// {
// authOptions: {
// accessAuth: [
// ],
// actionAuth: [
// ],
// },
// documentDeletedAt: null,
// documentId: 9827,
// email: "patrick@imexsystems.ca",
// expired: null,
// id: 13311,
// name: "Customer Fullname",
// readStatus: "OPENED",
// rejectionReason: null,
// role: "SIGNER",
// sendStatus: "SENT",
// signedAt: "2026-01-30T18:29:12.648Z",
// signingOrder: null,
// signingStatus: "SIGNED",
// templateId: null,
// token: "uiEWIsXUPTbWHd7QedVgt",
// },
// ],
// source: "DOCUMENT",
// status: "COMPLETED",
// teamId: 742,
// templateId: null,
// title: "Repair Authorization - 1/30/2026, 6:28:48 PM",
// updatedAt: "2026-01-30T18:29:16.280Z",
// userId: 654,
// visibility: "EVERYONE",
// },
// createdAt: "2026-01-30T18:29:18.504Z",
// webhookEndpoint: "https://dev.patrickfic.com/esign/webhook",
// }
function replaceAccents(str) {
// Verifies if the String has accents and replace them
if (str.search(/[\xC0-\xFF]/g) > -1) {
str = str
.replace(/[\xC0-\xC5]/g, "A")
.replace(/[\xC6]/g, "AE")
.replace(/[\xC7]/g, "C")
.replace(/[\xC8-\xCB]/g, "E")
.replace(/[\xCC-\xCF]/g, "I")
.replace(/[\xD0]/g, "D")
.replace(/[\xD1]/g, "N")
.replace(/[\xD2-\xD6\xD8]/g, "O")
.replace(/[\xD9-\xDC]/g, "U")
.replace(/[\xDD]/g, "Y")
.replace(/[\xDE]/g, "P")
.replace(/[\xE0-\xE5]/g, "a")
.replace(/[\xE6]/g, "ae")
.replace(/[\xE7]/g, "c")
.replace(/[\xE8-\xEB]/g, "e")
.replace(/[\xEC-\xEF]/g, "i")
.replace(/[\xF1]/g, "n")
.replace(/[\xF2-\xF6\xF8]/g, "o")
.replace(/[\xF9-\xFC]/g, "u")
.replace(/[\xFE]/g, "p")
.replace(/[\xFD\xFF]/g, "y");
}
return str;
}
`Unexpected Status or Content-Type: Status 200 Content-Type application/pdf\nBody: %PDF-1.7\n%<25><><EFBFBD><EFBFBD>\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Names 74 0 R\n/Dests 75 0 R\n/Info 77 0 R\n/Lang (en-US)\n/Version /1.7\n>>\nendobj\n77 0 obj\n<<\n/Type /Info\n/CreationDate (D:20260227230617Z00'00')\n/Producer <FEFF007000640066002D006C006900620020002800680074007400700073003A002F002F006700690074006800750062002E0063006F006D002F0048006F007000640069006E0067002F007000640066002D006C006900620029>\n/ModDate (D:20260227231057Z)…<>5[<5B>><3E>Wu7<><37>V<EFBFBD><56><EFBFBD><EFBFBD><EFBFBD>Pw<50>WX<57>ܮJ'6NWg<57>vYϳ<><CFB3><EFBFBD><EFBFBD><EFBFBD>Щr<D0A9>\n\t+<2B>1<EFBFBD><10>m{휑 <0C>hwb<><62><EFBFBD>8<EFBFBD><38>q y<>1e<31>)۱<>5m<35><6D><08><>MVM!<21>m<EFBFBD>[A<><41><10>{l<><6C>\t<EFBFBD>hia4<61><34>Tm<54><6D>8<><38>a<>e<EFBFBD>}<7D> ߫<><DFAB><15>]MVpяG<D18F><47>֏<EFBFBD>jJ<"<22>A<EFBFBD>mO*<2A>P<EFBFBD> <0B><><><7F><EFBFBD><EFBFBD>ѧЛ\nendstream\nendobj\n26 0 obj\n<<\n/Length 478/Filter /FlateDecode\n>>\nstream\nx<EFBFBD>MSK<EFBFBD>9<08><>)<29><>*<04>O<EFBFBD>i<EFBFBD><69>,<2C><>o <20><>kS%<25>$<EFBFBD><EFBFBD>hR\rS'<27>I<EFBFBD><49>~<7E><03><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T[/<2F>{<05>k<EFBFBD>FC#<23><>֛<><D69B><EFBFBD>;Ӏ<>[<5B>⫀m<E2AB80>|Q<1F><>\x1b<EFBFBD><16>><3E> R<><52><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD>E#<23>pI<70><49>._H<5F>ᆫt<E186AB>k<EFBFBD>D3p<33>I<EFBFBD><49><EFBFBD><EFBFBD><01>W2<57><32><EFBFBD>oJ0<4A>j<EFBFBD><6A><EFBFBD>j#<23><>!<21>$<EFBFBD><EFBFBD>-<2D><08><><EFBFBD><EFBFBD><EFBFBD><><CEAB><EFBFBD>TI|8D<38>H<1C><>Y<EFBFBD><59>x<EFBFBD><78><EFBFBD><EFBFBD>1<EFBFBD>73%<25>u<EFBFBD>T<EFBFBD><54>Ӑ.rcb<63>x<EFBFBD><78>Dd6=<3D><>Oڏ1 ^<5E>-<2D>...and 252354 more chars`

View File

@@ -0,0 +1,95 @@
export type WebhookEventType =
| "DOCUMENT_CREATED"
| "DOCUMENT_SENT"
| "DOCUMENT_COMPLETED"
| "DOCUMENT_REJECTED"
| "DOCUMENT_CANCELLED"
| "DOCUMENT_OPENED"
| "DOCUMENT_SIGNED";
export interface AuthOptions {
accessAuth: unknown[];
actionAuth: unknown[];
}
export interface Recipient {
id: number;
name: string;
role: string;
email: string;
token?: string | null;
signedAt?: string | null;
expiresAt?: string | null;
documentId?: number;
readStatus?: string | null;
sendStatus?: string | null;
templateId?: number | null;
authOptions?: AuthOptions;
signingOrder?: number | null;
signingStatus?: string | null;
rejectionReason?: string | null;
documentDeletedAt?: string | null;
expirationNotifiedAt?: string | null;
}
export interface EmailSettings {
documentDeleted: boolean;
documentPending: boolean;
recipientSigned: boolean;
recipientRemoved: boolean;
documentCompleted: boolean;
ownerDocumentCompleted: boolean;
recipientSigningRequest: boolean;
}
export interface DocumentMeta {
id: string;
message?: string | null;
subject?: string | null;
language?: string | null;
timezone?: string | null;
dateFormat?: string | null;
redirectUrl?: string | null;
signingOrder?: string | null;
emailSettings?: EmailSettings;
distributionMethod?: string | null;
drawSignatureEnabled?: boolean;
typedSignatureEnabled?: boolean;
allowDictateNextSigner?: boolean;
uploadSignatureEnabled?: boolean;
}
export interface DocumentAuthOptions {
globalAccessAuth: unknown[];
globalActionAuth: unknown[];
}
export interface DocumentPayload {
id: number;
title?: string | null;
source?: string | null;
status?: string | null;
teamId?: number | null;
userId?: number | null;
Recipient?: Recipient[];
recipients?: Recipient[];
createdAt?: string | null;
deletedAt?: string | null;
updatedAt?: string | null;
externalId?: string | null;
formValues?: unknown | null;
templateId?: number | null;
visibility?: string | null;
authOptions?: DocumentAuthOptions;
completedAt?: string | null;
documentMeta?: DocumentMeta | null;
}
export interface WebhookEventPayload {
event: WebhookEventType;
payload: DocumentPayload;
createdAt?: string | null;
webhookEndpoint?: string | null;
}
export default WebhookEventPayload;

View File

@@ -2253,18 +2253,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
exports.INSERT_NEW_TRANSITION = (
includeOldTransition
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
insert_transitions_one(object: $newTransition) {
id
}
${
includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
${includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}`
: ""
: ""
}
}`;
@@ -3256,3 +3254,46 @@ exports.SET_JOB_DMS_ID = `mutation SetJobDmsId($id: uuid!, $dms_id: String!, $dm
kmin
}
}`;
exports.QUERY_JOB_FOR_SIGNATURE = `query QUERY_JOB_FOR_SIGNATURE($jobid: uuid!) {
jobs_by_pk(id: $jobid) {
id
ownr_fn
ownr_ln
ownr_co_nm
ownr_ea
ownr_ph1
}
}
`
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
insert_audit_trail_one(object: $obj) {
id
}
}
`
exports.QUERY_META_FOR_ESIG_COMPLETION = `query QUERY_META_FOR_ESIG_COMPLETION($jobid: uuid!) {
jobs_by_pk(id: $jobid) {
id
ro_number
bodyshop {
id
uselocalmediaserver
localmediatoken
localmediaserverhttp
localmediaservernetwork
}
}
}`
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($docInput: documents_insert_input!) {
insert_documents_one(object: $docInput) {
id
name
key
}
}
`

View File

@@ -94,6 +94,47 @@ const generateSignedUploadUrls = async (req, res) => {
}
};
/**
* Upload a file buffer directly to S3.
* Accepts either `req.file.buffer` (e.g. from multer) or `req.body.buffer` (base64 string).
*/
const uploadFileBuffer = async ({ key, contentType, buffer }) => {
try {
if (!key) {
throw new Error("key is required");
}
if (!buffer) {
throw new Error("buffer is required");
}
const isPdf = key.toLowerCase().endsWith(".pdf");
const client = new S3Client({ region: InstanceRegion() });
const putParams = {
Bucket: imgproxyDestinationBucket,
Key: key,
Body: buffer,
StorageClass: "INTELLIGENT_TIERING"
};
if (contentType) {
putParams.ContentType = contentType;
} else if (isPdf) {
putParams.ContentType = "application/pdf";
}
await client.send(new PutObjectCommand(putParams));
return ({ success: true, key, bucket: imgproxyDestinationBucket });
} catch (error) {
return { success: false, message: error.message, stack: error.stack };
}
};
/**
* Get Thumbnail URLS
* @param req
@@ -500,6 +541,7 @@ const keyStandardize = (doc) => {
module.exports = {
generateSignedUploadUrls,
uploadFileBuffer,
getThumbnailUrls,
getOriginalImageByDocumentId,
downloadFiles,

View File

@@ -0,0 +1,17 @@
const express = require("express");
const router = express.Router();
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
const { newEsignDocument, distributeDocument, deleteDocument } = require("../esign/esign-new");
const { esignWebhook } = require("../esign/webhook");
//router.use(validateFirebaseIdTokenMiddleware);
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
router.post("/webhook", esignWebhook);
module.exports = router;

View File

@@ -2,6 +2,9 @@ exports.servertime = (req, res) => {
res.status(200).send(new Date());
};
exports.jsrAuthString =() => {
return "Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64")
}
exports.jsrAuth = async (req, res) => {
res.send("Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64"));
res.send(exports.jsrAuthString());
};