Files

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
teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
true true
SIGN-07
LEGAL-03
truths artifacts key_links
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)
path provides contains must_not_contain
teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts Restricted /file route serving original PDF only doc.filePath signedFilePath
path provides must_not_contain
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx PdfViewer without Download anchor for Signed documents docStatus !== 'Signed'
from to via note
PdfViewer.tsx /api/documents/[id]/file Document file prop (viewing only — not downloadable for Signed) 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'

<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.md

From 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>
  );
}
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:

  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.

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.

<success_criteria> 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 </success_criteria>
After completion, create `.planning/phases/07-audit-trail-and-download/07-04-SUMMARY.md` following the summary template.