--- phase: 07-audit-trail-and-download plan: 04 type: execute wave: 1 depends_on: [] files_modified: - teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx autonomous: true gap_closure: true requirements: - SIGN-07 - LEGAL-03 must_haves: truths: - "GET /api/documents/[id]/file NEVER serves the signed PDF — it always reads doc.filePath (the unsigned original only)" - "The Download anchor in PdfViewer is absent (not rendered) when docStatus is 'Signed'" - "LEGAL-03 satisfied: the only download path for a signed PDF is /api/documents/[id]/download?adt=[token] (presigned, 5-min TTL)" artifacts: - path: "teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts" provides: "Restricted /file route serving original PDF only" contains: "doc.filePath" must_not_contain: "signedFilePath" - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx" provides: "PdfViewer without Download anchor for Signed documents" must_not_contain: "docStatus !== 'Signed'" key_links: - from: "PdfViewer.tsx" to: "/api/documents/[id]/file" via: "Document file prop (viewing only — not downloadable for Signed)" note: "The viewer still LOADS the PDF via /file (original), but the Download anchor is hidden when Signed" --- Close LEGAL-03 gap: restrict the `/file` route to serve only the unsigned original PDF and remove the Download anchor from PdfViewer when a document is Signed. 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` — serves `doc.filePath` only (2-line change) - `PdfViewer.tsx` — Download anchor conditionally hidden when docStatus is 'Signed' @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md From teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts (full file, 41 lines): ```typescript 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): ```tsx '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 (
{/* ... Prev/Next/Zoom buttons ... */} Download
{/* ... Document/Page render ... */}
); } ```
Task 1: Restrict /file route to original PDF only teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts Replace line 25 (the signedFilePath fallback) with a direct use of doc.filePath. Current line 25: ```typescript const relativePath = doc.signedFilePath ?? doc.filePath; ``` Replace with: ```typescript // 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: ```typescript // 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): ```tsx Download ``` Replace with: ```tsx {docStatus !== 'Signed' && ( Download )} ``` 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. After both tasks complete: 1. Confirm /file no longer contains signedFilePath: `grep -n "signedFilePath" teressa-copeland-homes/src/app/api/documents/\[id\]/file/route.ts` — must return nothing. 2. 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. 3. TypeScript build check (optional but recommended): `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20` No new errors expected — both changes use types already present in the codebase. LEGAL-03 is fully satisfied when: 1. GET /api/documents/[id]/file always returns the unsigned original PDF (doc.filePath), never the signed PDF (doc.signedFilePath) 2. A logged-in agent navigating to /api/documents/[signed-doc-id]/file receives the original unsigned PDF, not the signed one 3. The PDF viewer toolbar shows no Download button when docStatus is 'Signed' 4. The PreparePanel Download Signed PDF anchor (presigned, 5-min TTL) remains the only way to download a signed PDF 5. SIGN-07 is unaffected — agent can still download signed PDFs via the PreparePanel presigned URL After completion, create `.planning/phases/07-audit-trail-and-download/07-04-SUMMARY.md` following the summary template.