import { NextRequest, NextResponse } from "next/server"; import { mkdir, writeFile, readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join } from "node:path"; const DATA_DIR = "/app/data"; const FEEDBACK_FILE = join(DATA_DIR, "feedback.jsonl"); interface FeedbackEntry { id: string; timestamp: string; url: string; error: string; message: string; userAgent: string; } function generateId(): string { const ts = Date.now().toString(36); const rand = Math.random().toString(36).substring(2, 8); return `fb-${ts}-${rand}`; } function sanitizeUA(ua: string): string { // Strip personal info from UA string return ua.replace(/\(.*?\)/g, "(redacted)").substring(0, 200); } export async function POST(request: NextRequest) { try { const body = await request.json(); const { url, error, message, userAgent } = body; if (!url || !error) { return NextResponse.json( { error: "url and error are required" }, { status: 400 } ); } const entry: FeedbackEntry = { id: generateId(), timestamp: new Date().toISOString(), url: url.substring(0, 500), error: error.substring(0, 1000), message: (message || "").substring(0, 2000), userAgent: sanitizeUA(userAgent || ""), }; // Ensure data dir exists await mkdir(DATA_DIR, { recursive: true }); // Append to JSONL file await writeFile(FEEDBACK_FILE, JSON.stringify(entry) + "\n", { flag: "a", }); return NextResponse.json({ id: entry.id }); } catch (err) { // Silent failure — don't throw errors from error-reporting endpoint console.error("Feedback save error:", err); return NextResponse.json({ id: null }, { status: 200 }); } } export async function GET(request: NextRequest) { // Admin-only endpoint — return paginated feedback const authHeader = request.headers.get("authorization") || ""; const adminToken = process.env.FEEDBACK_ADMIN_TOKEN; // Require token to be set in production if (!adminToken) { if (process.env.NODE_ENV === "production") { console.error("FEEDBACK_ADMIN_TOKEN not configured"); return NextResponse.json({ error: "Server configuration error" }, { status: 500 }); } // Dev fallback only return NextResponse.json({ error: "Unauthorized — configure FEEDBACK_ADMIN_TOKEN" }, { status: 401 }); } // Simple token auth if (authHeader !== `Bearer ${adminToken}`) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { searchParams } = new URL(request.url); const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 200); const offset = parseInt(searchParams.get("offset") || "0"); try { if (!existsSync(FEEDBACK_FILE)) { return NextResponse.json({ entries: [], total: 0 }); } const raw = await readFile(FEEDBACK_FILE, "utf-8"); const lines = raw.trim().split("\n").filter(Boolean); const entries = lines .map((line) => { try { return JSON.parse(line) as FeedbackEntry; } catch { return null; } }) .filter(Boolean) .reverse(); // newest first const total = entries.length; const page = entries.slice(offset, offset + limit); return NextResponse.json({ entries: page, total }); } catch (err) { console.error("Feedback read error:", err); return NextResponse.json({ entries: [], total: 0 }); } }