Skip to main content

Distribution PDF Security Migration

Overview

Distribution PDFs contain sensitive financial information (investor names, amounts, profit distributions, affiliate commissions) and were previously stored in public/distribution-pdfs/, making them accessible to anyone with the URL.

This migration moves PDFs to private storage with authenticated access.

Changes Made

1. New Authenticated Download API

  • Endpoint: GET /api/admin/distribution-pdfs/{id}/download
  • Authentication: Founder-only (checks JWT cookie)
  • Functionality: Serves PDF files from private storage
  • Backward Compatible: Handles both old /distribution-pdfs/ and new /storage/ paths

2. Private Storage Directory

  • Location: storage/distribution-pdfs/ (outside public/)
  • Docker Volume: Mounted in docker-compose.yml for persistence
  • Gitignore: Added to prevent committing sensitive files

3. Updated PDF Generation

  • app/api/cycles/[id]/generate-pdf/route.ts - Saves to storage/
  • scripts/regenerate-pdf.ts - Saves to storage/

4. Updated UI

  • app/admin/distribution-pdfs/page.tsx - Uses authenticated API endpoint
  • View and Download buttons now route through /api/admin/distribution-pdfs/{id}/download

Deployment Steps

Development/Local Testing

# 1. Run migration script to move existing PDFs
./opp migrate-pdfs-to-private

# 2. Verify PDFs are accessible at /admin/distribution-pdfs
# 3. Test download functionality
# 4. Test PDF viewer modal

# 5. After verification, remove old PDFs (optional)
rm -rf public/distribution-pdfs/*.pdf

Production Deployment

# ON PRODUCTION HOST (SSH into jeangrey)

# 1. Create storage directory on host
cd ~/public_html/reprise
mkdir -p storage/distribution-pdfs

# 2. Copy existing PDFs from Docker to host storage
docker cp opportunitydao-app:/app/public/distribution-pdfs/. ./storage/distribution-pdfs/

# 3. Set proper ownership
chown -R opportunitydao:opportunitydao ./storage

# 4. Deploy new code (git push triggers CI/CD)
# This will:
# - Update app with new authenticated endpoint
# - Mount storage/ directory via docker-compose.yml
# - Start serving PDFs from private storage

# 5. After deployment, run migration script inside container
docker-compose exec app npx tsx scripts/migrate-pdfs-to-private-storage.ts

# 6. Verify PDFs are accessible
# Navigate to https://opportunitydao.com/admin/distribution-pdfs
# Test download and view functionality

# 7. Clean up old public PDFs (after confirming everything works)
docker-compose exec app rm -rf /app/public/distribution-pdfs/*.pdf

Docker Volume Configuration

The docker-compose.yml now includes:

volumes:
# Private storage for sensitive user-generated files (PDFs, uploads)
# Persists across container restarts and deployments
- ./storage:/app/storage

This ensures:

  • ✅ PDFs persist across deployments
  • ✅ No data loss on container restart
  • ✅ Files are on host filesystem (easy backup)
  • ✅ Files are NOT in public/ (not web-accessible)

Security Benefits

Before (Insecure)

  • ❌ PDFs in public/distribution-pdfs/
  • ❌ Accessible to anyone with URL: https://domain.com/distribution-pdfs/Profit_Distribution_Cycle1.pdf
  • ❌ No authentication required
  • ❌ Sensitive financial data exposed

After (Secure)

  • ✅ PDFs in storage/distribution-pdfs/ (private)
  • ✅ Accessible only through authenticated API endpoint
  • ✅ Founder-only access (JWT verification)
  • ✅ Audit trail (API logs who accessed what)
  • ✅ Backward compatible (old paths still work during migration)

Migration Script Details

Command: ./opp migrate-pdfs-to-private

What it does:

  1. Creates storage/distribution-pdfs/ directory
  2. Finds all PDF records in database
  3. Copies files from public/distribution-pdfs/ to storage/distribution-pdfs/
  4. Updates database records with new file paths
  5. Provides summary of migration results

Safe to run multiple times - skips already migrated PDFs

Rollback Plan

If issues arise during production deployment:

# 1. Revert code changes
git revert <commit-hash>
git push origin main

# 2. PDFs remain in both locations, so old code still works

# 3. Clean up after confirming rollback works
rm -rf ~/public_html/reprise/storage/distribution-pdfs

Testing Checklist

  • View PDF list at /admin/distribution-pdfs
  • Download a PDF (verify it downloads)
  • View a PDF in modal (verify it displays)
  • Try to access old public URL directly (should fail after cleanup)
  • Try to access download API without authentication (should return 401)
  • Generate new PDF for a settled cycle (should save to storage/)
  • Regenerate old PDF using CLI (should save to storage/)

Future Enhancements

Consider adding:

  • Investor-specific PDF access (let investors download their own cycle reports)
  • PDF download audit logging
  • Automatic old file cleanup after successful migration
  • S3/cloud storage for scalability
  • PDF encryption at rest