Files
J-Board-Lite/src/app/api/subscription/[id]/route.ts
2026-04-29 14:26:25 +10:00

129 lines
3.3 KiB
TypeScript

import { NextResponse } from "next/server";
import { generateSubscriptionContent } from "@/services/subscription";
import { prisma } from "@/lib/prisma";
import { rateLimit } from "@/lib/rate-limit";
import { getClientRequestContext } from "@/lib/request-context";
import { recordSubscriptionAccess } from "@/services/subscription-risk";
const SUBSCRIPTION_TOKEN_LIMIT = 60;
const SUBSCRIPTION_IP_LIMIT = 180;
const SUBSCRIPTION_RATE_WINDOW_SECONDS = 60 * 60;
function jsonError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const url = new URL(req.url);
const token = url.searchParams.get("token");
const context = getClientRequestContext(req.headers);
const sub = await prisma.userSubscription.findUnique({
where: { id },
select: {
id: true,
userId: true,
downloadToken: true,
status: true,
plan: { select: { type: true } },
},
});
const ipLimit = await rateLimit(
`ratelimit:subscription:ip:${context.ip}`,
SUBSCRIPTION_IP_LIMIT,
SUBSCRIPTION_RATE_WINDOW_SECONDS,
);
if (!ipLimit.success) {
await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub?.userId,
subscriptionId: sub?.id ?? id,
allowed: false,
reason: "rate_limited",
});
return jsonError("Too many subscription requests", 429);
}
if (!token) {
await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub?.userId,
subscriptionId: sub?.id ?? id,
allowed: false,
reason: "missing_token",
});
return jsonError("Missing token", 401);
}
if (!sub || sub.downloadToken !== token) {
await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub?.userId,
subscriptionId: sub?.id ?? id,
allowed: false,
reason: "invalid_token",
});
return jsonError("Invalid token", 401);
}
const tokenLimit = await rateLimit(
`ratelimit:subscription:single:${sub.id}`,
SUBSCRIPTION_TOKEN_LIMIT,
SUBSCRIPTION_RATE_WINDOW_SECONDS,
);
if (!tokenLimit.success) {
await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub.userId,
subscriptionId: sub.id,
allowed: false,
reason: "rate_limited",
});
return jsonError("Too many subscription requests", 429);
}
if (sub.status !== "ACTIVE") {
await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub.userId,
subscriptionId: sub.id,
allowed: false,
reason: "subscription_inactive",
});
return jsonError("Subscription inactive", 403);
}
const risk = await recordSubscriptionAccess({
kind: "SINGLE",
context,
userId: sub.userId,
subscriptionId: sub.id,
evaluateRisk: sub.plan.type === "PROXY",
});
if (risk.suspended) {
return jsonError("Subscription suspended by risk control", 403);
}
const content = await generateSubscriptionContent(id);
return new Response(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": `attachment; filename="jboard-sub.txt"`,
"Cache-Control": "no-store",
},
});
}