diff --git a/teressa-copeland-homes/src/app/api/sign/[token]/route.ts b/teressa-copeland-homes/src/app/api/sign/[token]/route.ts index cb20348..854a21b 100644 --- a/teressa-copeland-homes/src/app/api/sign/[token]/route.ts +++ b/teressa-copeland-homes/src/app/api/sign/[token]/route.ts @@ -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({