feat(15-01): create public signer download route GET /api/sign/download/[token]

- Public route — no auth session required
- Validates signer-download JWT via verifySignerDownloadToken
- Guards: expired/invalid token (401), incomplete doc (404), path traversal (403)
- Serves signed PDF with Content-Disposition attachment header
This commit is contained in:
Chandler Copeland
2026-04-03 15:43:40 -06:00
parent e1cdfe9b7b
commit 14efa1dce4

View File

@@ -0,0 +1,46 @@
import { NextRequest } from 'next/server';
import { verifySignerDownloadToken } 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');
// Public route — no auth session required
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params;
let documentId: string;
try {
({ documentId } = await verifySignerDownloadToken(token));
} catch {
return new Response('Invalid or expired download link', { status: 401 });
}
const doc = await db.query.documents.findFirst({
where: eq(documents.id, documentId),
columns: { signedFilePath: true, status: true, name: true },
});
if (!doc || doc.status !== 'Signed' || !doc.signedFilePath) {
return new Response('Document not yet complete', { status: 404 });
}
const absPath = path.join(UPLOADS_DIR, doc.signedFilePath);
if (!absPath.startsWith(UPLOADS_DIR)) {
return new Response('Forbidden', { status: 403 });
}
const fileBytes = await readFile(absPath);
return new Response(fileBytes, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${doc.name}-signed.pdf"`,
},
});
}