mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: add subscription access risk controls
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
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,
|
||||
@@ -9,21 +20,101 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const url = new URL(req.url);
|
||||
const token = url.searchParams.get("token");
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Missing token" }, { status: 401 });
|
||||
}
|
||||
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) {
|
||||
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
|
||||
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") {
|
||||
return NextResponse.json({ error: "Subscription inactive" }, { status: 403 });
|
||||
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);
|
||||
@@ -31,6 +122,7 @@ export async function GET(
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="jboard-sub.txt"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user