10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, gap_closure, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | gap_closure | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-audit-trail-and-download | 04 | execute | 1 |
|
true | true |
|
|
Purpose: LEGAL-03 requires "agent downloads via authenticated presigned URLs only." The /file route currently serves signedFilePath ?? filePath — serving the signed PDF via session auth alone, with no short-lived token. This creates a second download path for signed PDFs that bypasses the presigned token system built in Plans 07-01 and 07-02.
User decision (Option A — locked): Remove the signedFilePath fallback from /file. Hide PdfViewer's Download anchor for Signed documents. The PreparePanel presigned URL is the sole signed PDF download path.
Output:
route.ts— servesdoc.filePathonly (2-line change)PdfViewer.tsx— Download anchor conditionally hidden when docStatus is 'Signed'
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-audit-trail-and-download/07-VERIFICATION.mdFrom teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts (full file, 41 lines):
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { documents } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const UPLOADS_BASE = path.join(process.cwd(), 'uploads');
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session) return new Response('Unauthorized', { status: 401 });
const { id } = await params;
const doc = await db.query.documents.findFirst({
where: eq(documents.id, id),
});
if (!doc || !doc.filePath) return new Response('Not found', { status: 404 });
// Serve signed PDF for completed documents, original otherwise
const relativePath = doc.signedFilePath ?? doc.filePath; // <-- LINE 25: THE GAP
const filePath = path.join(UPLOADS_BASE, relativePath);
// Path traversal guard — critical security check
if (!filePath.startsWith(UPLOADS_BASE)) {
return new Response('Forbidden', { status: 403 });
}
try {
const buffer = await readFile(filePath);
return new Response(buffer, {
headers: { 'Content-Type': 'application/pdf' },
});
} catch {
return new Response('File not found', { status: 404 });
}
}
From teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx (full file, 94 lines):
'use client';
import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import { FieldPlacer } from './FieldPlacer';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
interface PageInfo {
originalWidth: number;
originalHeight: number;
width: number;
height: number;
scale: number;
}
export function PdfViewer({ docId, docStatus }: { docId: string; docStatus?: string }) {
// ...
const readOnly = docStatus !== 'Draft';
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-3 text-sm">
{/* ... Prev/Next/Zoom buttons ... */}
<a // <-- LINES 60-66: THE GAP
href={`/api/documents/${docId}/file`}
download
className="px-3 py-1 border rounded hover:bg-gray-100"
>
Download
</a>
</div>
{/* ... Document/Page render ... */}
</div>
);
}
Current line 25:
const relativePath = doc.signedFilePath ?? doc.filePath;
Replace with:
// Always serve the original uploaded PDF — signed PDF is exclusively
// available via the presigned /download?adt=[token] route (LEGAL-03)
const relativePath = doc.filePath;
Also update the comment on the line above (currently "Serve signed PDF for completed documents, original otherwise") to remove the misleading description. Remove that comment entirely or replace it with:
// Serve the original unsigned PDF only — see LEGAL-03
No other changes to this file. The session auth check, path traversal guard, and readFile/Response logic are all correct and must remain untouched. grep -n "signedFilePath" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts; echo "exit:$?" grep returns no matches (exit 1 from grep is acceptable — means the string is absent). The file still compiles (no TypeScript errors introduced — doc.filePath is string | null per schema, same type as before).
Task 2: Hide Download anchor in PdfViewer for Signed documents teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx Wrap the Download anchor (lines 60-66) in a conditional that hides it when the document status is 'Signed'.Current code (lines 60-66):
<a
href={`/api/documents/${docId}/file`}
download
className="px-3 py-1 border rounded hover:bg-gray-100"
>
Download
</a>
Replace with:
{docStatus !== 'Signed' && (
<a
href={`/api/documents/${docId}/file`}
download
className="px-3 py-1 border rounded hover:bg-gray-100"
>
Download
</a>
)}
Rationale: For Signed documents, the PreparePanel already renders a Download Signed PDF anchor pointing to the presigned /download?adt=[token] URL. The viewer toolbar Download (which points to /file) is redundant and now incorrect for Signed docs since /file no longer serves the signed PDF. For Draft/Sent/Viewed documents, the toolbar Download is still useful — it serves the original PDF for reference.
No other changes to PdfViewer.tsx. The Document file prop (/api/documents/${docId}/file) remains unchanged — the PDF viewer still loads the original for in-browser display regardless of status.
grep -n "docStatus !== 'Signed'" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
grep finds the conditional wrapping the Download anchor.
The Download anchor is present in the file but only rendered when docStatus !== 'Signed'.
The Document file prop (/api/documents/${docId}/file) is unchanged on line 72.
-
Confirm /file no longer contains signedFilePath:
grep -n "signedFilePath" teressa-copeland-homes/src/app/api/documents/\[id\]/file/route.ts— must return nothing. -
Confirm PdfViewer Download anchor is conditional:
grep -n "docStatus !== 'Signed'" teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/PdfViewer.tsx— must find the conditional. -
TypeScript build check (optional but recommended):
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20No new errors expected — both changes use types already present in the codebase.
<success_criteria> LEGAL-03 is fully satisfied when:
- GET /api/documents/[id]/file always returns the unsigned original PDF (doc.filePath), never the signed PDF (doc.signedFilePath)
- A logged-in agent navigating to /api/documents/[signed-doc-id]/file receives the original unsigned PDF, not the signed one
- The PDF viewer toolbar shows no Download button when docStatus is 'Signed'
- The PreparePanel Download Signed PDF anchor (presigned, 5-min TTL) remains the only way to download a signed PDF
- SIGN-07 is unaffected — agent can still download signed PDFs via the PreparePanel presigned URL </success_criteria>