's'
This commit is contained in:
@@ -21,8 +21,11 @@ type PBAuthRecord = {
|
||||
|
||||
type PayjsVipEmailResult = {
|
||||
vip: boolean;
|
||||
error?: string;
|
||||
effectiveUserId?: string;
|
||||
anonymousUserId?: string;
|
||||
token?: string;
|
||||
record?: PBAuthRecord;
|
||||
};
|
||||
|
||||
type RecoveryResult = {
|
||||
@@ -69,7 +72,13 @@ async function checkVipByEmailFromPayjsapi(
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.vip) {
|
||||
return null;
|
||||
return {
|
||||
vip: false,
|
||||
error:
|
||||
typeof data?.error === "string" && data.error.trim()
|
||||
? data.error.trim()
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -82,6 +91,8 @@ async function checkVipByEmailFromPayjsapi(
|
||||
typeof data?.anonymousUserId === "string"
|
||||
? data.anonymousUserId.trim()
|
||||
: undefined,
|
||||
token: typeof data?.token === "string" ? data.token : undefined,
|
||||
record: data?.record?.id ? data.record : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("[check-by-email] payjsapi request failed:", error);
|
||||
@@ -246,18 +257,74 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const [authData, adminToken] = await Promise.all([
|
||||
loginPocketBaseUser(email, password),
|
||||
getAdminToken(),
|
||||
]);
|
||||
if (!adminToken) {
|
||||
let authData: { token: string; record: PBAuthRecord } | null = null;
|
||||
let loginError: string | null = null;
|
||||
try {
|
||||
authData = await loginPocketBaseUser(email, password);
|
||||
} catch (error) {
|
||||
loginError = error instanceof Error ? error.message : "登录失败";
|
||||
}
|
||||
|
||||
let payjsResult: PayjsVipEmailResult | null = await checkVipByEmailFromPayjsapi(
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
);
|
||||
|
||||
// PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复
|
||||
if (!authData && payjsResult?.vip && payjsResult.token && payjsResult.record?.id) {
|
||||
console.log("[check-by-email] fallback by payjsapi token email=%s", email);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip: true,
|
||||
token: payjsResult.token,
|
||||
record: payjsResult.record,
|
||||
effectiveUserId: payjsResult.record.id,
|
||||
anonymousUserId: payjsResult.anonymousUserId ?? null,
|
||||
memosAccount: payjsResult.anonymousUserId ?? null,
|
||||
source: "payjsapi_token_fallback",
|
||||
});
|
||||
}
|
||||
|
||||
if (!authData) {
|
||||
const msg = loginError || "登录失败";
|
||||
return NextResponse.json(
|
||||
{ ok: false, vip: false, error: "PocketBase 管理员认证失败" },
|
||||
{ status: 500 }
|
||||
{ ok: false, vip: false, error: msg },
|
||||
{ status: msg.includes("密码") || msg.includes("邮箱") ? 401 : 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const adminToken = await getAdminToken().catch((error) => {
|
||||
console.error("[check-by-email] getAdminToken failed:", error);
|
||||
return null;
|
||||
});
|
||||
if (!adminToken) {
|
||||
// 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500
|
||||
if (payjsResult?.vip) {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip: true,
|
||||
token: authData.token,
|
||||
record: authData.record,
|
||||
effectiveUserId: authData.record.id,
|
||||
anonymousUserId: payjsResult.anonymousUserId ?? null,
|
||||
memosAccount: payjsResult.anonymousUserId ?? null,
|
||||
source: "payjsapi_degraded",
|
||||
});
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
vip: false,
|
||||
error:
|
||||
payjsResult?.error ||
|
||||
"服务繁忙,请稍后重试",
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
let payjsResult: PayjsVipEmailResult | null = null;
|
||||
let recovery = await resolveRecovery(
|
||||
adminToken,
|
||||
email,
|
||||
@@ -267,12 +334,14 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
|
||||
if (!recovery.vip) {
|
||||
payjsResult = await checkVipByEmailFromPayjsapi(
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
);
|
||||
if (!payjsResult) {
|
||||
payjsResult = await checkVipByEmailFromPayjsapi(
|
||||
request,
|
||||
email,
|
||||
password,
|
||||
requestedAnonymousUserId || undefined
|
||||
);
|
||||
}
|
||||
recovery = await resolveRecovery(
|
||||
adminToken,
|
||||
email,
|
||||
@@ -291,18 +360,25 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
ok: false,
|
||||
vip: false,
|
||||
error:
|
||||
error: payjsResult?.error ||
|
||||
"未找到关联的会员记录,请确认已支付并绑定邮箱;如为历史匿名订单,可填写支付时的 user_id 再登录",
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackAnonymousUserId = isAnonymousUserId(requestedAnonymousUserId)
|
||||
? requestedAnonymousUserId
|
||||
: (await findAnonymousUserIdForUser(adminToken, authData.record.id)) ||
|
||||
let fallbackAnonymousUserId: string | undefined;
|
||||
if (
|
||||
isAnonymousUserId(requestedAnonymousUserId) &&
|
||||
(await hasVipByUserId(adminToken, requestedAnonymousUserId))
|
||||
) {
|
||||
fallbackAnonymousUserId = requestedAnonymousUserId;
|
||||
} else {
|
||||
fallbackAnonymousUserId =
|
||||
(await findAnonymousUserIdForUser(adminToken, authData.record.id)) ||
|
||||
(typeof payjsResult?.anonymousUserId === "string" &&
|
||||
isAnonymousUserId(payjsResult.anonymousUserId.trim())
|
||||
? payjsResult.anonymousUserId.trim()
|
||||
: undefined);
|
||||
}
|
||||
|
||||
const anonymousUserId =
|
||||
recovery.anonymousUserId || fallbackAnonymousUserId;
|
||||
|
||||
@@ -15,6 +15,29 @@ type PayjsVipResult = {
|
||||
email?: string;
|
||||
};
|
||||
|
||||
async function fetchPayjsVipWithRetry(
|
||||
url: string,
|
||||
timeoutMs: number,
|
||||
retries: number
|
||||
): Promise<Response | null> {
|
||||
let lastError: unknown = null;
|
||||
for (let i = 0; i <= retries; i += 1) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (i < retries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError instanceof Error ? lastError : new Error("payjsapi fetch failed");
|
||||
}
|
||||
|
||||
function resolvePayApiUrl(request: NextRequest): string {
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
@@ -35,10 +58,11 @@ async function checkVipFromPayjsapi(
|
||||
)}`;
|
||||
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) }
|
||||
);
|
||||
const retries = Math.max(0, Number(process.env.PAYJSAPI_CHECK_RETRIES) || 1);
|
||||
const res = await fetchPayjsVipWithRetry(url, checkTimeout, retries);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) {
|
||||
console.log(
|
||||
"[check-by-user] payjsapi non-ok user_id=%s status=%s",
|
||||
@@ -109,10 +133,18 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const adminToken = await getAdminToken();
|
||||
const adminToken = await getAdminToken().catch((error) => {
|
||||
console.error("[check-by-user] getAdminToken failed:", error);
|
||||
return null;
|
||||
});
|
||||
if (!adminToken) {
|
||||
console.error("[check-by-user] missing PocketBase admin token");
|
||||
return NextResponse.json({ ok: false, vip: false }, { status: 500 });
|
||||
console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result");
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip,
|
||||
alreadyLinked: !!email,
|
||||
email: email ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!vip) {
|
||||
@@ -135,11 +167,8 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (vip && !email && isAnonymousUserId(userId)) {
|
||||
const linkRecord = await findVipEmailLinkByAnonymousUserId(
|
||||
adminToken,
|
||||
userId
|
||||
);
|
||||
if (isAnonymousUserId(userId)) {
|
||||
const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId);
|
||||
const linkEmail =
|
||||
typeof linkRecord?.email === "string"
|
||||
? linkRecord.email.trim().toLowerCase()
|
||||
@@ -147,19 +176,34 @@ export async function POST(request: NextRequest) {
|
||||
const linkUserId =
|
||||
typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : "";
|
||||
|
||||
if (linkEmail) {
|
||||
// 匿名 user_id 本身可能已迁移到真实用户,需补查映射用户的 VIP
|
||||
if (!vip && linkUserId && linkUserId !== userId) {
|
||||
const linkedVip = await checkSiteVipByUserId(adminToken, linkUserId);
|
||||
const linkedPaymentVip = linkedVip
|
||||
? true
|
||||
: await checkPaymentsByUserId(adminToken, linkUserId);
|
||||
if (linkedVip || linkedPaymentVip) {
|
||||
vip = true;
|
||||
linkedUserId = linkUserId;
|
||||
vipSource = linkedVip ? "vip_email_link:site_vip" : "vip_email_link:payments";
|
||||
}
|
||||
}
|
||||
|
||||
if (vip && linkEmail) {
|
||||
email = linkEmail;
|
||||
if (linkUserId && !isAnonymousUserId(linkUserId)) {
|
||||
linkedUserId = linkUserId;
|
||||
}
|
||||
vipSource = "vip_email_link";
|
||||
if (!vipSource || vipSource === "none") {
|
||||
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 {
|
||||
} else if (vip) {
|
||||
const linkedMember = await findLinkedMemberFromOrder(adminToken, userId);
|
||||
if (linkedMember) {
|
||||
email = linkedMember.email;
|
||||
@@ -199,6 +243,7 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[check-by-user] failed:", error);
|
||||
return NextResponse.json({ ok: false, vip: false }, { status: 500 });
|
||||
// 接口容错:即使 PB 回退失败,也不抛 500 给前端
|
||||
return NextResponse.json({ ok: true, vip: false, error: "check_failed_degraded" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@ export function LoginForm({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const getCandidateUserId = (): string => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const fromMemos = localStorage.getItem("memosAccount")?.trim() || "";
|
||||
if (/^user\d+$/.test(fromMemos)) return fromMemos;
|
||||
const fromUserId = localStorage.getItem("userId")?.trim() || "";
|
||||
if (/^user\d+$/.test(fromUserId)) return fromUserId;
|
||||
return "";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) inputRef.current?.focus();
|
||||
}, [autoFocus, mode]);
|
||||
@@ -71,7 +80,12 @@ export function LoginForm({
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await onVerifyByEmail(trimmedEmail, trimmedPassword);
|
||||
const candidateUserId = getCandidateUserId();
|
||||
const result = await onVerifyByEmail(
|
||||
trimmedEmail,
|
||||
trimmedPassword,
|
||||
candidateUserId || undefined
|
||||
);
|
||||
const ok = typeof result === "object" ? result.ok : result;
|
||||
const errMsg = typeof result === "object" ? result.error : undefined;
|
||||
if (ok) onSuccess();
|
||||
|
||||
Reference in New Issue
Block a user