This commit is contained in:
eric
2026-03-26 03:15:35 -05:00
parent 340d363196
commit 1a6f2511c4
3 changed files with 70 additions and 53 deletions

View File

@@ -32,6 +32,9 @@ type RecoveryResult = {
source?: string; source?: string;
}; };
const PAYJS_CHECK_BY_EMAIL_TIMEOUT =
Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 5000;
function resolvePayApiUrl(request: NextRequest): string { function resolvePayApiUrl(request: NextRequest): string {
const payConfig = getPaymentConfig(); const payConfig = getPaymentConfig();
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
@@ -58,7 +61,7 @@ async function checkVipByEmailFromPayjsapi(
...(userId ? { user_id: userId } : {}), ...(userId ? { user_id: userId } : {}),
}), }),
cache: "no-store", cache: "no-store",
signal: AbortSignal.timeout(15000), signal: AbortSignal.timeout(PAYJS_CHECK_BY_EMAIL_TIMEOUT),
}); });
if (!res.ok) { if (!res.ok) {
return null; return null;
@@ -121,23 +124,26 @@ async function resolveRecovery(
requestedAnonymousUserId?: string, requestedAnonymousUserId?: string,
payjsResult?: PayjsVipEmailResult | null payjsResult?: PayjsVipEmailResult | null
): Promise<RecoveryResult> { ): Promise<RecoveryResult> {
if ( if (isAnonymousUserId(requestedAnonymousUserId)) {
isAnonymousUserId(requestedAnonymousUserId) && const requestUserHasVip = await hasVipByUserId(adminToken, requestedAnonymousUserId);
(await hasVipByUserId(adminToken, requestedAnonymousUserId)) if (requestUserHasVip) {
) { return {
return { vip: true,
vip: true, matchedUserId: requestedAnonymousUserId,
matchedUserId: requestedAnonymousUserId, anonymousUserId: requestedAnonymousUserId,
anonymousUserId: requestedAnonymousUserId, source: "request_user_id",
source: "request_user_id", };
}; }
} }
const linkRecord = await findVipEmailLinkByEmail(adminToken, email); const [linkRecord, fallbackAnonymousUserId] = await Promise.all([
findVipEmailLinkByEmail(adminToken, email),
findAnonymousUserIdForUser(adminToken, pbUserId),
]);
const linkedAnonymousUserId = const linkedAnonymousUserId =
typeof linkRecord?.anonymous_user_id === "string" typeof linkRecord?.anonymous_user_id === "string"
? linkRecord.anonymous_user_id.trim() ? linkRecord.anonymous_user_id.trim()
: (await findAnonymousUserIdForUser(adminToken, pbUserId)) ?? ""; : fallbackAnonymousUserId ?? "";
const linkedIds = Array.from( const linkedIds = Array.from(
new Set( new Set(
[ [
@@ -148,24 +154,30 @@ async function resolveRecovery(
) )
); );
for (const linkedId of linkedIds) { if (linkedIds.length) {
if (!(await hasVipByUserId(adminToken, linkedId))) { const linkedVipChecks = await Promise.all(
continue; linkedIds.map(async (linkedId) => ({
linkedId,
vip: await hasVipByUserId(adminToken, linkedId),
}))
);
const hit = linkedVipChecks.find((item) => item.vip);
if (hit) {
return {
vip: true,
matchedUserId: hit.linkedId,
anonymousUserId: isAnonymousUserId(linkedAnonymousUserId)
? linkedAnonymousUserId
: isAnonymousUserId(hit.linkedId)
? hit.linkedId
: undefined,
source: "vip_email_link",
};
} }
return {
vip: true,
matchedUserId: linkedId,
anonymousUserId: isAnonymousUserId(linkedAnonymousUserId)
? linkedAnonymousUserId
: isAnonymousUserId(linkedId)
? linkedId
: undefined,
source: "vip_email_link",
};
} }
if (await hasVipByUserId(adminToken, pbUserId)) { const pbUserHasVip = await hasVipByUserId(adminToken, pbUserId);
if (pbUserHasVip) {
return { return {
vip: true, vip: true,
matchedUserId: pbUserId, matchedUserId: pbUserId,
@@ -186,22 +198,27 @@ async function resolveRecovery(
) )
); );
for (const candidate of payjsCandidates) { if (payjsCandidates.length) {
if (!(await hasVipByUserId(adminToken, candidate))) { const payjsVipChecks = await Promise.all(
continue; payjsCandidates.map(async (candidate) => ({
candidate,
vip: await hasVipByUserId(adminToken, candidate),
}))
);
const hit = payjsVipChecks.find((item) => item.vip);
if (hit) {
return {
vip: true,
matchedUserId: hit.candidate,
anonymousUserId: isAnonymousUserId(hit.candidate)
? hit.candidate
: typeof payjsResult?.anonymousUserId === "string" &&
isAnonymousUserId(payjsResult.anonymousUserId.trim())
? payjsResult.anonymousUserId.trim()
: undefined,
source: "payjsapi",
};
} }
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 }; return { vip: false };
@@ -229,8 +246,10 @@ export async function POST(request: NextRequest) {
} }
try { try {
const authData = await loginPocketBaseUser(email, password); const [authData, adminToken] = await Promise.all([
const adminToken = await getAdminToken(); loginPocketBaseUser(email, password),
getAdminToken(),
]);
if (!adminToken) { if (!adminToken) {
return NextResponse.json( return NextResponse.json(
{ ok: false, vip: false, error: "PocketBase 管理员认证失败" }, { ok: false, vip: false, error: "PocketBase 管理员认证失败" },

View File

@@ -4,6 +4,11 @@
:root, :root,
.light { .light {
--font-geist-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
--font-geist-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--background: #fafafa; --background: #fafafa;
--foreground: #171717; --foreground: #171717;
--card: #ffffff; --card: #ffffff;

View File

@@ -1,13 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "./providers/ThemeProvider"; import { ThemeProvider } from "./providers/ThemeProvider";
import { themeInitScript } from "./theme-init"; import { themeInitScript } from "./theme-init";
const geistSans = GeistSans;
const geistMono = GeistMono;
export const metadata: Metadata = { export const metadata: Metadata = {
title: "异度星球", title: "异度星球",
description: "数字游民社群", description: "数字游民社群",
@@ -20,9 +15,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="zh-CN" suppressHydrationWarning> <html lang="zh-CN" suppressHydrationWarning>
<body <body className="antialiased">
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} /> <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
<ThemeProvider>{children}</ThemeProvider> <ThemeProvider>{children}</ThemeProvider>
</body> </body>