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