diff --git a/teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts b/teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts new file mode 100644 index 0000000..6728176 --- /dev/null +++ b/teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts @@ -0,0 +1,39 @@ +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 }); + + const filePath = path.join(UPLOADS_BASE, doc.filePath); + + // 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 }); + } +} diff --git a/teressa-copeland-homes/src/app/api/documents/route.ts b/teressa-copeland-homes/src/app/api/documents/route.ts new file mode 100644 index 0000000..f7f4170 --- /dev/null +++ b/teressa-copeland-homes/src/app/api/documents/route.ts @@ -0,0 +1,77 @@ +import { auth } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { documents, formTemplates } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import { copyFile, mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const SEEDS_DIR = path.join(process.cwd(), 'seeds', 'forms'); +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); + +export async function POST(req: Request) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + + const contentType = req.headers.get('content-type') ?? ''; + + let clientId: string; + let name: string; + let formTemplateId: string | undefined; + let fileBuffer: ArrayBuffer | undefined; + + if (contentType.includes('multipart/form-data')) { + // File picker path — custom PDF upload + const formData = await req.formData(); + clientId = formData.get('clientId') as string; + name = formData.get('name') as string; + const file = formData.get('file') as File; + fileBuffer = await file.arrayBuffer(); + } else { + // Library path — copy from seed template + const body = await req.json(); + clientId = body.clientId; + name = body.name; + formTemplateId = body.formTemplateId; + } + + if (!clientId || !name) { + return Response.json({ error: 'clientId and name are required' }, { status: 400 }); + } + + const docId = crypto.randomUUID(); + const destDir = path.join(UPLOADS_DIR, 'clients', clientId); + const destPath = path.join(destDir, `${docId}.pdf`); + const relPath = `clients/${clientId}/${docId}.pdf`; + + // Path traversal guard + if (!destPath.startsWith(UPLOADS_DIR)) { + return new Response('Forbidden', { status: 403 }); + } + + await mkdir(destDir, { recursive: true }); + + if (fileBuffer !== undefined) { + // Custom upload + await writeFile(destPath, Buffer.from(fileBuffer)); + } else { + // Template copy + const template = await db.query.formTemplates.findFirst({ + where: eq(formTemplates.id, formTemplateId!), + }); + if (!template) return Response.json({ error: 'Template not found' }, { status: 404 }); + + const srcPath = path.join(SEEDS_DIR, template.filename); + await copyFile(srcPath, destPath); + } + + const [doc] = await db.insert(documents).values({ + id: docId, + clientId, + name, + formTemplateId: formTemplateId ?? null, + filePath: relPath, + status: 'Draft', + }).returning(); + + return Response.json(doc, { status: 201 }); +}