Files
Ajasra 6772e0cf37 perf(booth): optimize assets and result flow
WebP reduces UI payload; JPEG keeps portrait saves compatible.\nFinish ends the visit without prompting another portrait.
2026-10-30 00:00:16 +08:00

53 lines
1.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";
export async function GET(
req: NextRequest,
{ params }: { params: { filename: string } }
) {
try {
const rawFilename = params.filename;
// 1. Strict filename validation: alphanumeric + hyphens/underscores + safe image extensions only
const validFilenameRegex = /^[a-zA-Z0-9_-]+\.(jpg|jpeg|png|webp)$/i;
if (!rawFilename || !validFilenameRegex.test(rawFilename)) {
return new NextResponse("Bad Request: Invalid or disallowed image filename", { status: 400 });
}
const filename = path.basename(rawFilename);
const imagesDir = path.resolve(process.cwd(), "data", "images");
const filePath = path.resolve(imagesDir, filename);
// 2. Prevent path traversal outside data/images directory
if (!filePath.startsWith(imagesDir)) {
return new NextResponse("Access Denied", { status: 403 });
}
if (!fs.existsSync(filePath)) {
return new NextResponse("Image Not Found", { status: 404 });
}
const fileBuffer = fs.readFileSync(filePath);
const ext = path.extname(filename).toLowerCase();
let contentType = "image/png";
if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
else if (ext === ".webp") contentType = "image/webp";
return new NextResponse(fileBuffer, {
status: 200,
headers: {
"Content-Type": contentType,
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy": "default-src 'none'; sandbox",
"Content-Disposition": `${req.nextUrl.searchParams.get("download") === "1" ? "attachment" : "inline"}; filename="${filename}"`,
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch (error) {
console.error("[API Image Serve] Error reading file:", error);
return new NextResponse("Server Error", { status: 500 });
}
}