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.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { requireAuth } from "@/lib/auth";
|
|
|
|
const TOP_UP_AMOUNTS = {
|
|
500: { usd: 0.99, bonus: 0, priceEnv: "POLAR_FLH_500" },
|
|
1000: { usd: 0.99, bonus: 0, priceEnv: "POLAR_FLH_1000" },
|
|
5000: { usd: 4.99, bonus: 500, priceEnv: "POLAR_FLH_5000" },
|
|
10000: { usd: 9.99, bonus: 2000, priceEnv: "POLAR_FLH_10000" },
|
|
50000: { usd: 49.99, bonus: 15000, priceEnv: "POLAR_FLH_50000" },
|
|
} as const;
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const jwtPayload = await requireAuth(req);
|
|
if (!jwtPayload) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const body = await req.json();
|
|
const { amount } = body;
|
|
|
|
if (!amount || !(amount in TOP_UP_AMOUNTS)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid amount. Choose 500, 1000, 5000, 10000, or 50000 FLH." },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const pricing = TOP_UP_AMOUNTS[amount as keyof typeof TOP_UP_AMOUNTS];
|
|
const totalFlh = amount + (pricing.bonus || 0);
|
|
|
|
// Mock mode: use when POLAR_ACCESS_TOKEN is not configured (dev only)
|
|
if (!process.env.POLAR_ACCESS_TOKEN) {
|
|
// Production guard: never allow mock top-ups in production
|
|
if (process.env.NODE_ENV === "production") {
|
|
return NextResponse.json(
|
|
{ error: "Payment service not configured" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
await prisma.user.update({
|
|
where: { id: jwtPayload.userId },
|
|
data: { flhBalance: { increment: totalFlh } },
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
url: "/wallet?topup=success",
|
|
mock: true,
|
|
amount,
|
|
bonus: pricing.bonus || 0,
|
|
amountUsd: pricing.usd,
|
|
});
|
|
}
|
|
|
|
// Production — Polar.sh checkout session
|
|
const productPriceId = process.env[pricing.priceEnv];
|
|
if (!productPriceId) {
|
|
return NextResponse.json(
|
|
{ error: `Polar price not configured for ${amount} FLH. Set ${pricing.priceEnv} env var.` },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const origin = req.headers.get("origin") || req.headers.get("host") || "";
|
|
const baseUrl = origin.startsWith("http") ? origin : `https://${origin}`;
|
|
|
|
const polarResponse = await fetch("https://api.polar.sh/v1/checkouts/", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.POLAR_ACCESS_TOKEN}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
product_price_id: productPriceId,
|
|
success_url: `${baseUrl}/wallet?topup=success&amount=${amount}`,
|
|
customer_email: jwtPayload.email,
|
|
return_url: `${baseUrl}/wallet`,
|
|
metadata: {
|
|
type: "flh_purchase",
|
|
flh_amount: totalFlh,
|
|
user_id: jwtPayload.userId,
|
|
user_email: jwtPayload.email,
|
|
},
|
|
allow_trial: false,
|
|
}),
|
|
});
|
|
|
|
if (!polarResponse.ok) {
|
|
const errorBody = await polarResponse.text();
|
|
console.error("Polar API error:", polarResponse.status, errorBody);
|
|
return NextResponse.json(
|
|
{ error: `Checkout creation failed (HTTP ${polarResponse.status})` },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
|
|
const polarData = await polarResponse.json();
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
url: polarData.url,
|
|
checkout_id: polarData.id,
|
|
amount,
|
|
amountUsd: pricing.usd,
|
|
bonus: pricing.bonus || 0,
|
|
});
|
|
} catch (error) {
|
|
console.error("Top-up checkout error:", error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "Failed to create checkout" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|