import { NextResponse } from "next/server"; import { stat } from "fs/promises"; import { createReadStream } from "fs"; import path from "path"; import mime from "mime"; import { getUserBearerFromRequest } from "@/lib/directus"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export const revalidate = 0; const ROOT = process.env.FILES_ROOT || "/app/files"; function safeJoin(root: string, p: string) { const rootResolved = path.resolve(root); const raw = path.normalize("/" + (p || "/")); // always absolute-ish const abs = path.resolve(rootResolved, "." + raw); // stays under root if (abs === rootResolved) return abs; // allow root if (abs.startsWith(rootResolved + path.sep)) return abs; throw new Error("Invalid path"); } export async function GET(req: Request) { try { // Auth gate: require ma_at const bearer = getUserBearerFromRequest(req); if (!bearer) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { searchParams } = new URL(req.url); const p = searchParams.get("path"); if (!p) { return NextResponse.json({ error: "Missing path" }, { status: 400 }); } const abs = safeJoin(ROOT, p); const s = await stat(abs); if (!s.isFile()) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } const stream = createReadStream(abs); const type = mime.getType(abs) || "application/octet-stream"; return new Response(stream as any, { headers: { "Content-Type": type, "Content-Length": String(s.size), // cache only for the requesting user/agent "Cache-Control": "private, max-age=3600", }, }); } catch (err: any) { const msg = err?.message || "Error"; const code = msg === "Invalid path" ? 400 : 404; return NextResponse.json({ error: msg }, { status: code }); } }