Files
WebForm-mw/app/api/staff/file/route.ts
T

103 lines
3.3 KiB
TypeScript

/**
* GET /api/staff/file?id=<civi_file_id>&key=<secret>
*
* Streams an attachment from CiviCRM to the caller. The Civi API user's
* credentials never leave the server. Auth is the same shared
* STAFF_REPORT_KEY used by /api/staff/report.
*
* In stub mode, returns a tiny placeholder PNG so the UI's preview path
* is exercisable in dev.
*/
import { NextRequest, NextResponse } from "next/server";
import { isStaffKeyValid } from "@/lib/staff-auth";
import { civi } from "@/lib/civicrm";
function isCiviStubMode(): boolean {
return !(
process.env.CIVI_BASE_URL &&
process.env.CIVI_API_KEY &&
process.env.CIVI_SITE_KEY
);
}
// 1x1 transparent PNG, base64-encoded — used as a stub attachment so the
// UI's image preview path renders something in dev.
const STUB_PNG_B64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
export async function GET(req: NextRequest) {
const url = new URL(req.url);
const key = url.searchParams.get("key");
const idStr = url.searchParams.get("id");
if (!isStaffKeyValid(key)) {
return new NextResponse("Not found", { status: 404 });
}
const id = Number(idStr);
if (!idStr || !Number.isFinite(id) || id <= 0) {
return new NextResponse("Bad request", { status: 400 });
}
if (isCiviStubMode()) {
const bytes = Buffer.from(STUB_PNG_B64, "base64");
return new NextResponse(bytes, {
status: 200,
headers: {
"content-type": "image/png",
"content-disposition": `inline; filename="stub-${id}.png"`,
"cache-control": "private, max-age=60",
},
});
}
try {
// Look up the attachment URL and metadata.
const meta = await civi<{ id: number; url: string; mime_type: string; name: string }>(
"Attachment",
"get",
{
select: ["id", "url", "mime_type", "name"],
where: [["id", "=", id]],
},
);
const row = meta.values?.[0];
if (!row?.url) {
return new NextResponse("Not found", { status: 404 });
}
const headers: Record<string, string> = {};
if (process.env.CIVI_HTTP_AUTH_USER && process.env.CIVI_HTTP_AUTH_PASS) {
const creds = Buffer.from(
`${process.env.CIVI_HTTP_AUTH_USER}:${process.env.CIVI_HTTP_AUTH_PASS}`,
).toString("base64");
headers["Authorization"] = `Basic ${creds}`;
}
const upstream = await fetch(row.url, { headers });
if (!upstream.ok || !upstream.body) {
console.error(
`[staff/file] upstream fetch failed: id=${id} status=${upstream.status}`,
);
return new NextResponse("Upstream error", { status: 502 });
}
const safeName = (row.name || `file-${id}`).replace(/"/g, "");
const mime = row.mime_type || upstream.headers.get("content-type") || "application/octet-stream";
const isInline = mime.startsWith("image/") || mime === "application/pdf";
return new NextResponse(upstream.body, {
status: 200,
headers: {
"content-type": mime,
"content-disposition": `${isInline ? "inline" : "attachment"}; filename="${safeName}"`,
"cache-control": "private, max-age=60",
},
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.error(`[staff/file] fetch threw: ${msg}`);
return new NextResponse("Upstream error", { status: 502 });
}
}