From e25174ff97a5230729eb25a5452bafa1c57b0cd4 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 27 Feb 2026 13:15:10 -0800 Subject: [PATCH 1/5] IO-2433 Basic embedded authoring. --- client/package-lock.json | 11 + client/package.json | 1 + client/src/App/App.styles.scss | 6 + .../esignature-modal.container.jsx | 70 ++ .../print-center-item.component.jsx | 49 +- .../pages/manage/manage.page.component.jsx | 10 +- client/src/redux/modals/modals.reducer.js | 3 +- client/src/redux/modals/modals.selectors.js | 1 + package-lock.json | 730 ++++++++++++++++++ package.json | 2 + server.js | 1 + server/esign/esign-new.js | 236 ++++++ server/esign/webhook.js | 150 ++++ server/routes/esignRoutes.js | 16 + server/utils/utils.js | 5 +- 15 files changed, 1284 insertions(+), 7 deletions(-) create mode 100644 client/src/components/esignature-modal/esignature-modal.container.jsx create mode 100644 server/esign/esign-new.js create mode 100644 server/esign/webhook.js create mode 100644 server/routes/esignRoutes.js diff --git a/client/package-lock.json b/client/package-lock.json index 225d2ba06..007edc5ac 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@documenso/embed-react": "^0.5.1", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.0.1", "@firebase/analytics": "^0.10.19", @@ -2564,6 +2565,16 @@ "react": ">=16.8.0" } }, + "node_modules/@documenso/embed-react": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@documenso/embed-react/-/embed-react-0.5.1.tgz", + "integrity": "sha512-PlkZ3vrdZVBTc0J3xfG2wtPVGmxCxWgpQ/SsdR2oBMdTwsR+rDbj9k+CeTv+M9Xi5tKbLr5Y78bS9Sb8K+ltTQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz", diff --git a/client/package.json b/client/package.json index b0cbdf6b8..95ac7a96a 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@documenso/embed-react": "^0.5.1", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.0.1", "@firebase/analytics": "^0.10.19", diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 657520e49..1701289af 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -475,3 +475,9 @@ margin-left: auto; flex: 0 0 auto; } + + +.esignature-embed { +width: 100%; +height: 100%; +} \ No newline at end of file diff --git a/client/src/components/esignature-modal/esignature-modal.container.jsx b/client/src/components/esignature-modal/esignature-modal.container.jsx new file mode 100644 index 000000000..847efa1e2 --- /dev/null +++ b/client/src/components/esignature-modal/esignature-modal.container.jsx @@ -0,0 +1,70 @@ +import { Button, Modal } from "antd"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectEsignature } from "../../redux/modals/modals.selectors"; +import { EmbedUpdateDocumentV1 } from "@documenso/embed-react"; +import axios from "axios"; + +const mapStateToProps = createStructuredSelector({ + esignatureModal: selectEsignature +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleModalVisible: () => dispatch(toggleModalVisible("esignature")) +}); + +export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }) { + const { t } = useTranslation(); + const { open, context } = esignatureModal; + const { token, envelopeId, documentId } = context; + + return ( + { + toggleModalVisible(); + }} + onCancel={() => { + toggleModalVisible(); + }} + cancelButtonProps={{ style: { display: "none" } }} + width="90%" + destroyOnHidden + > +
+ {token ? ( + { + console.log("Document updated:", data.documentId); + }} + /> + ) : ( +
No token...
+ )} + +
+
+ ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(EsignatureModalContainer); 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 7af2b15f1..e325e1daf 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 @@ -1,4 +1,4 @@ -import { MailOutlined, PrinterOutlined } from "@ant-design/icons"; +import { MailOutlined, PrinterOutlined, SignatureFilled } from "@ant-design/icons"; import { Space, Spin } from "antd"; import { useState } from "react"; import { connect } from "react-redux"; @@ -10,6 +10,8 @@ import { GenerateDocument } from "../../utils/RenderTemplate"; import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import axios from "axios"; +import { setModalContext } from "../../redux/modals/modals.actions.js"; const mapStateToProps = createStructuredSelector({ printCenterModal: selectPrintCenter, @@ -17,9 +19,25 @@ const mapStateToProps = createStructuredSelector({ technician: selectTechnician }); -const mapDispatchToProps = () => ({}); +const mapDispatchToProps = (dispatch) => ({ + setEsignatureContext: (context) => + dispatch( + setModalContext({ + context: context, + modal: "esignature" + }) + ) +}); -export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, disabled, technician }) { +export function PrintCenterItemComponent({ + printCenterModal, + setEsignatureContext, + item, + id, + bodyshop, + disabled, + technician +}) { const [loading, setLoading] = useState(false); const { context } = printCenterModal; const notification = useNotification(); @@ -39,6 +57,30 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, setLoading(false); }; + const esignatureGenerate = async () => { + setLoading(true); + try { + const { + data: { token, documentId, evnelopeId } + } = await axios.post("/esign/new", { + name: item.key, + variables: { id: id }, + context, + bodyshop, + templateObject: { + name: item.key, + variables: { id: id } + } + }); + + setEsignatureContext({ context: { token, documentId, evnelopeId }, jobid: id }); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + if ( disabled || (item.featureNameRestricted && !HasFeatureAccess({ featureName: item.featureNameRestricted, bodyshop })) @@ -54,6 +96,7 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
  • {item.title} + {!technician ? ( import("../../components/print-center-modal/print-center-modal.container") @@ -68,7 +69,9 @@ const FeatureRequestPage = lazyDev(() => import("../feature-request/feature-requ const JobCostingModal = lazyDev(() => import("../../components/job-costing-modal/job-costing-modal.container")); const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container")); const BillEnterModalContainer = lazyDev(() => import("../../components/bill-enter-modal/bill-enter-modal.container")); -const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container")); +const TimeTicketModalContainer = lazyDev( + () => import("../../components/time-ticket-modal/time-ticket-modal.container") +); const TimeTicketModalTask = lazyDev( () => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container") ); @@ -110,7 +113,9 @@ const TtApprovals = lazyDev(() => import("../tt-approvals/tt-approvals.page.cont const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.jsx")); const AllTasksPage = lazyDev(() => import("../tasks/allTasksPageContainer.jsx")); -const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container")); +const TaskUpsertModalContainer = lazyDev( + () => import("../../components/task-upsert-modal/task-upsert-modal.container") +); const { Content } = Layout; const mapStateToProps = createStructuredSelector({ @@ -178,6 +183,7 @@ export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, cu + diff --git a/client/src/redux/modals/modals.reducer.js b/client/src/redux/modals/modals.reducer.js index 0035ba17b..b7e67823f 100644 --- a/client/src/redux/modals/modals.reducer.js +++ b/client/src/redux/modals/modals.reducer.js @@ -27,7 +27,8 @@ const INITIAL_STATE = { contractFinder: { ...baseModal }, inventoryUpsert: { ...baseModal }, ca_bc_eftTableConvert: { ...baseModal }, - cardPayment: { ...baseModal } + cardPayment: { ...baseModal }, + esignature: { ...baseModal } }; const modalsReducer = (state = INITIAL_STATE, action) => { diff --git a/client/src/redux/modals/modals.selectors.js b/client/src/redux/modals/modals.selectors.js index cd836a3d1..ad796f8da 100644 --- a/client/src/redux/modals/modals.selectors.js +++ b/client/src/redux/modals/modals.selectors.js @@ -36,3 +36,4 @@ export const selectInventoryUpsert = createSelector([selectModals], (modals) => export const selectCaBcEtfTableConvert = createSelector([selectModals], (modals) => modals.ca_bc_eftTableConvert); export const selectCardPayment = createSelector([selectModals], (modals) => modals.cardPayment); +export const selectEsignature = createSelector([selectModals], (modals) => modals.esignature); diff --git a/package-lock.json b/package-lock.json index a72ac3df6..0b3e1381f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@aws-sdk/credential-provider-node": "^3.972.3", "@aws-sdk/lib-storage": "^3.978.0", "@aws-sdk/s3-request-presigner": "^3.978.0", + "@documenso/sdk-typescript": "^0.8.0", + "@jsreport/nodejs-client": "^4.1.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -1400,6 +1402,18 @@ "kuler": "^2.0.0" } }, + "node_modules/@documenso/sdk-typescript": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@documenso/sdk-typescript/-/sdk-typescript-0.8.0.tgz", + "integrity": "sha512-Emzd5j+v8tA8gxtL+M/svVuzSOKMZw3/U4bS8zRoagvQEqkt+XNU2JraPEAJzxTjf3ww6EnlURXydbglBmR7AQ==", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "bin": { + "mcp": "bin/mcp-server.js" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -2268,6 +2282,18 @@ "node": ">=6" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2396,12 +2422,364 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@jsreport/nodejs-client": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@jsreport/nodejs-client/-/nodejs-client-4.1.0.tgz", + "integrity": "sha512-QWupUQzMzxWFvY+AlSdUZGlinJv4cKhYmVE9rIe+he7rn4B24tezFmNdnrDcTSFv3hj4x7sTNqpeHT0fItfs5Q==", + "dependencies": { + "axios": "1.13.2", + "concat-stream": "2.0.0", + "mimic-response": "2.1.0" + }, + "engines": { + "node": ">=22.18" + } + }, + "node_modules/@jsreport/nodejs-client/node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -4270,6 +4648,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -4728,6 +5145,100 @@ "node": "*" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6441,6 +6952,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6497,6 +7029,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -6619,6 +7169,22 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", @@ -7374,6 +7940,15 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hpagent": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", @@ -7632,6 +8207,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7873,6 +8457,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8146,6 +8736,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -8645,6 +9241,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -9358,6 +9966,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9590,6 +10207,66 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9755,6 +10432,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -9926,6 +10612,32 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rsa-pem-from-mod-exp": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz", @@ -12196,6 +12908,24 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 8f7c36816..ff7943be7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@aws-sdk/credential-provider-node": "^3.972.3", "@aws-sdk/lib-storage": "^3.978.0", "@aws-sdk/s3-request-presigner": "^3.978.0", + "@documenso/sdk-typescript": "^0.8.0", + "@jsreport/nodejs-client": "^4.1.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", diff --git a/server.js b/server.js index 33cbe014c..b73100863 100644 --- a/server.js +++ b/server.js @@ -128,6 +128,7 @@ const applyRoutes = ({ app }) => { app.use("/sso", require("./server/routes/ssoRoutes")); app.use("/integrations", require("./server/routes/intergrationRoutes")); app.use("/chatter", require("./server/routes/chatterRoutes")); + app.use("/esign", require("./server/routes/eSignRoutes")); // Default route for forbidden access app.get("/", (req, res) => { diff --git a/server/esign/esign-new.js b/server/esign/esign-new.js new file mode 100644 index 000000000..387d49f8a --- /dev/null +++ b/server/esign/esign-new.js @@ -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 +} diff --git a/server/esign/webhook.js b/server/esign/webhook.js new file mode 100644 index 000000000..dc3adefd2 --- /dev/null +++ b/server/esign/webhook.js @@ -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", +// } \ No newline at end of file diff --git a/server/routes/esignRoutes.js b/server/routes/esignRoutes.js new file mode 100644 index 000000000..2541d658b --- /dev/null +++ b/server/routes/esignRoutes.js @@ -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; diff --git a/server/utils/utils.js b/server/utils/utils.js index 7b7e61fc2..bd78094b3 100644 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -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()); }; From 52f43a600cd484849a426ac6edd30f2de605efdc Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 27 Feb 2026 15:44:23 -0800 Subject: [PATCH 2/5] 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; From 51fba24a3d44261180f16fdf52cd7e294641177b Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 27 Feb 2026 16:03:27 -0800 Subject: [PATCH 3/5] IO-2433 Delete on cancel, improved styling. --- client/src/App/App.styles.scss | 1 + .../esignature-modal.container.jsx | 57 ++++++++++--------- server/esign/esign-new.js | 24 +++++++- server/esign/webhook.js | 21 ++++--- server/routes/esignRoutes.js | 3 +- 5 files changed, 66 insertions(+), 40 deletions(-) diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 1701289af..7f2e670e3 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -480,4 +480,5 @@ .esignature-embed { width: 100%; height: 100%; +border-width: 0; } \ No newline at end of file diff --git a/client/src/components/esignature-modal/esignature-modal.container.jsx b/client/src/components/esignature-modal/esignature-modal.container.jsx index 0c6553d88..be82394e0 100644 --- a/client/src/components/esignature-modal/esignature-modal.container.jsx +++ b/client/src/components/esignature-modal/esignature-modal.container.jsx @@ -1,11 +1,11 @@ -import { Button, Modal } from "antd"; +import { EmbedUpdateDocumentV1 } from "@documenso/embed-react"; +import { Modal } from "antd"; +import axios from "axios"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; 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({ @@ -26,23 +26,42 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, { - toggleModalVisible(); + onOk={async () => { + try { + const distResult = await axios.post("/esign/distribute", { + documentId, + envelopeId, + jobid, + bodyshopid: bodyshop.id + }); + console.log("Distribution result:", distResult); + toggleModalVisible(); + } catch (error) { + console.error("Error distributing document:", error); + } }} - onCancel={() => { - toggleModalVisible(); + onCancel={async () => { + try { + const cancelResult = await axios.post("/esign/delete", { + documentId, + envelopeId + }); + console.log("Cancel result:", cancelResult); + toggleModalVisible(); + } catch (error) { + console.error("Error cancelling document:", error); + } }} - cancelButtonProps={{ style: { display: "none" } }} + okButtonProps={{ title: "Distribute by Email" }} width="90%" destroyOnHidden > -
    +
    {token ? ( { console.log("Document updated:", data.documentId); @@ -51,24 +70,6 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, ) : (
    No token...
    )} -
    ); diff --git a/server/esign/esign-new.js b/server/esign/esign-new.js index f8900bec9..5c976e4de 100644 --- a/server/esign/esign-new.js +++ b/server/esign/esign-new.js @@ -43,8 +43,25 @@ async function distributeDocument(req, res) { } } -async function newEsignDocument(req, res) { +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 @@ -58,7 +75,7 @@ async function newEsignDocument(req, res) { const createDocumentResponse = await documenso.documents.create({ payload: { title: esigData?.title, - externalId: req.body.jobid, + 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, @@ -264,7 +281,8 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => { module.exports = { newEsignDocument, - distributeDocument + distributeDocument, + deleteDocument } diff --git a/server/esign/webhook.js b/server/esign/webhook.js index b863522f2..3c83a07ae 100644 --- a/server/esign/webhook.js +++ b/server/esign/webhook.js @@ -85,8 +85,14 @@ 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: payload.externalId + jobid }); const document = await documenso.document.documentDownload({ documentId: payload.id, @@ -97,25 +103,24 @@ async function handleDocumentCompleted(payload = sampleComplete) { 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 - 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, + jobid: jobid, documentId: payload.id }); } else { logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", { - jobid: payload.externalId, + jobid: jobid, documentId: payload.id, s3Key: key, bucket: uploadResult.bucket @@ -125,7 +130,7 @@ async function handleDocumentCompleted(payload = sampleComplete) { 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. + useremail: uploaded_by, type: 'esig-complete' } }) @@ -133,12 +138,12 @@ async function handleDocumentCompleted(payload = sampleComplete) { await client.request(INSERT_ESIGNATURE_DOCUMENT, { docInput: { jobid: jobs_by_pk.id, - uploaded_by: "patrick@imex.dev", //TODO: Figure out the hard coded bypass. + uploaded_by: uploaded_by, 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. + size: buffer.length, takenat: new Date().toISOString(), } }) diff --git a/server/routes/esignRoutes.js b/server/routes/esignRoutes.js index 264979ba3..059f49666 100644 --- a/server/routes/esignRoutes.js +++ b/server/routes/esignRoutes.js @@ -3,13 +3,14 @@ const router = express.Router(); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); -const { newEsignDocument, distributeDocument } = require("../esign/esign-new"); +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); From 97d8047a3d7940b3c85f2940268ff722039f5a55 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 5 Mar 2026 15:56:13 -0800 Subject: [PATCH 4/5] Update casing for esign route. --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index b28225a55..82f2928e1 100644 --- a/server.js +++ b/server.js @@ -130,7 +130,7 @@ const applyRoutes = ({ app }) => { app.use("/ai", require("./server/routes/aiRoutes")); app.use("/chatter", require("./server/routes/chatterRoutes")); - app.use("/esign", require("./server/routes/eSignRoutes")); + app.use("/esign", require("./server/routes/esignRoutes")); // Default route for forbidden access app.get("/", (req, res) => { From 6ef56f97c027663451e8287e6ee909e9d3b9ecc4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 12 Mar 2026 16:58:59 -0700 Subject: [PATCH 5/5] IO-2433 Missing Translation Signed-off-by: Allan Carr --- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 3 files changed, 3 insertions(+) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 02e952d61..371ab7ab6 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2186,6 +2186,7 @@ "duplicateconfirm": "Are you sure you want to duplicate this Job? Some elements of this Job will not be duplicated.", "emailaudit": "Email Audit Trail", "employeeassignments": "Employee Assignments", + "esignature": "E-Signature", "estimatelines": "Estimate Lines", "estimator": "Estimator", "existing_jobs": "Existing Jobs", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f10585f6f..957b73eba 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2186,6 +2186,7 @@ "duplicateconfirm": "", "emailaudit": "", "employeeassignments": "", + "esignature": "", "estimatelines": "", "estimator": "", "existing_jobs": "Empleos existentes", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 4f7edc58d..0e9cc4649 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2186,6 +2186,7 @@ "duplicateconfirm": "", "emailaudit": "", "employeeassignments": "", + "esignature": "", "estimatelines": "", "estimator": "", "existing_jobs": "Emplois existants",