From 52f43a600cd484849a426ac6edd30f2de605efdc Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 27 Feb 2026 15:44:23 -0800 Subject: [PATCH] IO-2433 Basic completion webhook, S3 upload, audit trail. --- .../esignature-modal.container.jsx | 17 +- .../print-center-item.component.jsx | 4 +- server/esign/esign-new.js | 144 ++++++--- server/esign/webhook.js | 284 ++++++++++++++++-- server/esign/webhook.types.ts | 95 ++++++ server/graphql-client/queries.js | 53 +++- server/media/imgproxy-media.js | 42 +++ server/routes/esignRoutes.js | 6 +- 8 files changed, 559 insertions(+), 86 deletions(-) create mode 100644 server/esign/webhook.types.ts diff --git a/client/src/components/esignature-modal/esignature-modal.container.jsx b/client/src/components/esignature-modal/esignature-modal.container.jsx index 847efa1e2..0c6553d88 100644 --- a/client/src/components/esignature-modal/esignature-modal.container.jsx +++ b/client/src/components/esignature-modal/esignature-modal.container.jsx @@ -6,19 +6,21 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectEsignature } from "../../redux/modals/modals.selectors"; import { EmbedUpdateDocumentV1 } from "@documenso/embed-react"; import axios from "axios"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - esignatureModal: selectEsignature + esignatureModal: selectEsignature, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("esignature")) }); -export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }) { +export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop }) { const { t } = useTranslation(); const { open, context } = esignatureModal; - const { token, envelopeId, documentId } = context; + const { token, envelopeId, documentId, jobid } = context; return ( { console.log("Document updated:", data.documentId); @@ -53,7 +55,12 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible } onClick={async () => { // Add your button click handler logic here try { - const distResult = await axios.post("/esign/distribute", { documentId, envelopeId }); + const distResult = await axios.post("/esign/distribute", { + documentId, + envelopeId, + jobid, + bodyshopid: bodyshop.id + }); console.log("Distribution result:", distResult); } catch (error) { console.error("Error distributing document:", error); diff --git a/client/src/components/print-center-item/print-center-item.component.jsx b/client/src/components/print-center-item/print-center-item.component.jsx index e325e1daf..191f6b811 100644 --- a/client/src/components/print-center-item/print-center-item.component.jsx +++ b/client/src/components/print-center-item/print-center-item.component.jsx @@ -64,7 +64,7 @@ export function PrintCenterItemComponent({ data: { token, documentId, evnelopeId } } = await axios.post("/esign/new", { name: item.key, - variables: { id: id }, + jobid: id, context, bodyshop, templateObject: { @@ -73,7 +73,7 @@ export function PrintCenterItemComponent({ } }); - setEsignatureContext({ context: { token, documentId, evnelopeId }, jobid: id }); + setEsignatureContext({ context: { token, documentId, evnelopeId, jobid: id } }); } catch (error) { console.log(error); } finally { diff --git a/server/esign/esign-new.js b/server/esign/esign-new.js index 387d49f8a..f8900bec9 100644 --- a/server/esign/esign-new.js +++ b/server/esign/esign-new.js @@ -2,23 +2,43 @@ 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." }); } } @@ -27,25 +47,32 @@ async function newEsignDocument(req, res) { try { const client = req.userGraphQLClient; - const { pdf: fileBuffer, esigFields } = await RenderTemplate({ client, req }) + 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: `Repair Authorization - ${new Date().toLocaleString()}`, + title: esigData?.title, + externalId: req.body.jobid, recipients: [ { - email: "patrick.fic@convenient-brands.com", - name: "Customer Fullname", + email: "patrick@imexsystems.ca",//jobData.ownr_ea, + name: `${jobData.ownr_fn} ${jobData.ownr_ln}`, role: "SIGNER", } ], meta: { - timezone: "America/Vancouver", + timezone: bodyshop.timezone, dateFormat: "MM/dd/yyyy hh:mm a", language: "en", - subject: "Repair Authorization for ABC Collision", - message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ", + subject: esigData?.subject, + message: esigData?.message, + } }, file: fileBlob @@ -55,35 +82,44 @@ async function newEsignDocument(req, res) { documentId: createDocumentResponse.id, }); - if (esigFields && esigFields.length > 0) { - console.log("Adding placeholder fields.") + + if (esigData?.fields && esigData.fields.length > 0) { try { - // await axios.post(`https://stg-app.documenso.com/api/v2/envelope/field/create-many`, { - // envelopeId: createDocumentResponse.envelopeId, - // data: esigFields.map(sigField => ({ ...sigField, recipientId: result.recipients[0].id, })) - // }, { - // headers: { - // Authorization: DOCUMENSO_API_KEY - // } - // }) - const fieldResult = await documenso.envelopes.fields.createMany({ + await documenso.envelopes.fields.createMany({ envelopeId: createDocumentResponse.envelopeId, - data: esigFields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, })) + data: esigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, })) }); } catch (error) { - console.log("Error adding placeholders", JSON.stringify(error, null, 2)); + 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) { - console.error("Error in newEsignDocument:", 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." }); } } @@ -95,10 +131,9 @@ async function RenderTemplate({ req }) { 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, esigFields } = await fetchContextData({ templateObject, jsrAuth, req }); - //TODO - Refactor to pull template content and render on server instead of posting back to client for rendering. This is necessary to get the rendered PDF buffer that we can then upload to Documenso. - const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name]; + 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}`, @@ -138,32 +173,29 @@ async function RenderTemplate({ req }) { //Check render object and download. It should be the PDF? const pdfBuffer = await render.body() - return { pdf: pdfBuffer, esigFields } + return { pdf: pdfBuffer, esigData } } const fetchContextData = async ({ templateObject, jsrAuth, req, }) => { const { bodyshop } = req.body - const server = "https://reports.test.imex.online"; - //jsreport.headers["FirebaseAuthorization"] = req.headers.authorization; - - const folders = await axios.get(`${server}/odata/folders`, { + 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( - `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, + `${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`, { headers: { Authorization: jsrAuth } } ); const jsReportEsig = await axios.get( - `${server}/odata/assets?$filter=name eq '${templateObject.name}.esig'`, + `${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`, { headers: { Authorization: jsrAuth } } ); let templateQueryToExecute; - let esigFields; + let esigData; let useShopSpecificTemplate = false; // let shopSpecificTemplate; @@ -179,7 +211,7 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => { (f) => f?.folder?.shortid === shopSpecificFolder.shortid ); if (shopSpecificEsig) { - esigFields = (atob(shopSpecificEsig.content)); + esigData = (atob(shopSpecificEsig.content)); } } @@ -188,23 +220,14 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => { useShopSpecificTemplate = false; templateQueryToExecute = atob(generalTemplate.content); } - if (!esigFields) { + if (!esigData) { const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder); useShopSpecificTemplate = false; if (generalTemplate && generalTemplate.content) { - esigFields = atob(generalTemplate?.content); + esigData = atob(generalTemplate?.content); } } - // Commented out for future revision debugging - // console.log('Template Object'); - // console.dir(templateObject); - // console.log('Unmodified Query'); - // console.dir(templateQueryToExecute); - - // const hasFilters = templateObject?.filters?.length > 0; - // const hasSorters = templateObject?.sorters?.length > 0; - // const hasDefaultSorters = templateObject?.defaultSorters?.length > 0; const client = req.userGraphQLClient; @@ -219,11 +242,20 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => { ); 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, - esigFields: esigFields ? JSON.parse(esigFields) : [] //TODO: Do the parsing earlier and harden this. Causes a lot of failures on mini format issues. + esigData: parsedEsigData }; // } @@ -234,3 +266,21 @@ module.exports = { newEsignDocument, distributeDocument } + + + +// 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" +// } \ No newline at end of file diff --git a/server/esign/webhook.js b/server/esign/webhook.js index dc3adefd2..b863522f2 100644 --- a/server/esign/webhook.js +++ b/server/esign/webhook.js @@ -2,51 +2,258 @@ 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 result = await documenso.documents.download({ - documentId: req.body.payload.id, + const message = req.body + logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", { + event: message.event, + body: message }); - 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) + 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 (err) { - 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); + } 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 { + const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, { + jobid: payload.externalId + }); + 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); + + + + if (jobs_by_pk?.bodyshop?.uselocalmediaserver) { + //LMS not yet implemented. + + } else { + //S3 Upload + let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`; + + 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: payload.externalId, + documentId: payload.id + }); + } else { + logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", { + jobid: payload.externalId, + 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: "patrick@imex.dev", //TODO: Figure out the hardcoded bypass. + 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: "patrick@imex.dev", //TODO: Figure out the hard coded bypass. + key, + type: "application/pdf", + extension: "pdf", + bodyshopid: jobs_by_pk.bodyshop.id, + size: buffer.length, //Leftover from Cloudinary. We don't do any optimization on upload, so it will always be file.size. + 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: { @@ -147,4 +354,35 @@ module.exports = { // }, // createdAt: "2026-01-30T18:29:18.504Z", // webhookEndpoint: "https://dev.patrickfic.com/esign/webhook", -// } \ No newline at end of file +// } + +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%����\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 \n/ModDate (D:20260227231057Z)…�5[�>�Wu7��V�����Pw�WX�ܮJ'6NWg�vYϳ�����Щr�\n\t+�1��m{휑 �hwb���8��q y�1e�)۱�5m����MVM!�m�[A���{l��\t�hia4��Tm��8��a�e�}� ߫���]MVpяG��֏�jJ<"�A�mO*�P� ������ѧЛ\nendstream\nendobj\n26 0 obj\n<<\n/Length 478/Filter /FlateDecode\n>>\nstream\nx�MSK�9��)��*�O�i��,��o ��kS%�$��hR\rS'�I��~��������T[/�{�k�FC#��֛���;Ӏ�[�⫀m�|Q��\x1b��>� R�����a�E#�pI��._H�ᆫt�k�D3p�I�����W2���oJ0�j���j#��!�$��-������.Ϋ���TI|8D�H��Y��x����1�73%�u�T��Ӑ.rcb�x��Dd6=��Oڏ1 ^�-�...and 252354 more chars` \ No newline at end of file diff --git a/server/esign/webhook.types.ts b/server/esign/webhook.types.ts new file mode 100644 index 000000000..d82e74061 --- /dev/null +++ b/server/esign/webhook.types.ts @@ -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; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 40e99d174..ebefcf270 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2251,18 +2251,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 }` - : "" + : "" } }`; @@ -3248,3 +3246,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 + } +} +` \ No newline at end of file diff --git a/server/media/imgproxy-media.js b/server/media/imgproxy-media.js index b6f480e5b..34d609571 100644 --- a/server/media/imgproxy-media.js +++ b/server/media/imgproxy-media.js @@ -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, diff --git a/server/routes/esignRoutes.js b/server/routes/esignRoutes.js index 2541d658b..264979ba3 100644 --- a/server/routes/esignRoutes.js +++ b/server/routes/esignRoutes.js @@ -8,9 +8,9 @@ const { esignWebhook } = require("../esign/webhook"); //router.use(validateFirebaseIdTokenMiddleware); -router.post("/new", withUserGraphQLClientMiddleware, newEsignDocument); -router.post("/distribute", withUserGraphQLClientMiddleware, distributeDocument); -router.post("/webhook", withUserGraphQLClientMiddleware, esignWebhook); +router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument); +router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument); +router.post("/webhook", esignWebhook); module.exports = router;