feat(10-02): fix POST handler — signable field filter and date stamping at sign time

- Add getFieldType to schema import
- Add PDFDocument, StandardFonts, rgb from @cantoo/pdf-lib for date stamping
- Add readFile, writeFile, unlink from node:fs/promises
- Hoist const now = new Date() to before step 8 (shared for date stamp + DB update)
- Step 8a: stamp signing date onto date fields in prepared PDF before embed
- Step 8b: filter signableFields to client-signature and initials only
- signaturesWithCoords now maps only signable fields (no 500 on text/checkbox/date)
- Update embedSignatureInPdf call to use dateStampedPath
- Fire-and-forget cleanup of temporary .datestamped.tmp file after embed
This commit is contained in:
Chandler Copeland
2026-03-21 12:50:21 -06:00
parent 1e92ca363a
commit d395d85ebb

View File

@@ -3,9 +3,11 @@ import { NextRequest, NextResponse } from 'next/server';
import { verifySigningToken } from '@/lib/signing/token';
import { logAuditEvent } from '@/lib/signing/audit';
import { db } from '@/lib/db';
import { signingTokens, documents, clients, isClientVisibleField } from '@/lib/db/schema';
import { signingTokens, documents, clients, isClientVisibleField, getFieldType } from '@/lib/db/schema';
import { eq, isNull, and } from 'drizzle-orm';
import path from 'node:path';
import { readFile, writeFile, unlink } from 'node:fs/promises';
import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib';
import { embedSignatureInPdf } from '@/lib/signing/embed-signature';
import { sendAgentNotificationEmail } from '@/lib/signing/signing-mailer';
@@ -168,9 +170,54 @@ export async function POST(
return NextResponse.json({ error: 'forbidden' }, { status: 403 });
}
// 8. Merge client-supplied dataURLs with server-stored field coordinates
// Capture signing timestamp here — reused for date stamping (8a) and DB update (11)
const now = new Date();
// 8a. Stamp date text at each 'date' field coordinate (signing date = now, captured server-side)
const dateFields = (doc.signatureFields ?? []).filter(
(f) => getFieldType(f) === 'date'
);
// Only load and modify PDF if there are date fields to stamp
let dateStampedPath = preparedAbsPath;
if (dateFields.length > 0) {
const pdfBytes = await readFile(preparedAbsPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const signingDateStr = now.toLocaleDateString('en-US', {
month: '2-digit', day: '2-digit', year: 'numeric',
});
for (const field of dateFields) {
const page = pages[field.page - 1];
if (!page) continue;
// Overwrite the amber placeholder rectangle with white background + date text
page.drawRectangle({
x: field.x, y: field.y, width: field.width, height: field.height,
borderColor: rgb(0.39, 0.45, 0.55), borderWidth: 0.5,
color: rgb(1.0, 1.0, 1.0),
});
page.drawText(signingDateStr, {
x: field.x + 4,
y: field.y + field.height / 2 - 4, // vertically center
size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.55),
});
}
const stampedBytes = await pdfDoc.save();
// Write to a temporary date-stamped path; embedSignatureInPdf reads from this path
dateStampedPath = `${preparedAbsPath}.datestamped.tmp`;
await writeFile(dateStampedPath, stampedBytes);
}
// 8b. Build signaturesWithCoords for client-signable fields only (client-signature + initials)
// text/checkbox/date are embedded at prepare time; the client was never shown these as interactive fields
// CRITICAL: x/y/width/height come ONLY from the DB — never from the request body
const signaturesWithCoords = (doc.signatureFields ?? []).map((field) => {
const signableFields = (doc.signatureFields ?? []).filter((f) => {
const t = getFieldType(f);
return t === 'client-signature' || t === 'initials';
});
const signaturesWithCoords = signableFields.map((field) => {
const clientSig = signatures.find((s) => s.fieldId === field.id);
if (!clientSig) {
throw new Error(`Missing signature for field ${field.id}`);
@@ -189,12 +236,17 @@ export async function POST(
// 9. Embed signatures into PDF — returns SHA-256 hex hash
let pdfHash: string;
try {
pdfHash = await embedSignatureInPdf(preparedAbsPath, signedAbsPath, signaturesWithCoords);
pdfHash = await embedSignatureInPdf(dateStampedPath, signedAbsPath, signaturesWithCoords);
} catch (err) {
console.error('[sign/POST] embedSignatureInPdf failed:', err);
return NextResponse.json({ error: 'pdf-embed-failed' }, { status: 500 });
}
// Clean up temporary date-stamped file if it was created
if (dateStampedPath !== preparedAbsPath) {
unlink(dateStampedPath).catch(() => {});
}
// 10. Log pdf_hash_computed audit event
await logAuditEvent({
documentId: payload.documentId,
@@ -205,7 +257,6 @@ export async function POST(
});
// 11. Update documents table: status, signedAt, signedFilePath, pdfHash
const now = new Date();
await db
.update(documents)
.set({