import { NextResponse } from "next/server"; import { BUCKETS, DATABASE_ID, TABLES, type Job, type JobFile } from "@/lib/appwrite/schema"; import { createAdminClient } from "@/lib/appwrite/server"; import { requireTenant } from "@/lib/appwrite/tenant-guard"; /** * Server-side download proxy. The Appwrite bucket files are scoped to the * job's two teams (clinic + lab) and the lab's frontend domain doesn't carry * an Appwrite session cookie, so a direct browser → Appwrite link 401s. We * authenticate the caller via the lab session, verify they actually have * access to the job, then stream the file out with a forced attachment * disposition. */ export async function GET( _request: Request, { params }: { params: Promise<{ jobId: string; fileId: string }> }, ) { const { jobId, fileId } = await params; let ctx; try { ctx = await requireTenant(); } catch { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { tablesDB, storage } = createAdminClient(); let job: Job; let file: JobFile; try { const [j, f] = await Promise.all([ tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId) as Promise, tablesDB.getRow(DATABASE_ID, TABLES.jobFiles, fileId) as Promise, ]); job = j as Job; file = f as JobFile; } catch { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (file.jobId !== jobId) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (ctx.tenantId !== job.clinicTenantId && ctx.tenantId !== job.labTenantId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const buf = (await storage.getFileDownload(BUCKETS.jobFiles, file.fileId)) as | ArrayBuffer | Buffer; const body = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf); // Quote the filename so spaces / non-ASCII don't break the header. const safeName = file.name.replace(/["\\]/g, "_"); return new NextResponse(body, { status: 200, headers: { "Content-Type": "application/octet-stream", "Content-Disposition": `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(file.name)}`, "Cache-Control": "private, no-store", }, }); }