← arena
Download your invoice
On-callTypeScriptNext.jsAuth
BILL-215the ticket
Let customers download their own invoice PDFs
Billing generates a PDF per invoice and drops it on the shared volume. Right now customers have to email support to get a copy.
Add an authenticated endpoint that serves a customer their own invoice PDF. Invoices belong to an organisation; a session carries organizationId.
the agent’s descriptionagent · sonnet-class model
New route at /api/invoices/[id]/pdf. Guarded with requireSession, returns 401 for anonymous callers and 404 for an invoice id that doesn't exist. Streams the PDF back with the right content type.
Tested logged out (401), logged in with a real invoice (PDF downloads), and logged in with a garbage id (404).
Written by the agent that opened the PR. Fluent, specific, and not evidence of anything.
src/app/api/invoices/[id]/pdf/route.ts34 lines
1
import { NextRequest, NextResponse } from "next/server";2
import path from "node:path";3
import { readFile } from "node:fs/promises";4
import { requireSession } from "@/server/auth";5
import { db } from "@/server/db";6
7
const INVOICE_DIR = "/var/data/invoices";8
9
export async function GET(10
req: NextRequest,11
{ params }: { params: Promise<{ id: string }> },12
) {13
const session = await requireSession(req);14
if (!session) {15
return NextResponse.json({ error: "unauthorized" }, { status: 401 });16
}17
18
const { id } = await params;19
20
const invoice = await db.invoice.findUnique({ where: { id } });21
if (!invoice) {22
return NextResponse.json({ error: "not found" }, { status: 404 });23
}24
25
const filePath = path.join(INVOICE_DIR, id + ".pdf");26
const pdf = await readFile(filePath);27
28
return new NextResponse(pdf, {29
headers: {30
"Content-Type": "application/pdf",31
"Content-Disposition": 'inline; filename="invoice.pdf"',32
},33
});34
}