✨🚧 signDocument using node signpdf and custom placeholder insert
This commit is contained in:
@@ -12,6 +12,7 @@ const withTM = require("next-transpile-modules")([
|
|||||||
"@documenso/ui",
|
"@documenso/ui",
|
||||||
"@documenso/pdf",
|
"@documenso/pdf",
|
||||||
"@documenso/features",
|
"@documenso/features",
|
||||||
|
"@documenso/signing",
|
||||||
"react-signature-canvas",
|
"react-signature-canvas",
|
||||||
]);
|
]);
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
|
|||||||
@@ -32,9 +32,12 @@
|
|||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.18.3",
|
"next-auth": "^4.18.3",
|
||||||
"next-transpile-modules": "^10.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"node-signpdf": "^1.5.0",
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.0",
|
||||||
"nodemailer-sendgrid": "^1.0.3",
|
"nodemailer-sendgrid": "^1.0.3",
|
||||||
"npm": "^9.1.3",
|
"npm": "^9.1.3",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"placeholder-loading": "^0.6.0",
|
"placeholder-loading": "^0.6.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import prisma from "@documenso/prisma";
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { signDocument } from "@documenso/signing/signDocument";
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
@@ -24,7 +25,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!document)
|
if (!document)
|
||||||
res.status(404).end(`No document with id ${documentId} found.`);
|
res.status(404).end(`No document with id ${documentId} found.`);
|
||||||
|
|
||||||
const buffer: Buffer = Buffer.from(document.document.toString(), "base64");
|
const signedDocumentAsBase64 = await signDocument(
|
||||||
|
document.document.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
|
|||||||
30
apps/web/pages/api/test-sign/[id].ts
Normal file
30
apps/web/pages/api/test-sign/[id].ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
defaultHandler,
|
||||||
|
defaultResponder,
|
||||||
|
getUserFromToken,
|
||||||
|
} from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { signDocument } from "@documenso/signing/signDocument";
|
||||||
|
|
||||||
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const documentId = req.query.id || 1;
|
||||||
|
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||||
|
const signedDocumentAsBase64 = await signDocument(document.document.toString());
|
||||||
|
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||||
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename=${document.title}`
|
||||||
|
);
|
||||||
|
res.setHeader("Content-Length", buffer.length);
|
||||||
|
|
||||||
|
res.status(200).send(buffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||||
|
});
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -68,9 +68,12 @@
|
|||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.18.3",
|
"next-auth": "^4.18.3",
|
||||||
"next-transpile-modules": "^10.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"node-signpdf": "^1.5.0",
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.0",
|
||||||
"nodemailer-sendgrid": "^1.0.3",
|
"nodemailer-sendgrid": "^1.0.3",
|
||||||
"npm": "^9.1.3",
|
"npm": "^9.1.3",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"placeholder-loading": "^0.6.0",
|
"placeholder-loading": "^0.6.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
@@ -3959,11 +3962,30 @@
|
|||||||
"enhanced-resolve": "^5.10.0"
|
"enhanced-resolve": "^5.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-forge": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.8",
|
"version": "2.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
|
||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/node-signpdf": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zDOduHZGadYUpCGd1JUtOHgVn6QTW6t1feMKNe3NLexsGQXDWFjOr1mGTjqTq1UWXAp47T4ShllLnmhGu9GQsA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"node-forge": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "6.9.0",
|
"version": "6.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
|
||||||
@@ -8659,9 +8681,12 @@
|
|||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.18.3",
|
"next-auth": "^4.18.3",
|
||||||
"next-transpile-modules": "^10.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"node-signpdf": "^1.5.0",
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.0",
|
||||||
"nodemailer-sendgrid": "^1.0.3",
|
"nodemailer-sendgrid": "^1.0.3",
|
||||||
"npm": "^9.1.3",
|
"npm": "^9.1.3",
|
||||||
|
"pdf-lib": "*",
|
||||||
"placeholder-loading": "^0.6.0",
|
"placeholder-loading": "^0.6.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@@ -11461,11 +11486,22 @@
|
|||||||
"enhanced-resolve": "^5.10.0"
|
"enhanced-resolve": "^5.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node-forge": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
|
||||||
|
},
|
||||||
"node-releases": {
|
"node-releases": {
|
||||||
"version": "2.0.8",
|
"version": "2.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
|
||||||
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
|
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
|
||||||
},
|
},
|
||||||
|
"node-signpdf": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zDOduHZGadYUpCGd1JUtOHgVn6QTW6t1feMKNe3NLexsGQXDWFjOr1mGTjqTq1UWXAp47T4ShllLnmhGu9GQsA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"nodemailer": {
|
"nodemailer": {
|
||||||
"version": "6.9.0",
|
"version": "6.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
|
||||||
|
|||||||
55
packages/signing/PDFArrayCustom.js
Normal file
55
packages/signing/PDFArrayCustom.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const { PDFArray, CharCodes } = require("pdf-lib");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends PDFArray class in order to make ByteRange look like this:
|
||||||
|
* /ByteRange [0 /********** /********** /**********]
|
||||||
|
* Not this:
|
||||||
|
* /ByteRange [ 0 /********** /********** /********** ]
|
||||||
|
*/
|
||||||
|
class PDFArrayCustom extends PDFArray {
|
||||||
|
static withContext(context) {
|
||||||
|
return new PDFArrayCustom(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(context) {
|
||||||
|
const clone = PDFArrayCustom.withContext(context || this.context);
|
||||||
|
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||||
|
clone.push(this.array[idx]);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let arrayString = "[";
|
||||||
|
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||||
|
arrayString += this.get(idx).toString();
|
||||||
|
if (idx < len - 1) arrayString += " ";
|
||||||
|
}
|
||||||
|
arrayString += "]";
|
||||||
|
return arrayString;
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeInBytes() {
|
||||||
|
let size = 2;
|
||||||
|
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||||
|
size += this.get(idx).sizeInBytes();
|
||||||
|
if (idx < len - 1) size += 1;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBytesInto(buffer, offset) {
|
||||||
|
const initialOffset = offset;
|
||||||
|
|
||||||
|
buffer[offset++] = CharCodes.LeftSquareBracket;
|
||||||
|
for (let idx = 0, len = this.size(); idx < len; idx++) {
|
||||||
|
offset += this.get(idx).copyBytesInto(buffer, offset);
|
||||||
|
if (idx < len - 1) buffer[offset++] = CharCodes.Space;
|
||||||
|
}
|
||||||
|
buffer[offset++] = CharCodes.RightSquareBracket;
|
||||||
|
|
||||||
|
return offset - initialOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PDFArrayCustom;
|
||||||
@@ -1,26 +1,78 @@
|
|||||||
const signer = require("../../node_modules/node-signpdf/dist/signpdf");
|
const fs = require("fs");
|
||||||
const {
|
const signer = require("./node-signpdf/dist/signpdf");
|
||||||
pdfkitAddPlaceholder,
|
import {
|
||||||
} = require("../../node_modules/node-signpdf/dist/helpers/pdfkitAddPlaceholder");
|
PDFDocument,
|
||||||
import * as fs from "fs";
|
PDFName,
|
||||||
|
PDFNumber,
|
||||||
|
PDFHexString,
|
||||||
|
PDFString,
|
||||||
|
} from "pdf-lib";
|
||||||
|
|
||||||
export const signDocument = (documentAsBase64: string): any => {
|
export const signDocument = async (documentAsBase64: string): Promise<any> => {
|
||||||
|
// Custom code to add Byterange to PDF
|
||||||
|
const PDFArrayCustom = require("./PDFArrayCustom");
|
||||||
|
|
||||||
|
// The PDF we're going to sign
|
||||||
const pdfBuffer = Buffer.from(documentAsBase64, "base64");
|
const pdfBuffer = Buffer.from(documentAsBase64, "base64");
|
||||||
const certBuffer = fs.readFileSync("public/certificate.p12");
|
|
||||||
|
|
||||||
console.log("adding placeholder..");
|
// The p12 certificate we're going to sign with
|
||||||
console.log(signer.pdfkitAddPlaceholder);
|
const p12Buffer = fs.readFileSync("ressources/certificate.p12");
|
||||||
const inputBuffer = signer.pdfkitAddPlaceholder({
|
|
||||||
pdfBuffer,
|
const SIGNATURE_LENGTH = 4540;
|
||||||
reason: "Signed Certificate.",
|
|
||||||
contactInfo: "sign@example.com",
|
const pdfDoc = await PDFDocument.load(pdfBuffer);
|
||||||
name: "Example",
|
const pages = pdfDoc.getPages();
|
||||||
location: "Jakarta",
|
|
||||||
signatureLength: certBuffer.length,
|
const ByteRange = PDFArrayCustom.withContext(pdfDoc.context);
|
||||||
|
ByteRange.push(PDFNumber.of(0));
|
||||||
|
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
|
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
|
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
|
|
||||||
|
const signatureDict = pdfDoc.context.obj({
|
||||||
|
Type: "Sig",
|
||||||
|
Filter: "Adobe.PPKLite",
|
||||||
|
SubFilter: "adbe.pkcs7.detached",
|
||||||
|
ByteRange,
|
||||||
|
Contents: PDFHexString.of("A".repeat(SIGNATURE_LENGTH)),
|
||||||
|
Reason: PDFString.of("Signed by Documenso"),
|
||||||
|
M: PDFString.fromDate(new Date()),
|
||||||
|
});
|
||||||
|
const signatureDictRef = pdfDoc.context.register(signatureDict);
|
||||||
|
|
||||||
|
const widgetDict = pdfDoc.context.obj({
|
||||||
|
Type: "Annot",
|
||||||
|
Subtype: "Widget",
|
||||||
|
FT: "Sig",
|
||||||
|
Rect: [0, 0, 0, 0],
|
||||||
|
V: signatureDictRef,
|
||||||
|
T: PDFString.of("Signature1"),
|
||||||
|
F: 4,
|
||||||
|
P: pages[0].ref,
|
||||||
|
});
|
||||||
|
const widgetDictRef = pdfDoc.context.register(widgetDict);
|
||||||
|
|
||||||
|
// Add our signature widget to the first page
|
||||||
|
pages[0].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef]));
|
||||||
|
|
||||||
|
// Create an AcroForm object containing our signature widget
|
||||||
|
pdfDoc.catalog.set(
|
||||||
|
PDFName.of("AcroForm"),
|
||||||
|
pdfDoc.context.obj({
|
||||||
|
SigFlags: 3,
|
||||||
|
Fields: [widgetDictRef],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
|
||||||
|
const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes);
|
||||||
|
|
||||||
|
const signObj = new signer.SignPdf();
|
||||||
|
const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer, {
|
||||||
|
passphrase: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("signing..");
|
// Write the signed file
|
||||||
const signedPdf = new signer.SignPdf().sign(inputBuffer, certBuffer);
|
// fs.writeFileSync("./signed.pdf", signedPdfBuffer);
|
||||||
|
return signedPdfBuffer;
|
||||||
return signedPdf;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user