Files

564 lines
21 KiB
JavaScript

// Isolated UI preview: synthetic data only. No upstream API or payment access.
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { gzipSync } from "node:zlib";
import { randomBytes } from "node:crypto";
const port = Number(process.env.PREVIEW_PORT || 4173);
const paused = process.env.PREVIEW_PAUSED === "true";
const states = new Map();
const attempts = new Map();
const linkJobs = new Map();
let folders = [
{ id: "a".repeat(32), name: "本地预览 · 示例批次" },
{ id: "b".repeat(32), name: "国庆活动" },
];
const plaintext = new Map();
const now = Math.floor(Date.now() / 1000);
let codes = ["active", "processing", "succeeded", "review", "revoked"].map(
(status, index) => {
// Two batches plus one unfiled code so client-side filtering is demonstrable.
const folder = index < 2 ? folders[0] : index < 4 ? folders[1] : null;
return {
id: (index + 1).toString(16).padStart(32, "0"),
folder: folder?.id ?? "",
copyable: false,
hint: `DEMO000${index}`,
batch: folder?.name ?? "",
months: index % 2 ? 3 : 6,
status,
username: index > 0 && index < 4 ? "demo_user" : "",
message: status === "review" ? "示例:结果正在核实,请勿重复兑换。" : "",
created: now - index * 3600,
};
},
);
const files = {
"/": ["index.html", "text/html"],
"/admin": ["admin.html", "text/html"],
"/appearance.js": ["appearance.js", "application/javascript"],
"/payment-notifications.js": ["payment-notifications.js", "application/javascript"],
"/app.js": ["app.js", "application/javascript"],
"/admin.js": ["admin.js", "application/javascript"],
"/favicon.svg": ["favicon.svg", "image/svg+xml"],
};
createServer(async (req, res) => {
const nonce = randomBytes(16).toString("hex");
res.setHeader("Cache-Control", "no-store");
res.setHeader(
"Content-Security-Policy",
`default-src 'none'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; style-src 'self' 'nonce-${nonce}'; style-src-attr 'unsafe-inline'; connect-src 'self'; worker-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`,
);
res.setHeader("X-Content-Type-Options", "nosniff");
const url = new URL(req.url, `http://127.0.0.1:${port}`);
function json(status, data) {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
try {
if (url.pathname === "/api/security") {
return json(200, { turnstile_enabled: process.env.PREVIEW_TURNSTILE === "true", turnstile_site_key: process.env.PREVIEW_TURNSTILE === "true" ? "1x00000000000000000000AA" : "" });
}
if (process.env.PREVIEW_TURNSTILE === "true" && req.method === "POST" && ["/api/check", "/api/redeem", "/api/manual-link"].includes(url.pathname) && !req.headers["x-turnstile-token"]) {
return json(403, { message: "请完成人机验证后重试。" });
}
if (req.method === "GET" && files[url.pathname]) {
const [file, type] = files[url.pathname];
let data = await readFile(
new URL(`../../internal/site/assets/${file}`, import.meta.url),
);
if (type === "text/html")
data = data
.toString()
.replaceAll("__XGIFT_NONCE__", nonce)
.replace("</title>", " · 本地模拟预览</title>");
res.setHeader("Vary", "Accept-Encoding");
if (/\bgzip\b/.test(req.headers["accept-encoding"] || "")) {
data = gzipSync(data, { level: 9 });
res.setHeader("Content-Encoding", "gzip");
}
res.writeHead(200, { "Content-Type": type });
res.end(data);
return;
}
if (req.method === "GET" && ["/api/admin/manual-link/plans", "/api/manual-link/plans"].includes(url.pathname)) {
json(200, { plans: [{months: 3, amount: 30000, currency: "BDT"}, {months: 6, amount: 60000, currency: "BDT"}] });
return;
}
if (req.method === "GET" && url.pathname === "/api/manual-link/queue/current") {
const latest = [...linkJobs.entries()].at(-1);
return latest ? json(200, {ticket: latest[0], username:latest[1].username, months:latest[1].months}) : json(404, {});
}
if (req.method === "POST" && /^\/api\/manual-link\/queue\/[^/]+\/leave$/.test(url.pathname)) return json(200, {leaving:true});
if (req.method === "POST" && /^\/api\/manual-link\/queue\/[^/]+\/cancel$/.test(url.pathname)) {
linkJobs.delete(url.pathname.split("/").at(-2));
return json(200, {cancelled:true});
}
if (req.method === "GET" && url.pathname.startsWith("/api/manual-link/queue/")) {
const ticket = url.pathname.split("/").pop();
const job = linkJobs.get(ticket);
if (!job) { json(404, {message: "排队记录已失效,请重新提交。"}); return; }
job.polls++;
if (job.polls < 3 || process.env.PREVIEW_QUEUE_HOLD === "true") {
json(202, {ticket, status: job.polls === 1 ? "queued" : "processing", position: 1, ahead: 0, estimated_wait_seconds: job.polls === 1 ? 20 : 10, message: job.polls === 1 ? "前方还有 0 人,预计约 20 秒后生成链接。请保持页面打开。" : "正在生成付款链接,预计还需约 10 秒。"}); return;
}
if (job.username === "expired_demo" && !job.verified_unpaid) {
json(409, {message: "原付款链接已失效,请核实原订单未付款后再重新生成。", needs_unpaid_verification: true}); return;
}
json(200, {username: job.username, months: job.months, amount: job.months === 3 ? 30000 : 60000, currency: "BDT", status: "created", checkout_url: `https://checkout.stripe.com/c/pay/cs_test_PreviewOnly${ticket}`}); return;
}
if (req.method === "GET" && url.pathname === "/healthz") {
json(200, { ok: true, payments_enabled: !paused });
return;
}
if (req.method === "GET" && url.pathname === "/api/admin/codes") {
const page = Math.max(0, Number(url.searchParams.get("page")) || 0);
const folder = url.searchParams.get("folder") || "";
if (
folder &&
folder !== "unfiled" &&
!folders.some((item) => item.id === folder)
) {
json(404, { message: "文件夹不存在,请刷新列表。" });
return;
}
const filtered = codes.filter(
(code) =>
!folder ||
(folder === "unfiled" ? !code.folder : code.folder === folder),
);
const stats = {
total: codes.length,
active: 0,
processing: 0,
succeeded: 0,
review: 0,
revoked: 0,
unfiled: codes.filter((code) => !code.folder).length,
};
for (const code of codes) stats[code.status]++;
json(200, {
codes: filtered.slice(page * 100, (page + 1) * 100),
page,
folder,
stats,
folders: folders
.map((item) => ({
...item,
count: codes.filter((code) => code.folder === item.id).length,
}))
.sort((a, b) => a.name.localeCompare(b.name)),
has_more: filtered.length > (page + 1) * 100,
payments_enabled: !paused,
});
return;
}
if (req.method !== "POST") {
if (req.method === "GET" && url.pathname === "/api/admin/recovery") {
// PREVIEW_NETWORK=direct 可预览直连模式;默认节点池。
const direct = process.env.PREVIEW_NETWORK === "direct";
json(200, {
batch: null,
network: direct
? { mode: "direct", nodes: 0 }
: { mode: "pool", nodes: 12, available: 9, cooling: 3 },
cards: [
{ last4: "4242", usable: true },
{ last4: "1881", usable: true, cooling_seconds: 1500 },
{
last4: "0005",
usable: true,
blocked: "do_not_try_again",
pair_cooling: 2,
},
{ last4: "9917", usable: false, problem: "card has expired" },
],
rotation: {
batch_size: 3,
used: 2,
card_last4: "4242",
node: direct ? "direct" : "node-1a2b3c4d5e6f",
},
paused: false,
summary: {
review: codes.filter((code) => code.status === "review").length,
processing: codes.filter((code) => code.status === "processing")
.length,
},
});
return;
}
if (req.method === "GET" && url.pathname === "/api/admin/customer") {
const key = url.searchParams.get("id") || "";
const user = (url.searchParams.get("username") || "")
.replace(/^@/, "")
.toLowerCase();
const found = codes.find(
(code) =>
(key && code.id === key) || (user && code.username === user),
);
if (!found) {
json(404, { message: "没有找到对应的订单,请核对后重试。" });
return;
}
json(200, {
order: {
id: found.id,
username: found.username,
hint: found.hint,
months: found.months,
status: found.status,
message: found.message,
batch: found.batch,
},
code: plaintext.get(found.id) || "",
checkout_url: "",
previous_checkout_url: "",
can_recover: false,
replacement_count: 0,
});
return;
}
if (req.method === "GET" && url.pathname === "/api/admin/stats") {
const daily = [];
for (let offset = 29; offset >= 0; offset--) {
const day = new Date();
day.setDate(day.getDate() - offset);
const date = [
day.getFullYear(),
String(day.getMonth() + 1).padStart(2, "0"),
String(day.getDate()).padStart(2, "0"),
].join("-");
// Deterministic synthetic series with some zero days.
const created = offset % 4 === 0 ? 0 : ((offset * 7) % 9) + 1;
const redeemed = Math.max(0, created - (offset % 3));
const succeeded = Math.max(0, redeemed - (offset % 2));
daily.push({ date, created, redeemed, succeeded });
}
json(200, {
codes: {
total: 240,
active: 62,
processing: 8,
review: 3,
succeeded: 150,
revoked: 17,
redeemed: 161,
unfiled: 12,
},
rates: { redeemed: 161 / 240, success: 150 / 161 },
months: [
{ months: 3, total: 90, succeeded: 52 },
{ months: 6, total: 150, succeeded: 98 },
],
daily,
review_stages: [
{ progress: 20, count: 1 },
{ progress: 50, count: 1 },
{ progress: 90, count: 1 },
],
});
return;
}
json(404, { message: "预览路由不存在" });
return;
}
let raw = "";
for await (const chunk of req) {
raw += chunk;
if (raw.length > 4096) {
json(413, { message: "请求过大" });
return;
}
}
const body = JSON.parse(raw);
if (["/api/admin/manual-link", "/api/manual-link"].includes(url.pathname)) {
if (![3, 6].includes(body.months) || !/^[a-z0-9_]{1,15}$/.test(body.username || "")) {
json(400, {message: "请填写正确用户名和套餐。"}); return;
}
if (url.pathname === "/api/manual-link") {
const ticket = randomBytes(12).toString("hex");
linkJobs.set(ticket, {...body, polls: 0});
json(202, {ticket, status: "queued", position: 2, ahead: 1, estimated_wait_seconds: 40, message: "前方还有 1 人,预计约 40 秒后生成链接。请保持页面打开。"}); return;
}
if (body.username === "expired_demo" && !body.verified_unpaid) {
json(409, {message: "原付款链接已失效,请核实原订单未付款后再重新生成。", needs_unpaid_verification: true}); return;
}
json(200, {username: body.username, months: body.months, amount: body.months === 3 ? 30000 : 60000, currency: "BDT", status: "created", checkout_url: "https://checkout.stripe.com/c/pay/cs_test_ManualPreviewOnly"});
return;
}
if (url.pathname.startsWith("/api/admin/recovery/")) {
json(503, { message: "本地模拟预览不提供补单操作。" });
return;
}
if (url.pathname === "/api/admin/folders" ||
url.pathname === "/api/admin/folders/rename"
) {
const name = typeof body.name === "string" ? body.name.trim() : "";
if (!name || Buffer.byteLength(name) > 120) {
json(400, { message: "请输入有效文件夹名称(最多 120 字节)。" });
return;
}
if (
folders.some(
(folder) =>
folder.name.toLowerCase() === name.toLowerCase() &&
folder.id !== body.id,
)
) {
json(409, { message: "已存在同名文件夹。" });
return;
}
if (url.pathname.endsWith("/rename")) {
const folder = folders.find((item) => item.id === body.id);
if (!folder) {
json(404, { message: "文件夹不存在。" });
return;
}
folder.name = name;
for (const code of codes)
if (code.folder === folder.id) code.batch = name;
json(200, { message: "已重命名。" });
return;
}
const folder = { id: randomBytes(16).toString("hex"), name };
folders.push(folder);
json(201, { ...folder, count: 0 });
return;
}
if (url.pathname === "/api/admin/folders/delete") {
if (!folders.some((folder) => folder.id === body.id)) {
json(404, { message: "文件夹不存在。" });
return;
}
folders = folders.filter((folder) => folder.id !== body.id);
for (const code of codes)
if (code.folder === body.id) {
code.folder = "";
code.batch = "";
}
json(200, { message: "文件夹已删除,兑换码已移至未分类。" });
return;
}
if (url.pathname === "/api/admin/codes/move") {
if (
!Array.isArray(body.ids) ||
!body.ids.length ||
body.ids.length > 100 ||
new Set(body.ids).size !== body.ids.length ||
!body.ids.every((id) => codes.some((code) => code.id === id))
) {
json(409, { message: "兑换码选择无效,请刷新列表。" });
return;
}
if (body.folder && !folders.some((folder) => folder.id === body.folder)) {
json(404, { message: "文件夹不存在。" });
return;
}
for (const code of codes)
if (body.ids.includes(code.id)) {
code.folder = body.folder || "";
code.batch = folders.find((f) => f.id === code.folder)?.name || "";
}
json(200, { message: "已更新分类。" });
return;
}
if (url.pathname === "/api/admin/codes") {
if (
![3, 6].includes(body.months) ||
!Number.isInteger(body.count) ||
body.count < 1 ||
body.count > 500 ||
typeof body.batch !== "string" ||
Buffer.byteLength(body.batch) > 120
) {
json(400, { message: "请检查套餐、数量和批次名称。" });
return;
}
if (body.folder && !folders.some((folder) => folder.id === body.folder)) {
json(404, { message: "文件夹不存在。" });
return;
}
let batch = body.batch || "本地预览-" + Date.now();
let folder = folders.find(
(f) => f.name.toLowerCase() === batch.toLowerCase(),
);
if (!folder) {
folder = { id: randomBytes(16).toString("hex"), name: batch };
folders.push(folder);
}
batch = folder.name;
body.folder = folder.id;
const generated = Array.from(
{ length: body.count },
() => "XG-" + randomBytes(24).toString("hex").toUpperCase(),
);
codes = [
...generated.map((code) => {
const id = randomBytes(16).toString("hex");
plaintext.set(id, code);
return {
id,
copyable: true,
folder: body.folder || "",
hint: code.slice(-8),
batch,
months: body.months,
status: "active",
username: "",
message: "",
created: now,
};
}),
...codes,
];
json(201, {
codes: generated,
batch,
months: body.months,
folder: body.folder || "",
});
return;
}
if (url.pathname === "/api/admin/lookup") {
const full =
typeof body.code === "string" ? body.code.trim().toUpperCase() : "";
if (!/^XG-[A-F0-9]{48}$/.test(full)) {
json(400, { message: "请填写 XG- 开头、后接 48 位字符的完整兑换码。" });
return;
}
// Fixed demo hit so a found result can be previewed without generating.
if (full === "XG-" + "5".repeat(48)) {
json(200, { ...codes[2], progress: 100, updated: now });
return;
}
const entry = [...plaintext.entries()].find(
([, value]) => value === full,
);
const found = entry && codes.find((code) => code.id === entry[0]);
json(
found ? 200 : 404,
found
? { ...found, updated: found.created, progress: 0 }
: { message: "没有找到这个兑换码,请核对后重试。" },
);
return;
}
if (url.pathname === "/api/admin/codes/copy") {
const code = plaintext.get(body.id);
json(
code ? 200 : 409,
code ? { code } : { message: "历史兑换码未保存完整内容。" },
);
return;
}
if (url.pathname === "/api/admin/revoke") {
const code = codes.find((item) => item.id === body.id);
if (!code || code.status !== "active") {
json(409, { message: "只能停用未使用的兑换码。" });
return;
}
code.status = "revoked";
json(200, { message: "兑换码已停用。" });
return;
}
if (url.pathname === "/api/check") {
if (!/^[a-z0-9_]{1,15}$/.test(body.username || "")) {
json(400, { message: "请填写正确的 X 用户名(不是显示名称)。" });
return;
}
if (body.username === "blocked_user") {
json(200, {
eligible: false,
message: "示例:X 当前不允许向这个账号赠送 Premium。",
});
return;
}
if (body.username === "busy_user") {
json(503, { message: "暂时无法向 X 核实赠送资格,请稍后重试检测。" });
return;
}
json(200, { eligible: true, message: "该账号当前可以接收赠送。" });
return;
}
if (url.pathname === "/api/redeem" || url.pathname === "/api/status") {
if (
!/^XG-[A-F0-9]{48}$/.test(body.code) ||
!/^[a-z0-9_]{1,15}$/.test(body.username)
) {
json(400, { message: "请检查兑换码和用户名。" });
return;
}
const ending = body.code.slice(-1);
if (ending === "C") {
json(422, {
message: "示例:X 当前不允许向这个账号赠送 Premium。兑换码未使用。",
});
return;
}
if (ending === "D") {
json(200, {
status: "revoked",
message: "兑换码已停用,请联系提供方。",
});
return;
}
if (req.url === "/api/redeem" && paused) {
json(503, {
status: "paused",
message: "账号可以接收赠送,但充值服务暂未开放。兑换码未使用。",
});
return;
}
const key = body.code + ":" + body.username;
if (req.url === "/api/redeem") {
states.set(key, Date.now());
attempts.set(key, (attempts.get(key) || 0) + 1);
}
if (!states.has(key)) {
json(200, { status: "active", progress: 0, months: 6 });
return;
}
const elapsed = Date.now() - states.get(key);
if (ending === "E" && attempts.get(key) === 1 && elapsed > 3000) {
json(200, {
status: "review", progress: 50, months: 6,
message: "本地模拟:原订单尚未付款,可重新检查并继续兑换。",
});
return;
}
if (ending === "B" && elapsed > 3000) {
json(200, {
status: "review",
progress: 90,
months: 6,
message: "示例:结果暂未确认,请查询原订单或联系管理员。",
});
return;
}
const progress =
ending === "F"
? 70
: Math.min(100, 20 + Math.floor(elapsed / 1500) * 20);
json(200, {
status: progress === 100 ? "succeeded" : "processing",
progress,
months: 6,
message:
progress === 100
? `本地模拟:已为 @${body.username} 完成 6 个月 Premium 赠送(未执行真实付款)。`
: "本地模拟:正在核验订单,请稍候。",
});
return;
}
json(404, { message: "预览路由不存在" });
} catch {
json(400, { message: "预览请求无法处理。" });
}
}).listen(port, "127.0.0.1", () =>
console.log(
`本地模拟预览:http://127.0.0.1:${port} /admin;所有数据均为示例,不连接生产或付款服务。`,
),
);