Files
red/teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts

87 lines
2.7 KiB
TypeScript
Raw Normal View History

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"`,
},
});
}