Files
gitlab-instance-0a899031_no…/app/api/vip/check-by-email/route.ts
2026-03-13 06:16:36 -05:00

328 lines
8.5 KiB
TypeScript

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) ||
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 }
);
}
}