)}
{/* Save for later checkbox (only on draw/type tabs) */}
{tab !== 'saved' && (
)}
);
}
```
**Update SigningPageClient.tsx** — wire the modal:
1. Import SignatureModal
2. Add state: `const [modalOpen, setModalOpen] = useState(false)` and `const [activeFieldId, setActiveFieldId] = useState(null)`
3. Replace the no-op `onFieldClick` stub with: `setActiveFieldId(fieldId); setModalOpen(true)`
4. Add `onConfirm` handler: store `{ fieldId, dataURL }` in a `signedFields` Map state; if all fields signed, enable Submit
5. Render ` setModalOpen(false)} />`
6. Update the field overlay: if `signedFields.has(field.id)`, show a small signature image preview inside the overlay instead of the pulsing outline
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "error|Error" | grep -v "^$" | head -10SignatureModal.tsx exists; SigningPageClient.tsx imports and renders it; npm run build passes with no TypeScript errorsTask 2: POST /api/sign/[token] — atomic submission with PDF embedding + audit trail
teressa-copeland-homes/src/app/api/sign/[token]/route.ts
Add a POST handler to the existing `src/app/api/sign/[token]/route.ts` file (which already has the GET handler from Plan 06-03).
The POST handler implements the signing submission with CRITICAL one-time enforcement:
Request body (JSON): `{ signatures: Array<{ fieldId: string; dataURL: string }> }`
Logic (order is critical):
1. Parse body: `const { signatures } = await req.json()`
2. Resolve token from params, call `verifySigningToken(token)` — on failure return 401
3. **ATOMIC ONE-TIME ENFORCEMENT** (the most critical step):
```sql
UPDATE signing_tokens SET used_at = NOW() WHERE jti = ? AND used_at IS NULL RETURNING jti
```
Use Drizzle: `db.update(signingTokens).set({ usedAt: new Date() }).where(and(eq(signingTokens.jti, payload.jti), isNull(signingTokens.usedAt))).returning()`
If 0 rows returned → return 409 `{ error: 'already-signed' }` — do NOT proceed to embed PDF
4. Log `signature_submitted` audit event (with IP from headers)
5. Fetch document with `signatureFields` and `preparedFilePath`
6. Guard: if `preparedFilePath` is null return 422 (should never happen, but defensive)
7. Build `signedFilePath`: same directory as preparedFilePath but with `_signed.pdf` suffix
```typescript
const signedFilePath = doc.preparedFilePath!.replace(/_prepared\.pdf$/, '_signed.pdf');
```
8. Merge client-supplied signature dataURLs with server-stored field coordinates:
```typescript
const signaturesWithCoords = (doc.signatureFields ?? []).map(field => {
const clientSig = signatures.find(s => s.fieldId === field.id);
if (!clientSig) throw new Error(`Missing signature for field ${field.id}`);
return { fieldId: field.id, dataURL: clientSig.dataURL, x: field.x, y: field.y, width: field.width, height: field.height, page: field.page };
});
```
9. Call `embedSignatureInPdf(doc.preparedFilePath!, signedFilePath, signaturesWithCoords)` — returns SHA-256 hash
10. Log `pdf_hash_computed` audit event with `metadata: { hash, signedFilePath }`
11. Update documents table: `status = 'Signed', signedAt = new Date(), signedFilePath, pdfHash = hash`
12. Fire-and-forget: call `sendAgentNotificationEmail` (catch and log errors but do NOT fail the response)
13. Return 200 JSON: `{ ok: true }` — client redirects to `/sign/${token}/confirmed`
IP/UA extraction:
```typescript
const hdrs = await headers();
const ip = hdrs.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
const ua = hdrs.get('user-agent') ?? 'unknown';
```
Import sendAgentNotificationEmail from signing-mailer.tsx for the agent notification.
NEVER trust coordinates from the client request body — always use `signatureFields` from the DB. Client only provides fieldId and dataURL. Coordinates come from server.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -5POST handler added to sign/[token]/route.ts; npm run build passes; grep confirms UPDATE...WHERE usedAt IS NULL pattern in route.ts; documents columns updated in the handler
- Build passes: `npm run build` exits 0
- Modal exists: `ls teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx`
- Atomic check present: `grep -n "isNull\|usedAt IS NULL\|usedAt.*null" teressa-copeland-homes/src/app/api/sign/*/route.ts`
- Hash logged: `grep -n "pdf_hash_computed\|pdfHash" teressa-copeland-homes/src/app/api/sign/*/route.ts`
- Coordinates from DB only: verify route does NOT read x/y/width/height from request body
Submission flow complete when: modal opens on field click with Draw/Type/Use Saved tabs, signature_pad initializes with devicePixelRatio scaling and touch-none, POST route atomically claims the token (0 rows = 409), embeds all signatures at server-stored coordinates, stores pdfHash, logs all remaining audit events, and npm run build passes.