Add security middleware, audit log, alerts, PayPal, cron, radio + apps
Security (launch blockers): - src/middleware.ts (edge): forwards x-pathname + real client IP. - access-guard.ts (Node, from root layout): routes non-staff to /maintenance when maintenance mode is on, banned users to /banned. New /banned + /maintenance pages (the consumers the admin toggle was missing). Admin layout enforces force_staff_2fa before /admin. - staff-activity.ts audit log wired into ban/lift/give-currency/set-rank actions. Infra (parallel agents): alert service (alert_logs + Discord embed + email), PayPal top-up (create/capture API routes + /shop/topup), cron worker (scripts/jobs-worker.ts via croner: emulator-ping->alert, maintenance-check, bans-cleanup), social connections page, admin radio settings/banners/ranks. Public radio subsystem: /radio (+schedule, shouts+post, contests, giveaways, apply, leaderboard) and /apply/staff + /apply/team submission forms. Radio nav link added. .env.example documents the new optional vars. (radio song-requests dropped: its table is a stub in AtomCMS — columns added by un-modeled alter-migrations.) Verified: tsc exit 0, vitest 48/48, next build exit 0 (82 page routes).
This commit is contained in:
1 parent
e19debb795
commit
22d53d0e9c
40 files changed
+3781
-6
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import {
|
||||
createOrder,
|
||||
creditsPerUnit,
|
||||
isPayPalConfigured,
|
||||
PAYPAL_CURRENCY,
|
||||
} from "@/lib/services/paypal";
|
||||
import { env } from "@/env";
|
||||
|
||||
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) {
|
||||
console.error("[paypal/create]", (e as Error).message);
|
||||
return NextResponse.json(
|
||||
{ error: "Could not start the PayPal checkout. Please try again." },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user