feat(pitch-deck): data room — file sharing and investor uploads
Build pitch-deck / build-push-deploy (push) Successful in 1m21s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 31s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 32s

- lib/dataroom-storage.ts: local volume storage (DATAROOM_PATH env var,
  default /data/dataroom) replacing NextCloud WebDAV
- Admin API: upload documents, rename, delete, manage per-investor releases
- Investor API: list released documents, stream download with audit log,
  upload own documents (max DATAROOM_MAX_UPLOAD_MB, default 50MB)
- /pitch-admin/dataroom: document list + release toggles + investor uploads tab
- /dataroom: investor-facing document library + upload section
- All reads and writes logged to pitch_audit_logs
- Migration 005: dataroom_documents, dataroom_releases, dataroom_investor_uploads
- AdminShell: Data Room nav link (FolderOpen icon)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-05-01 15:38:21 +02:00
parent 1bf1411c66
commit 9888b1b5d7
13 changed files with 930 additions and 0 deletions
@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server'
import pool from '@/lib/db'
import { investorUploadDir, saveFile, safeName } from '@/lib/dataroom-storage'
import { logAudit } from '@/lib/auth'
import { randomUUID } from 'crypto'
export const dynamic = 'force-dynamic'
const MAX_BYTES = parseInt(process.env.DATAROOM_MAX_UPLOAD_MB || '50') * 1024 * 1024
export async function GET(request: NextRequest) {
const investorId = request.headers.get('x-investor-id')
if (!investorId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { rows } = await pool.query(
`SELECT id, filename, display_name, mime_type, file_size, created_at
FROM dataroom_investor_uploads
WHERE investor_id = $1
ORDER BY created_at DESC`,
[investorId],
)
return NextResponse.json({ uploads: rows })
}
export async function POST(request: NextRequest) {
const investorId = request.headers.get('x-investor-id')
const sessionId = request.headers.get('x-session-id')
if (!investorId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const formData = await request.formData()
const file = formData.get('file') as File | null
const displayName = (formData.get('display_name') as string | null) || null
if (!file || file.size === 0) return NextResponse.json({ error: 'No file provided' }, { status: 400 })
if (file.size > MAX_BYTES) return NextResponse.json({ error: `File exceeds ${process.env.DATAROOM_MAX_UPLOAD_MB || 50}MB limit` }, { status: 413 })
const uploadId = randomUUID()
const filename = safeName(file.name)
const buffer = Buffer.from(await file.arrayBuffer())
const filePath = await saveFile(investorUploadDir(investorId, uploadId), filename, buffer)
const { rows } = await pool.query(
`INSERT INTO dataroom_investor_uploads (id, investor_id, filename, file_path, display_name, mime_type, file_size)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, filename, display_name, mime_type, file_size, created_at`,
[uploadId, investorId, filename, filePath, displayName || file.name, file.type || 'application/octet-stream', file.size],
)
await logAudit(investorId, 'dataroom_investor_uploaded', { upload_id: uploadId, filename, file_size: file.size }, request, undefined, sessionId ?? undefined)
return NextResponse.json({ upload: rows[0] }, { status: 201 })
}