- Remove production DB dump (db_backup_*.sql) and update.log from git tracking - Add DB backups to .gitignore - Replace all console.log/console.error with structured logger module - Translate Dutch error messages to English (link-discord.ts) - Remove dead code blocks (register-form.tsx false && pattern) - Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins - Add Prettier config - Add eslint-plugin-security for security-aware linting - Fix all 119+ ESLint warnings across the codebase: - Resolve security/detect-object-injection with safe access patterns - Resolve security/detect-non-literal-fs-filename with path traversal validation - Replace <img> with next/image <Image> component - Remove unused variables and imports - Replace non-null assertions with proper type guards - Replace <a> with <Link> for internal navigation - Use next/script Script component for external scripts - Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher) - Add lint and format scripts to package.json All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
import {
|
|
createOrder,
|
|
creditsPerUnit,
|
|
isPayPalConfigured,
|
|
PAYPAL_CURRENCY,
|
|
} from "@/lib/services/paypal";
|
|
import { env } from "@/env";
|
|
import { logger } from "@/lib/logger";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const MIN_AMOUNT = 1;
|
|
const MAX_AMOUNT = 500;
|
|
|
|
/**
|
|
* POST /api/paypal/create — create a PayPal CAPTURE order for the signed-in user.
|
|
* Body: { amount: number } (in the configured currency, default USD).
|
|
* Returns { id, approveUrl } on success; a clear JSON error otherwise.
|
|
*
|
|
* Auth-gated via auth(): the order is tied to the session, never to a body field.
|
|
*/
|
|
export async function POST(req: Request): Promise<Response> {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "You must be signed in to top up." }, { status: 401 });
|
|
}
|
|
|
|
// Fail fast (and clearly) when the sandbox/live keys aren't set.
|
|
if (!isPayPalConfigured()) {
|
|
return NextResponse.json(
|
|
{ error: "PayPal is not configured. Set PAYPAL_CLIENT_ID and PAYPAL_SECRET." },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
|
|
}
|
|
|
|
const raw = (body as { amount?: unknown })?.amount;
|
|
const amount = Math.round(Number(raw) * 100) / 100;
|
|
if (!Number.isFinite(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) {
|
|
return NextResponse.json(
|
|
{ error: `Enter an amount between ${MIN_AMOUNT} and ${MAX_AMOUNT} ${PAYPAL_CURRENCY}.` },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
|
|
const credits = Math.floor(amount * creditsPerUnit());
|
|
const base = env.APP_URL.replace(/\/+$/, "");
|
|
|
|
try {
|
|
const order = await createOrder(amount, {
|
|
description: `${env.HOTEL_NAME} top-up: ${credits} credits`,
|
|
returnUrl: `${base}/shop/topup?status=success`,
|
|
cancelUrl: `${base}/shop/topup?status=cancel`,
|
|
});
|
|
|
|
if (!order.approveUrl) {
|
|
return NextResponse.json(
|
|
{ error: "PayPal did not return an approval link. Try again." },
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
id: order.id,
|
|
approveUrl: order.approveUrl,
|
|
amount,
|
|
currency: PAYPAL_CURRENCY,
|
|
credits,
|
|
});
|
|
} catch (e) {
|
|
logger.error("PayPal create order failed", { module: "paypal/create", error: (e as Error).message });
|
|
return NextResponse.json(
|
|
{ error: "Could not start the PayPal checkout. Please try again." },
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
}
|