de48918acf
Deploy Staging / build (push) Failing after 26m15s
- Webhook polar/route: block direct ?userId=&tier= upgrades in production (NODE_ENV guard) - Seed route: require ADMIN_SECRET via x-admin-secret header - Feedback route: require FEEDBACK_ADMIN_TOKEN env var in production - CORS middleware: add Vary: Origin header for caching correctness - MOCK_PAYMENTS: add NODE_ENV production guard - Wallet top-up: add production guard for mock mode when POLAR_ACCESS_TOKEN unset
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
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 });
|
|
}
|
|
}
|