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