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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user