feat(12-01): POST /api/documents/[id]/preview route

- Auth-guarded Next.js 15 route handler with async params
- Versioned temp path (_preview_{timestamp}.pdf) — never overwrites _prepared.pdf
- Path traversal guard mirrors prepare route
- Mirrors 422 guards for agent-signature-missing and agent-initials-missing
- try/finally ensures temp file deleted after bytes are read
This commit is contained in:
Chandler Copeland
2026-03-21 15:29:47 -06:00
parent 10d4eb738a
commit 99205bca9f

View File

@@ -0,0 +1,73 @@
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { documents, users, getFieldType } from '@/lib/db/schema';
import type { SignatureFieldData } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { preparePdf } from '@/lib/pdf/prepare-document';
import path from 'node:path';
import { readFile, unlink } from 'node:fs/promises';
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
export async function POST(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
const { id } = await params;
const body = await req.json() as { textFillData?: Record<string, string> };
const doc = await db.query.documents.findFirst({ where: eq(documents.id, id) });
if (!doc) return Response.json({ error: 'Not found' }, { status: 404 });
if (!doc.filePath) return Response.json({ error: 'Document has no PDF file' }, { status: 422 });
const srcPath = path.join(UPLOADS_DIR, doc.filePath);
// Versioned preview path — never overwrites _prepared.pdf
const previewRelPath = doc.filePath.replace(/\.pdf$/, `_preview_${Date.now()}.pdf`);
const previewPath = path.join(UPLOADS_DIR, previewRelPath);
// Path traversal guard
if (!previewPath.startsWith(UPLOADS_DIR)) {
return new Response('Forbidden', { status: 403 });
}
const sigFields = (doc.signatureFields as SignatureFieldData[]) ?? [];
const textFields = body.textFillData ?? {};
// Fetch agent's saved signature and initials
const agentUser = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { agentSignatureData: true, agentInitialsData: true },
});
const agentSignatureData = agentUser?.agentSignatureData ?? null;
const agentInitialsData = agentUser?.agentInitialsData ?? null;
// 422 guard: agent-signature fields present but no signature saved
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
if (hasAgentSigFields && !agentSignatureData) {
return Response.json(
{ error: 'agent-signature-missing', message: 'No agent signature saved.' },
{ status: 422 }
);
}
// 422 guard: agent-initials fields present but no initials saved
const hasAgentInitialsFields = sigFields.some(f => getFieldType(f) === 'agent-initials');
if (hasAgentInitialsFields && !agentInitialsData) {
return Response.json(
{ error: 'agent-initials-missing', message: 'No agent initials saved.' },
{ status: 422 }
);
}
try {
await preparePdf(srcPath, previewPath, textFields, sigFields, agentSignatureData, agentInitialsData);
const pdfBytes = await readFile(previewPath);
return new Response(pdfBytes, { headers: { 'Content-Type': 'application/pdf' } });
} finally {
// Fire-and-forget: always delete the temp preview file
unlink(previewPath).catch(() => {});
}
}