IO-2433 Basic completion webhook, S3 upload, audit trail.

This commit is contained in:
Patrick Fic
2026-02-27 15:44:23 -08:00
parent e25174ff97
commit 52f43a600c
8 changed files with 559 additions and 86 deletions

View File

@@ -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 (
<Modal
@@ -40,7 +42,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }
presignToken={token}
host="https://stg-app.documenso.com"
documentId={documentId}
externalId="order-12345"
externalId={jobid}
className="esignature-embed"
onDocumentUpdated={(data) => {
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);

View File

@@ -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 {

View File

@@ -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"
// }

View File

@@ -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",
// }
// }
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

@@ -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
}
}
`

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

@@ -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;