205 lines
5.6 KiB
TypeScript
205 lines
5.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
|
|
import {
|
|
checkPaymentsByUserId,
|
|
checkSiteVipByUserId,
|
|
findLinkedMemberFromOrder,
|
|
findVipEmailLinkByAnonymousUserId,
|
|
getAdminToken,
|
|
isAnonymousUserId,
|
|
upsertVipEmailLink,
|
|
} from "@/app/api/vip/_helpers";
|
|
|
|
type PayjsVipResult = {
|
|
vip: boolean;
|
|
email?: string;
|
|
};
|
|
|
|
function resolvePayApiUrl(request: NextRequest): string {
|
|
const payConfig = getPaymentConfig();
|
|
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
|
return (
|
|
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
|
payConfig.apiUrl
|
|
).replace(/\/$/, "");
|
|
}
|
|
|
|
async function checkVipFromPayjsapi(
|
|
request: NextRequest,
|
|
userId: string
|
|
): Promise<PayjsVipResult | null> {
|
|
try {
|
|
const apiUrl = resolvePayApiUrl(request);
|
|
const url = `${apiUrl}/nomadvip/check_vip?user_id=${encodeURIComponent(
|
|
userId
|
|
)}`;
|
|
console.log("[check-by-user] payjsapi request user_id=%s url=%s", userId, url);
|
|
const checkTimeout = Number(process.env.PAYJSAPI_CHECK_TIMEOUT) || 15000;
|
|
const res = await fetch(
|
|
url,
|
|
{ cache: "no-store", signal: AbortSignal.timeout(checkTimeout) }
|
|
);
|
|
if (!res.ok) {
|
|
console.log(
|
|
"[check-by-user] payjsapi non-ok user_id=%s status=%s",
|
|
userId,
|
|
res.status
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
const result = {
|
|
vip: !!data?.vip,
|
|
email:
|
|
typeof data?.email === "string" && data.email.trim()
|
|
? data.email.trim().toLowerCase()
|
|
: undefined,
|
|
};
|
|
console.log(
|
|
"[check-by-user] payjsapi response user_id=%s vip=%s email=%s",
|
|
userId,
|
|
result.vip,
|
|
result.email ?? "(none)"
|
|
);
|
|
return result;
|
|
} catch (error) {
|
|
console.log(
|
|
"[check-by-user] payjsapi request failed user_id=%s error=%s",
|
|
userId,
|
|
error instanceof Error ? error.message : String(error)
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const body = await request.json().catch(() => ({}));
|
|
const userId = String(body?.user_id ?? body?.userId ?? "").trim();
|
|
|
|
console.log("[check-by-user] request user_id=%s", userId || "(empty)");
|
|
|
|
if (!userId) {
|
|
return NextResponse.json(
|
|
{ ok: false, vip: false, error: "缺少 user_id" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const payjsResult = await checkVipFromPayjsapi(request, userId);
|
|
let vip = !!payjsResult?.vip;
|
|
let email = payjsResult?.email ?? null;
|
|
let linkedUserId: string | undefined;
|
|
let vipFromSite = false;
|
|
let vipFromPayments = false;
|
|
let vipSource = vip ? "payjsapi" : "none";
|
|
|
|
try {
|
|
if (vip) {
|
|
console.log(
|
|
"[check-by-user] hit payjsapi user_id=%s alreadyLinked=%s",
|
|
userId,
|
|
!!email
|
|
);
|
|
return NextResponse.json({
|
|
ok: true,
|
|
vip: true,
|
|
alreadyLinked: !!email,
|
|
email: email ?? undefined,
|
|
});
|
|
}
|
|
|
|
const adminToken = await getAdminToken();
|
|
if (!adminToken) {
|
|
console.error("[check-by-user] missing PocketBase admin token");
|
|
return NextResponse.json({ ok: false, vip: false }, { status: 500 });
|
|
}
|
|
|
|
if (!vip) {
|
|
vipFromSite = await checkSiteVipByUserId(adminToken, userId);
|
|
vip = vipFromSite;
|
|
if (!vip) {
|
|
vipFromPayments = await checkPaymentsByUserId(adminToken, userId);
|
|
vip = vipFromPayments;
|
|
}
|
|
if (vipFromSite) {
|
|
vipSource = "pocketbase:site_vip";
|
|
} else if (vipFromPayments) {
|
|
vipSource = "pocketbase:payments";
|
|
}
|
|
console.log(
|
|
"[check-by-user] pb fallback user_id=%s site_vip=%s payments=%s",
|
|
userId,
|
|
vipFromSite,
|
|
vipFromPayments
|
|
);
|
|
}
|
|
|
|
if (vip && !email && isAnonymousUserId(userId)) {
|
|
const linkRecord = await findVipEmailLinkByAnonymousUserId(
|
|
adminToken,
|
|
userId
|
|
);
|
|
const linkEmail =
|
|
typeof linkRecord?.email === "string"
|
|
? linkRecord.email.trim().toLowerCase()
|
|
: "";
|
|
const linkUserId =
|
|
typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : "";
|
|
|
|
if (linkEmail) {
|
|
email = linkEmail;
|
|
if (linkUserId && !isAnonymousUserId(linkUserId)) {
|
|
linkedUserId = linkUserId;
|
|
}
|
|
vipSource = "vip_email_link";
|
|
console.log(
|
|
"[check-by-user] linked by vip_email_link user_id=%s linkedUserId=%s email=%s",
|
|
userId,
|
|
linkedUserId || "(none)",
|
|
email
|
|
);
|
|
} else {
|
|
const linkedMember = await findLinkedMemberFromOrder(adminToken, userId);
|
|
if (linkedMember) {
|
|
email = linkedMember.email;
|
|
linkedUserId = linkedMember.userId;
|
|
await upsertVipEmailLink(
|
|
adminToken,
|
|
linkedMember.email,
|
|
linkedMember.userId,
|
|
userId
|
|
);
|
|
vipSource = "linked_from_order";
|
|
console.log(
|
|
"[check-by-user] linked by order user_id=%s linkedUserId=%s email=%s",
|
|
userId,
|
|
linkedUserId,
|
|
email
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
"[check-by-user] result user_id=%s vip=%s source=%s alreadyLinked=%s linkedUserId=%s",
|
|
userId,
|
|
vip,
|
|
vipSource,
|
|
!!email,
|
|
linkedUserId || "(none)"
|
|
);
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
vip,
|
|
alreadyLinked: !!email,
|
|
email: email ?? undefined,
|
|
linkedUserId,
|
|
});
|
|
} catch (error) {
|
|
console.error("[check-by-user] failed:", error);
|
|
return NextResponse.json({ ok: false, vip: false }, { status: 500 });
|
|
}
|
|
}
|