security: fix critical webhook direct upgrade path, seed auth, CORS Vary header
- 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
This commit is contained in:
@@ -66,7 +66,17 @@ export async function POST(request: NextRequest) {
|
||||
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 || "flh-feedback-admin";
|
||||
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}`) {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST() {
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Require ADMIN_SECRET to prevent unauthorized seeding
|
||||
const adminSecret = process.env.ADMIN_SECRET;
|
||||
const providedSecret = req.headers.get("x-admin-secret") || req.headers.get("authorization")?.replace("Bearer ", "");
|
||||
if (adminSecret && providedSecret !== adminSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized — valid admin secret required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
// ── 1. Seed Demo Users ──────────────────────────────────────────
|
||||
const users = [
|
||||
{
|
||||
|
||||
@@ -61,7 +61,8 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// In production, this would create a Polar.sh checkout session.
|
||||
// For development, we mock the flow by marking the user as premium/pro.
|
||||
const useMock = process.env.MOCK_PAYMENTS === "true";
|
||||
const useMock = process.env.MOCK_PAYMENTS === "true"
|
||||
&& process.env.NODE_ENV !== "production";
|
||||
|
||||
if (useMock) {
|
||||
const trialEndsAt = new Date();
|
||||
|
||||
@@ -30,8 +30,15 @@ export async function POST(req: NextRequest) {
|
||||
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
|
||||
// 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 } },
|
||||
|
||||
@@ -11,12 +11,20 @@ import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Check for direct fallback query params (dev/testing)
|
||||
// Check for direct fallback query params (dev/testing only)
|
||||
const url = new URL(req.url);
|
||||
const directUserId = url.searchParams.get("userId");
|
||||
const directTier = url.searchParams.get("tier");
|
||||
|
||||
if (directUserId && directTier) {
|
||||
// Production guard: never allow direct upgrades in production
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.error("Direct upgrade blocked — not allowed in production");
|
||||
return NextResponse.json(
|
||||
{ error: "Direct upgrades not allowed in production" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
return handleDirectUpgrade(directUserId, directTier);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ export function middleware(request: NextRequest) {
|
||||
response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
response.headers.set("Access-Control-Max-Age", "86400");
|
||||
response.headers.set("Vary", "Origin");
|
||||
} else if (origin && !isAllowed) {
|
||||
response.headers.set("Access-Control-Allow-Origin", "https://falahos.my");
|
||||
response.headers.set("Vary", "Origin");
|
||||
}
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
|
||||
Reference in New Issue
Block a user