's'
This commit is contained in:
510
app/api/vip/_helpers.ts
Normal file
510
app/api/vip/_helpers.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
import { SITE_ID, getPocketBaseConfig } from "@/config/services.config";
|
||||
|
||||
const VIP_EMAIL_LINK_COLLECTION = "vip_email_link";
|
||||
|
||||
type PBRecord = Record<string, unknown>;
|
||||
|
||||
function getPBBaseUrl(): string {
|
||||
return getPocketBaseConfig().url.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function escapeFilterValue(value: string): string {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function uniqueStrings(values: Array<string | null | undefined>): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function listRecords(
|
||||
collection: string,
|
||||
filter: string,
|
||||
adminToken: string,
|
||||
options: { perPage?: number; sort?: string } = {}
|
||||
): Promise<PBRecord[]> {
|
||||
const url = new URL(`${getPBBaseUrl()}/api/collections/${collection}/records`);
|
||||
url.searchParams.set("filter", filter);
|
||||
url.searchParams.set("perPage", String(options.perPage ?? 20));
|
||||
if (options.sort) {
|
||||
url.searchParams.set("sort", options.sort);
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return Array.isArray(data?.items) ? data.items : [];
|
||||
}
|
||||
|
||||
async function createRecord(
|
||||
collection: string,
|
||||
adminToken: string,
|
||||
body: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await fetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
async function patchRecord(
|
||||
collection: string,
|
||||
recordId: string,
|
||||
adminToken: string,
|
||||
body: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await fetch(
|
||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteRecord(
|
||||
collection: string,
|
||||
recordId: string,
|
||||
adminToken: string
|
||||
): Promise<void> {
|
||||
await fetch(
|
||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getRecord(
|
||||
collection: string,
|
||||
recordId: string,
|
||||
adminToken: string
|
||||
): Promise<PBRecord | null> {
|
||||
const res = await fetch(
|
||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await res.json().catch(() => null)) as PBRecord | null;
|
||||
}
|
||||
|
||||
function isVipExpired(expiresAt: unknown): boolean {
|
||||
if (typeof expiresAt !== "string" || !expiresAt.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(expiresAt);
|
||||
return Number.isFinite(timestamp) ? timestamp < Date.now() : false;
|
||||
}
|
||||
|
||||
export function isAnonymousUserId(
|
||||
value: string | null | undefined
|
||||
): value is string {
|
||||
return !!value && /^user\d+$/.test(value);
|
||||
}
|
||||
|
||||
export async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = getPBBaseUrl();
|
||||
const paths = [
|
||||
"/api/admins/auth-with-password",
|
||||
"/api/collections/_superusers/auth-with-password",
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (typeof data?.token === "string" && data.token) {
|
||||
return data.token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function listSiteVipByUserId(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<PBRecord[]> {
|
||||
const safeUserId = escapeFilterValue(userId);
|
||||
const safeSiteId = escapeFilterValue(SITE_ID);
|
||||
return listRecords(
|
||||
"site_vip",
|
||||
`user_id = "${safeUserId}" && site_id = "${safeSiteId}"`,
|
||||
adminToken,
|
||||
{ perPage: 500, sort: "-created" }
|
||||
);
|
||||
}
|
||||
|
||||
export async function listSiteVipByOrderId(
|
||||
adminToken: string,
|
||||
orderId: string
|
||||
): Promise<PBRecord[]> {
|
||||
const safeOrderId = escapeFilterValue(orderId);
|
||||
const safeSiteId = escapeFilterValue(SITE_ID);
|
||||
return listRecords(
|
||||
"site_vip",
|
||||
`order_id = "${safeOrderId}" && site_id = "${safeSiteId}"`,
|
||||
adminToken,
|
||||
{ perPage: 500, sort: "-created" }
|
||||
);
|
||||
}
|
||||
|
||||
async function listPaymentsByOrderId(
|
||||
adminToken: string,
|
||||
orderId: string
|
||||
): Promise<PBRecord[]> {
|
||||
const safeOrderId = escapeFilterValue(orderId);
|
||||
return listRecords(
|
||||
"payments",
|
||||
`order_id = "${safeOrderId}" && type = "vip"`,
|
||||
adminToken,
|
||||
{ perPage: 20, sort: "-created" }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getActiveSiteVipRecord(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<PBRecord | null> {
|
||||
const records = await listSiteVipByUserId(adminToken, userId);
|
||||
for (const record of records) {
|
||||
if (!isVipExpired(record.expires_at)) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function checkSiteVipByUserId(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
return !!(await getActiveSiteVipRecord(adminToken, userId));
|
||||
}
|
||||
|
||||
export async function getLatestVipPayment(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<PBRecord | null> {
|
||||
const safeUserId = escapeFilterValue(userId);
|
||||
const records = await listRecords(
|
||||
"payments",
|
||||
`user_id = "${safeUserId}" && type = "vip"`,
|
||||
adminToken,
|
||||
{ perPage: 1, sort: "-created" }
|
||||
);
|
||||
return records[0] ?? null;
|
||||
}
|
||||
|
||||
export async function checkPaymentsByUserId(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
return !!(await getLatestVipPayment(adminToken, userId));
|
||||
}
|
||||
|
||||
export async function hasVipByUserId(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
if (await checkSiteVipByUserId(adminToken, userId)) {
|
||||
return true;
|
||||
}
|
||||
return checkPaymentsByUserId(adminToken, userId);
|
||||
}
|
||||
|
||||
export async function ensureSiteVipForUser(
|
||||
adminToken: string,
|
||||
fromUserId: string,
|
||||
toUserId: string
|
||||
): Promise<void> {
|
||||
if (!fromUserId || !toUserId || fromUserId === toUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceRecords = await listSiteVipByUserId(adminToken, fromUserId);
|
||||
const targetRecords = await listSiteVipByUserId(adminToken, toUserId);
|
||||
const latestPayment = await getLatestVipPayment(adminToken, fromUserId);
|
||||
const orderId =
|
||||
typeof latestPayment?.order_id === "string" ? latestPayment.order_id.trim() : "";
|
||||
const orderRecords = orderId
|
||||
? await listSiteVipByOrderId(adminToken, orderId)
|
||||
: [];
|
||||
|
||||
const canonicalRecord =
|
||||
targetRecords[0] ?? sourceRecords[0] ?? orderRecords[0] ?? null;
|
||||
const canonicalRecordId =
|
||||
typeof canonicalRecord?.id === "string" ? canonicalRecord.id : "";
|
||||
|
||||
if (canonicalRecordId) {
|
||||
const nextOrderId =
|
||||
orderId ||
|
||||
(typeof canonicalRecord?.order_id === "string"
|
||||
? canonicalRecord.order_id.trim()
|
||||
: "");
|
||||
|
||||
const body: Record<string, unknown> = { user_id: toUserId, site_id: SITE_ID };
|
||||
if (nextOrderId) {
|
||||
body.order_id = nextOrderId;
|
||||
}
|
||||
|
||||
await patchRecord("site_vip", canonicalRecordId, adminToken, body);
|
||||
|
||||
const duplicateIds = new Set<string>();
|
||||
for (const record of [...targetRecords, ...sourceRecords, ...orderRecords]) {
|
||||
const recordId = typeof record.id === "string" ? record.id : "";
|
||||
if (recordId && recordId !== canonicalRecordId) {
|
||||
duplicateIds.add(recordId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const recordId of duplicateIds) {
|
||||
await deleteRecord("site_vip", recordId, adminToken);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createRecord("site_vip", adminToken, {
|
||||
user_id: toUserId,
|
||||
site_id: SITE_ID,
|
||||
order_id: orderId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertVipEmailLink(
|
||||
adminToken: string,
|
||||
email: string,
|
||||
userId: string,
|
||||
anonymousUserId?: string
|
||||
): Promise<void> {
|
||||
if (!email || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const safeEmail = escapeFilterValue(normalizedEmail);
|
||||
const records = await listRecords(
|
||||
VIP_EMAIL_LINK_COLLECTION,
|
||||
`email = "${safeEmail}"`,
|
||||
adminToken,
|
||||
{ perPage: 1 }
|
||||
);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
email: normalizedEmail,
|
||||
user_id: userId,
|
||||
};
|
||||
if (anonymousUserId) {
|
||||
body.anonymous_user_id = anonymousUserId;
|
||||
}
|
||||
|
||||
const recordId = typeof records[0]?.id === "string" ? records[0].id : "";
|
||||
if (recordId) {
|
||||
await patchRecord(VIP_EMAIL_LINK_COLLECTION, recordId, adminToken, body);
|
||||
return;
|
||||
}
|
||||
|
||||
await createRecord(VIP_EMAIL_LINK_COLLECTION, adminToken, body);
|
||||
}
|
||||
|
||||
export async function findVipEmailLinkByEmail(
|
||||
adminToken: string,
|
||||
email: string
|
||||
): Promise<PBRecord | null> {
|
||||
const candidates = uniqueStrings([email, email.toLowerCase()]);
|
||||
for (const candidate of candidates) {
|
||||
const safeEmail = escapeFilterValue(candidate);
|
||||
const records = await listRecords(
|
||||
VIP_EMAIL_LINK_COLLECTION,
|
||||
`email = "${safeEmail}"`,
|
||||
adminToken,
|
||||
{ perPage: 1 }
|
||||
);
|
||||
if (records[0]) {
|
||||
return records[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function findVipEmailLinkByUserId(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<PBRecord | null> {
|
||||
const safeUserId = escapeFilterValue(userId);
|
||||
const records = await listRecords(
|
||||
VIP_EMAIL_LINK_COLLECTION,
|
||||
`user_id = "${safeUserId}"`,
|
||||
adminToken,
|
||||
{ perPage: 1 }
|
||||
);
|
||||
return records[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findVipEmailLinkByAnonymousUserId(
|
||||
adminToken: string,
|
||||
anonymousUserId: string
|
||||
): Promise<PBRecord | null> {
|
||||
const filters = uniqueStrings([
|
||||
`anonymous_user_id = "${escapeFilterValue(anonymousUserId)}"`,
|
||||
isAnonymousUserId(anonymousUserId)
|
||||
? `user_id = "${escapeFilterValue(anonymousUserId)}"`
|
||||
: null,
|
||||
]);
|
||||
|
||||
for (const filter of filters) {
|
||||
const records = await listRecords(
|
||||
VIP_EMAIL_LINK_COLLECTION,
|
||||
filter,
|
||||
adminToken,
|
||||
{ perPage: 1 }
|
||||
);
|
||||
if (records[0]) {
|
||||
return records[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getUserEmailById(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<string | null> {
|
||||
const record = await getRecord("users", userId, adminToken);
|
||||
const email =
|
||||
typeof record?.email === "string" ? record.email.trim().toLowerCase() : "";
|
||||
return email || null;
|
||||
}
|
||||
|
||||
export async function findLinkedMemberFromOrder(
|
||||
adminToken: string,
|
||||
anonymousUserId: string
|
||||
): Promise<{ userId: string; email: string } | null> {
|
||||
const safeUserId = escapeFilterValue(anonymousUserId);
|
||||
const payments = await listRecords(
|
||||
"payments",
|
||||
`user_id = "${safeUserId}" && type = "vip"`,
|
||||
adminToken,
|
||||
{ perPage: 20, sort: "-created" }
|
||||
);
|
||||
|
||||
const seenOrders = new Set<string>();
|
||||
for (const payment of payments) {
|
||||
const orderId =
|
||||
typeof payment.order_id === "string" ? payment.order_id.trim() : "";
|
||||
if (!orderId || seenOrders.has(orderId)) {
|
||||
continue;
|
||||
}
|
||||
seenOrders.add(orderId);
|
||||
|
||||
const siteVipRecords = await listSiteVipByOrderId(adminToken, orderId);
|
||||
for (const record of siteVipRecords) {
|
||||
const linkedUserId =
|
||||
typeof record.user_id === "string" ? record.user_id.trim() : "";
|
||||
if (
|
||||
!linkedUserId ||
|
||||
linkedUserId === anonymousUserId ||
|
||||
isAnonymousUserId(linkedUserId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const email = await getUserEmailById(adminToken, linkedUserId);
|
||||
if (email) {
|
||||
return { userId: linkedUserId, email };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function findAnonymousUserIdForUser(
|
||||
adminToken: string,
|
||||
userId: string
|
||||
): Promise<string | null> {
|
||||
if (isAnonymousUserId(userId)) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
const linkRecord = await findVipEmailLinkByUserId(adminToken, userId);
|
||||
const linkedAnonymousUserId =
|
||||
typeof linkRecord?.anonymous_user_id === "string"
|
||||
? linkRecord.anonymous_user_id.trim()
|
||||
: "";
|
||||
if (isAnonymousUserId(linkedAnonymousUserId)) {
|
||||
return linkedAnonymousUserId;
|
||||
}
|
||||
|
||||
const siteVipRecords = await listSiteVipByUserId(adminToken, userId);
|
||||
for (const record of siteVipRecords) {
|
||||
const orderId =
|
||||
typeof record.order_id === "string" ? record.order_id.trim() : "";
|
||||
if (!orderId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payments = await listPaymentsByOrderId(adminToken, orderId);
|
||||
for (const payment of payments) {
|
||||
const anonymousUserId =
|
||||
typeof payment.user_id === "string" ? payment.user_id.trim() : "";
|
||||
if (isAnonymousUserId(anonymousUserId)) {
|
||||
return anonymousUserId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
329
app/api/vip/check-by-email/route.ts
Normal file
329
app/api/vip/check-by-email/route.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getApiUrlFromRequest,
|
||||
getPaymentConfig,
|
||||
getPocketBaseConfig,
|
||||
} from "@/config/services.config";
|
||||
import {
|
||||
ensureSiteVipForUser,
|
||||
findAnonymousUserIdForUser,
|
||||
findVipEmailLinkByEmail,
|
||||
getAdminToken,
|
||||
hasVipByUserId,
|
||||
isAnonymousUserId,
|
||||
upsertVipEmailLink,
|
||||
} from "@/app/api/vip/_helpers";
|
||||
|
||||
type PBAuthRecord = {
|
||||
id: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
type PayjsVipEmailResult = {
|
||||
vip: boolean;
|
||||
effectiveUserId?: string;
|
||||
anonymousUserId?: string;
|
||||
};
|
||||
|
||||
type RecoveryResult = {
|
||||
vip: boolean;
|
||||
matchedUserId?: string;
|
||||
anonymousUserId?: string;
|
||||
source?: 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) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
async function checkVipByEmailFromPayjsapi(
|
||||
request: NextRequest,
|
||||
email: string,
|
||||
password: string,
|
||||
userId?: string
|
||||
): Promise<PayjsVipEmailResult | null> {
|
||||
try {
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const res = await fetch(`${apiUrl}/nomadvip/check_vip_by_email`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
...(userId ? { user_id: userId } : {}),
|
||||
}),
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.vip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
vip: true,
|
||||
effectiveUserId:
|
||||
typeof data?.effectiveUserId === "string"
|
||||
? data.effectiveUserId.trim()
|
||||
: undefined,
|
||||
anonymousUserId:
|
||||
typeof data?.anonymousUserId === "string"
|
||||
? data.anonymousUserId.trim()
|
||||
: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("[check-by-email] payjsapi request failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginPocketBaseUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ token: string; record: PBAuthRecord }> {
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/collections/users/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({}));
|
||||
throw new Error(error?.message || "邮箱或密码错误");
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.token || !data?.record?.id) {
|
||||
throw new Error("登录失败");
|
||||
}
|
||||
|
||||
return {
|
||||
token: data.token,
|
||||
record: data.record,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRecovery(
|
||||
adminToken: string,
|
||||
email: string,
|
||||
pbUserId: string,
|
||||
requestedAnonymousUserId?: string,
|
||||
payjsResult?: PayjsVipEmailResult | null
|
||||
): Promise<RecoveryResult> {
|
||||
if (
|
||||
isAnonymousUserId(requestedAnonymousUserId) &&
|
||||
(await hasVipByUserId(adminToken, requestedAnonymousUserId))
|
||||
) {
|
||||
return {
|
||||
vip: true,
|
||||
matchedUserId: requestedAnonymousUserId,
|
||||
anonymousUserId: requestedAnonymousUserId,
|
||||
source: "request_user_id",
|
||||
};
|
||||
}
|
||||
|
||||
const linkRecord = await findVipEmailLinkByEmail(adminToken, email);
|
||||
const linkedAnonymousUserId =
|
||||
typeof linkRecord?.anonymous_user_id === "string"
|
||||
? linkRecord.anonymous_user_id.trim()
|
||||
: (await findAnonymousUserIdForUser(adminToken, pbUserId)) ?? "";
|
||||
const linkedIds = Array.from(
|
||||
new Set(
|
||||
[
|
||||
linkedAnonymousUserId,
|
||||
typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : "",
|
||||
pbUserId,
|
||||
].filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
for (const linkedId of linkedIds) {
|
||||
if (!(await hasVipByUserId(adminToken, linkedId))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
vip: true,
|
||||
matchedUserId: linkedId,
|
||||
anonymousUserId: isAnonymousUserId(linkedAnonymousUserId)
|
||||
? linkedAnonymousUserId
|
||||
: isAnonymousUserId(linkedId)
|
||||
? linkedId
|
||||
: undefined,
|
||||
source: "vip_email_link",
|
||||
};
|
||||
}
|
||||
|
||||
if (await hasVipByUserId(adminToken, pbUserId)) {
|
||||
return {
|
||||
vip: true,
|
||||
matchedUserId: pbUserId,
|
||||
source: "pb_user_id",
|
||||
};
|
||||
}
|
||||
|
||||
const payjsCandidates = Array.from(
|
||||
new Set(
|
||||
[
|
||||
typeof payjsResult?.effectiveUserId === "string"
|
||||
? payjsResult.effectiveUserId.trim()
|
||||
: "",
|
||||
typeof payjsResult?.anonymousUserId === "string"
|
||||
? payjsResult.anonymousUserId.trim()
|
||||
: "",
|
||||
].filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
for (const candidate of payjsCandidates) {
|
||||
if (!(await hasVipByUserId(adminToken, candidate))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
vip: true,
|
||||
matchedUserId: candidate,
|
||||
anonymousUserId: isAnonymousUserId(candidate)
|
||||
? candidate
|
||||
: typeof payjsResult?.anonymousUserId === "string" &&
|
||||
isAnonymousUserId(payjsResult.anonymousUserId.trim())
|
||||
? payjsResult.anonymousUserId.trim()
|
||||
: undefined,
|
||||
source: "payjsapi",
|
||||
};
|
||||
}
|
||||
|
||||
return { vip: false };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const email = String(body?.email ?? "").trim().toLowerCase();
|
||||
const password = String(body?.password ?? "").trim();
|
||||
const requestedAnonymousUserId = String(
|
||||
body?.user_id ?? body?.anonymousUserId ?? ""
|
||||
).trim();
|
||||
|
||||
console.log(
|
||||
"[check-by-email] request email=%s user_id=%s",
|
||||
email || "(empty)",
|
||||
requestedAnonymousUserId || "(empty)"
|
||||
);
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, vip: false, error: "请填写邮箱和密码" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const authData = await loginPocketBaseUser(email, password);
|
||||
const adminToken = await getAdminToken();
|
||||
if (!adminToken) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, vip: false, error: "PocketBase 管理员认证失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
let payjsResult: PayjsVipEmailResult | null = null;
|
||||
let recovery = await resolveRecovery(
|
||||
adminToken,
|
||||
email,
|
||||
authData.record.id,
|
||||
requestedAnonymousUserId || undefined,
|
||||
payjsResult
|
||||
);
|
||||
|
||||
if (!recovery.vip) {
|
||||
payjsResult = await checkVipByEmailFromPayjsapi(
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
);
|
||||
recovery = await resolveRecovery(
|
||||
adminToken,
|
||||
email,
|
||||
authData.record.id,
|
||||
requestedAnonymousUserId || undefined,
|
||||
payjsResult
|
||||
);
|
||||
}
|
||||
|
||||
if (!recovery.vip) {
|
||||
console.log(
|
||||
"[check-by-email] no-vip email=%s pbUserId=%s",
|
||||
email,
|
||||
authData.record.id
|
||||
);
|
||||
return NextResponse.json({
|
||||
ok: false,
|
||||
vip: false,
|
||||
error:
|
||||
"未找到关联的会员记录,请确认已支付并绑定邮箱;如为历史匿名订单,可填写支付时的 user_id 再登录",
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackAnonymousUserId = isAnonymousUserId(requestedAnonymousUserId)
|
||||
? requestedAnonymousUserId
|
||||
: (await findAnonymousUserIdForUser(adminToken, authData.record.id)) ||
|
||||
(typeof payjsResult?.anonymousUserId === "string" &&
|
||||
isAnonymousUserId(payjsResult.anonymousUserId.trim())
|
||||
? payjsResult.anonymousUserId.trim()
|
||||
: undefined);
|
||||
|
||||
const anonymousUserId =
|
||||
recovery.anonymousUserId || fallbackAnonymousUserId;
|
||||
|
||||
if (isAnonymousUserId(anonymousUserId)) {
|
||||
await ensureSiteVipForUser(adminToken, anonymousUserId, authData.record.id);
|
||||
}
|
||||
|
||||
await upsertVipEmailLink(
|
||||
adminToken,
|
||||
email,
|
||||
authData.record.id,
|
||||
anonymousUserId
|
||||
);
|
||||
|
||||
console.log(
|
||||
"[check-by-email] success source=%s pbUserId=%s anonymousUserId=%s",
|
||||
recovery.source ?? "pocketbase",
|
||||
authData.record.id,
|
||||
anonymousUserId ?? "(none)"
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip: true,
|
||||
token: authData.token,
|
||||
record: authData.record,
|
||||
effectiveUserId: authData.record.id,
|
||||
anonymousUserId: anonymousUserId ?? null,
|
||||
memosAccount: anonymousUserId ?? null,
|
||||
source: recovery.source ?? "pocketbase",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "登录失败";
|
||||
const status = message.includes("密码") || message.includes("邮箱") ? 401 : 500;
|
||||
return NextResponse.json(
|
||||
{ ok: false, vip: false, error: message },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
}
|
||||
205
app/api/vip/check-by-user/route.ts
Normal file
205
app/api/vip/check-by-user/route.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
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) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
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 res = await fetch(
|
||||
url,
|
||||
{ cache: "no-store", signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,85 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig, SITE_ID } from "@/config/services.config";
|
||||
import { COOKIE_NAME } from "@/app/lib/auth-cookie";
|
||||
import {
|
||||
findVipEmailLinkByUserId,
|
||||
getActiveSiteVipRecord,
|
||||
getAdminToken,
|
||||
hasVipByUserId,
|
||||
isAnonymousUserId,
|
||||
} from "@/app/api/vip/_helpers";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
type SessionPayload = {
|
||||
userId?: string;
|
||||
anonymousUserId?: string;
|
||||
};
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) return null;
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/admins/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.token ?? null;
|
||||
function readSessionPayload(request: NextRequest): SessionPayload | null {
|
||||
const raw = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(Buffer.from(raw, "base64url").toString("utf-8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (!cookie) {
|
||||
const payload = readSessionPayload(request);
|
||||
const userId =
|
||||
typeof payload?.userId === "string" ? payload.userId.trim() : "";
|
||||
const anonymousUserId =
|
||||
typeof payload?.anonymousUserId === "string"
|
||||
? payload.anonymousUserId.trim()
|
||||
: "";
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||||
const userId = payload?.userId;
|
||||
if (!userId) return NextResponse.json({ ok: true, vip: false });
|
||||
const adminToken = await getAdminToken();
|
||||
if (!adminToken) {
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
}
|
||||
|
||||
const token = await getAdminToken();
|
||||
if (!token) return NextResponse.json({ ok: true, vip: false });
|
||||
const userRecord = await getActiveSiteVipRecord(adminToken, userId);
|
||||
if (userRecord) {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip: true,
|
||||
expires_at:
|
||||
typeof userRecord.expires_at === "string" ? userRecord.expires_at : null,
|
||||
});
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const filter = `user_id = "${userId}" && site_id = "${SITE_ID}"`;
|
||||
const res = await fetch(
|
||||
`${pb.url}/api/collections/site_vip/records?filter=${encodeURIComponent(filter)}&perPage=1`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
if (!res.ok) return NextResponse.json({ ok: true, vip: false });
|
||||
const data = await res.json();
|
||||
const items = data?.items ?? [];
|
||||
const record = items[0];
|
||||
const now = new Date().toISOString();
|
||||
const isExpired = record?.expires_at && record.expires_at < now;
|
||||
const vip = !!record && !isExpired;
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip,
|
||||
expires_at: record?.expires_at ?? null,
|
||||
});
|
||||
if (await hasVipByUserId(adminToken, userId)) {
|
||||
return NextResponse.json({ ok: true, vip: true, expires_at: null });
|
||||
}
|
||||
|
||||
const candidates = isAnonymousUserId(anonymousUserId)
|
||||
? [anonymousUserId]
|
||||
: [];
|
||||
if (!isAnonymousUserId(userId)) {
|
||||
const linkRecord = await findVipEmailLinkByUserId(adminToken, userId);
|
||||
const linkedAnonymousUserId =
|
||||
typeof linkRecord?.anonymous_user_id === "string"
|
||||
? linkRecord.anonymous_user_id.trim()
|
||||
: "";
|
||||
if (isAnonymousUserId(linkedAnonymousUserId)) {
|
||||
candidates.push(linkedAnonymousUserId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (await hasVipByUserId(adminToken, candidate)) {
|
||||
return NextResponse.json({ ok: true, vip: true, expires_at: null });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
} catch {
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
}
|
||||
|
||||
117
app/api/vip/linked-email/route.ts
Normal file
117
app/api/vip/linked-email/route.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
|
||||
import {
|
||||
findAnonymousUserIdForUser,
|
||||
findLinkedMemberFromOrder,
|
||||
findVipEmailLinkByAnonymousUserId,
|
||||
findVipEmailLinkByUserId,
|
||||
getAdminToken,
|
||||
isAnonymousUserId,
|
||||
upsertVipEmailLink,
|
||||
} from "@/app/api/vip/_helpers";
|
||||
|
||||
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) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
async function getLinkedEmailFromPayjsapi(
|
||||
request: NextRequest,
|
||||
userId: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const res = await fetch(
|
||||
`${apiUrl}/nomadvip/check_vip?user_id=${encodeURIComponent(userId)}`,
|
||||
{ cache: "no-store", signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const email =
|
||||
typeof data?.email === "string" ? data.email.trim().toLowerCase() : "";
|
||||
return data?.vip && email ? email : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const userId = request.nextUrl.searchParams.get("user_id")?.trim() || "";
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ email: null, error: "缺少 user_id" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const adminToken = await getAdminToken();
|
||||
if (!adminToken) {
|
||||
return NextResponse.json({ email: null }, { status: 500 });
|
||||
}
|
||||
|
||||
const linkRecord = isAnonymousUserId(userId)
|
||||
? await findVipEmailLinkByAnonymousUserId(adminToken, userId)
|
||||
: await findVipEmailLinkByUserId(adminToken, userId);
|
||||
const linkedEmail =
|
||||
typeof linkRecord?.email === "string"
|
||||
? linkRecord.email.trim().toLowerCase()
|
||||
: "";
|
||||
const anonymousUserId =
|
||||
typeof linkRecord?.anonymous_user_id === "string" &&
|
||||
isAnonymousUserId(linkRecord.anonymous_user_id.trim())
|
||||
? linkRecord.anonymous_user_id.trim()
|
||||
: !isAnonymousUserId(userId)
|
||||
? await findAnonymousUserIdForUser(adminToken, userId)
|
||||
: isAnonymousUserId(userId)
|
||||
? userId
|
||||
: null;
|
||||
if (linkedEmail) {
|
||||
return NextResponse.json({
|
||||
email: linkedEmail,
|
||||
anonymousUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isAnonymousUserId(userId)) {
|
||||
const linkedMember = await findLinkedMemberFromOrder(adminToken, userId);
|
||||
if (linkedMember) {
|
||||
await upsertVipEmailLink(
|
||||
adminToken,
|
||||
linkedMember.email,
|
||||
linkedMember.userId,
|
||||
userId
|
||||
);
|
||||
return NextResponse.json({
|
||||
email: linkedMember.email,
|
||||
anonymousUserId: userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const payjsEmail = await getLinkedEmailFromPayjsapi(request, userId);
|
||||
if (payjsEmail) {
|
||||
if (anonymousUserId && !isAnonymousUserId(userId)) {
|
||||
await upsertVipEmailLink(adminToken, payjsEmail, userId, anonymousUserId);
|
||||
}
|
||||
return NextResponse.json({
|
||||
email: payjsEmail,
|
||||
anonymousUserId: anonymousUserId ?? (isAnonymousUserId(userId) ? userId : null),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ email: null, anonymousUserId: anonymousUserId ?? null });
|
||||
} catch (error) {
|
||||
console.error("[linked-email] failed:", error);
|
||||
return NextResponse.json({ email: null, anonymousUserId: null }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user