feat(06-03): signing page — server component, PDF viewer, field overlays, progress bar
- page.tsx: server component validates JWT + one-time-use before rendering any UI - Three error states (expired/used/invalid) show static pages with no canvas - SigningPageClientWrapper: dynamic import (ssr:false) for react-pdf browser requirement - SigningPageClient: full-scroll PDF viewer with pulsing blue field overlays - Field overlay coordinates convert PDF user-space (bottom-left) to screen (top-left) - SigningProgressBar: sticky bottom bar with X/Y count + jump-to-next + submit button - api/sign/[token]/pdf: token-authenticated PDF streaming route (no agent auth)
This commit is contained in:
55
teressa-copeland-homes/src/app/api/sign/[token]/pdf/route.ts
Normal file
55
teressa-copeland-homes/src/app/api/sign/[token]/pdf/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { verifySigningToken } from '@/lib/signing/token';
|
||||
import { db } from '@/lib/db';
|
||||
import { documents } from '@/lib/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
// Public route — authenticated by signing token, no agent auth required
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ token: string }> }
|
||||
) {
|
||||
const { token } = await params;
|
||||
|
||||
// Validate JWT (no usedAt check — client may still be viewing while session is active)
|
||||
let payload: { documentId: string; jti: string; exp: number };
|
||||
try {
|
||||
payload = await verifySigningToken(token);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Fetch preparedFilePath
|
||||
const doc = await db.query.documents.findFirst({
|
||||
where: eq(documents.id, payload.documentId),
|
||||
columns: { preparedFilePath: true },
|
||||
});
|
||||
|
||||
if (!doc?.preparedFilePath) {
|
||||
return NextResponse.json({ error: 'Document not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Path traversal guard — preparedFilePath is stored as relative path (e.g. clients/{id}/{uuid}.pdf)
|
||||
const normalizedPath = doc.preparedFilePath.replace(/^\/+/, '');
|
||||
if (normalizedPath.includes('..')) {
|
||||
return NextResponse.json({ error: 'Invalid file path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const absolutePath = join(process.cwd(), 'uploads', normalizedPath);
|
||||
|
||||
try {
|
||||
const fileBuffer = await readFile(absolutePath);
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': 'inline',
|
||||
'Cache-Control': 'private, no-store',
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user