diff --git a/teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts b/teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts new file mode 100644 index 0000000..7fc92f2 --- /dev/null +++ b/teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts @@ -0,0 +1,86 @@ +import { NextRequest } from 'next/server'; +import { verifyAgentDownloadToken } from '@/lib/signing/token'; +import { db } from '@/lib/db'; +import { documents } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); + +// GET /api/documents/[id]/download?adt=[agentDownloadToken] +// Requires: valid agent-download JWT in adt query param (generated server-side in document detail page) +// No Auth.js session check at this route — the short-lived JWT IS the credential (same as client download pattern) +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const url = new URL(req.url); + const adt = url.searchParams.get('adt'); + + if (!adt) { + return new Response(JSON.stringify({ error: 'Missing download token' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let documentId: string; + try { + const verified = await verifyAgentDownloadToken(adt); + documentId = verified.documentId; + } catch { + return new Response(JSON.stringify({ error: 'Download link expired or invalid' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Defense in depth: token documentId must match route [id] param + if (documentId !== id) { + return new Response(JSON.stringify({ error: 'Forbidden' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const doc = await db.query.documents.findFirst({ + where: eq(documents.id, documentId), + columns: { id: true, name: true, signedFilePath: true }, + }); + + if (!doc || !doc.signedFilePath) { + return new Response(JSON.stringify({ error: 'Signed PDF not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Path traversal guard — required on every file read from uploads/ + const absPath = path.join(UPLOADS_DIR, doc.signedFilePath); + if (!absPath.startsWith(UPLOADS_DIR)) { + return new Response(JSON.stringify({ error: 'Forbidden' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let fileBuffer: Buffer; + try { + fileBuffer = await readFile(absPath); + } catch { + return new Response(JSON.stringify({ error: 'File not found on disk' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const safeName = doc.name.replace(/[^a-zA-Z0-9-_ ]/g, ''); + return new Response(new Uint8Array(fileBuffer), { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${safeName}_signed.pdf"`, + }, + }); +}