IO-2433 Basic embedded authoring.

This commit is contained in:
Patrick Fic
2026-02-27 13:15:10 -08:00
parent e03546d989
commit e25174ff97
15 changed files with 1284 additions and 7 deletions

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

@@ -0,0 +1,236 @@
const { Documenso } = require("@documenso/sdk-typescript");
const axios = require("axios");
const { jsrAuthString } = require("../utils/utils");
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 jsreport = require("@jsreport/nodejs-client");
async function distributeDocument(req, res) {
try {
const { documentId } = req.body;
const distributeResult = await documenso.documents.distribute({
documentId,
});
res.json({ success: true, distributeResult });
} catch (error) {
console.error("Error distributing document:", error?.data);
res.status(500).json({ error: "An error occurred while distributing the document." });
}
}
async function newEsignDocument(req, res) {
try {
const client = req.userGraphQLClient;
const { pdf: fileBuffer, esigFields } = await RenderTemplate({ client, req })
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
const createDocumentResponse = await documenso.documents.create({
payload: {
title: `Repair Authorization - ${new Date().toLocaleString()}`,
recipients: [
{
email: "patrick.fic@convenient-brands.com",
name: "Customer Fullname",
role: "SIGNER",
}
],
meta: {
timezone: "America/Vancouver",
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. ",
}
},
file: fileBlob
});
const documentResult = await documenso.documents.get({
documentId: createDocumentResponse.id,
});
if (esigFields && esigFields.length > 0) {
console.log("Adding placeholder fields.")
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({
envelopeId: createDocumentResponse.envelopeId,
data: esigFields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
});
} catch (error) {
console.log("Error adding placeholders", JSON.stringify(error, null, 2));
}
}
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
res.json({ token: presignToken.token, documentId: createDocumentResponse.id, envelopeId: createDocumentResponse.envelopeId });
}
catch (error) {
console.error("Error in newEsignDocument:", error);
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, 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 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, esigFields }
}
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`, {
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'`,
{ headers: { Authorization: jsrAuth } }
);
const jsReportEsig = await axios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
{ headers: { Authorization: jsrAuth } }
);
let templateQueryToExecute;
let esigFields;
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) {
esigFields = (atob(shopSpecificEsig.content));
}
}
if (!templateQueryToExecute) {
const generalTemplate = jsReportQueries.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
templateQueryToExecute = atob(generalTemplate.content);
}
if (!esigFields) {
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate && generalTemplate.content) {
esigFields = 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;
// 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;
}
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.
};
// }
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
};
module.exports = {
newEsignDocument,
distributeDocument
}

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

@@ -0,0 +1,150 @@
const { Documenso } = require("@documenso/sdk-typescript");
const fs = require("fs");
const path = require("path");
const documenso = new Documenso({
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
serverURL: "https://stg-app.documenso.com/api/v2",
});
async function esignWebhook(req, res) {
console.log("Esign Webhook Received:", req.body);
try {
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);
res.sendStatus(500)
}
}
module.exports = {
esignWebhook
}
// 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",
// }

View File

@@ -0,0 +1,16 @@
const express = require("express");
const router = express.Router();
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
const { newEsignDocument, distributeDocument } = require("../esign/esign-new");
const { esignWebhook } = require("../esign/webhook");
//router.use(validateFirebaseIdTokenMiddleware);
router.post("/new", withUserGraphQLClientMiddleware, newEsignDocument);
router.post("/distribute", withUserGraphQLClientMiddleware, distributeDocument);
router.post("/webhook", withUserGraphQLClientMiddleware, 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());
};