Speed up entitlement auth sync

This commit is contained in:
2569718930@qq.com
2026-06-01 22:36:52 +08:00
parent fc0a8b8ff5
commit 9d136a337c
11 changed files with 334 additions and 42 deletions
@@ -0,0 +1,67 @@
import {
buildAuthMePath,
mergeAccountAuthSnapshot,
} from "@/lib/auth-snapshot";
import type { AuthSnapshotLike } from "@/lib/auth-snapshot";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
export function runTests() {
assert(
buildAuthMePath({ scope: "entitlement" }) ===
"/api/auth/me?scope=entitlement",
"account entitlement probe must use the lightweight auth/me scope",
);
assert(
buildAuthMePath({ preferSnapshot: true, scope: "entitlement" }) ===
"/api/auth/me?prefer_snapshot=1&scope=entitlement",
"terminal auth probe must combine snapshot preference with lightweight entitlement scope",
);
const active: AuthSnapshotLike & { referral?: { code?: string } } = {
authenticated: true,
user_id: "user-1",
subscription_active: true,
subscription_plan_code: "pro_monthly",
subscription_expires_at: "2026-07-01T00:00:00Z",
subscription_total_expires_at: "2026-07-01T00:00:00Z",
subscription_queued_days: 0,
points: 120,
referral: { code: "PW-ABC" },
};
const degraded = mergeAccountAuthSnapshot(active, {
authenticated: true,
user_id: "user-1",
subscription_active: null,
subscription_plan_code: null,
subscription_expires_at: null,
subscription_total_expires_at: null,
subscription_queued_days: 0,
points: 0,
degraded_auth_profile: true,
});
assert(
degraded.subscription_active === true &&
degraded.subscription_plan_code === "pro_monthly" &&
degraded.points === 120 &&
degraded.referral?.code === "PW-ABC",
"account snapshot merge must preserve confirmed Pro status when a later auth/me response is degraded",
);
const inactive = mergeAccountAuthSnapshot(active, {
authenticated: true,
user_id: "user-1",
subscription_active: false,
subscription_plan_code: null,
points: 0,
});
assert(
inactive.subscription_active === false,
"account snapshot merge must still accept a confirmed inactive subscription response",
);
}
@@ -299,9 +299,14 @@ export function runTests() {
authMeRouteSource.includes("!auth.authUserId") &&
authMeRouteSource.includes('req.headers.get("authorization")') &&
authMeRouteSource.indexOf("authenticated: false") <
authMeRouteSource.indexOf("await fetch(`${API_BASE}/api/auth/me`"),
authMeRouteSource.indexOf("await fetch(buildBackendAuthMeUrl(req)"),
"auth profile proxy must return unauthenticated locally for no-session Supabase requests instead of forwarding the backend entitlement token",
);
assert(
authMeRouteSource.includes("function buildBackendAuthMeUrl") &&
authMeRouteSource.includes('url.searchParams.set("scope", "entitlement")'),
"auth profile proxy must forward the lightweight entitlement scope to the backend auth/me endpoint",
);
assert(
authMeRouteSource.includes('reason: "prefer_snapshot_fast_path"') &&
authMeRouteSource.indexOf('reason: "prefer_snapshot_fast_path"') <
+4
View File
@@ -19,6 +19,10 @@ export type AuthMeResponse = {
subscription_queued_count?: number | null;
telegram_pricing?: TelegramPricing | null;
referral?: ReferralSummary | null;
degraded_auth_profile?: boolean | null;
degraded_reason?: string | null;
entitlement_snapshot?: boolean | null;
entitlement_snapshot_reason?: string | null;
};
export type ReferralSummary = {
@@ -1,7 +1,12 @@
"use client";
import { useCallback, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { User } from "@supabase/supabase-js";
import {
buildAuthMePath,
mergeAccountAuthSnapshot,
} from "@/lib/auth-snapshot";
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
import type {
@@ -25,7 +30,7 @@ export interface UseAccountPaymentParams {
backend: AuthMeResponse | null;
user: User | null;
setUser: (user: User | null) => void;
setBackend: (backend: AuthMeResponse | null) => void;
setBackend: Dispatch<SetStateAction<AuthMeResponse | null>>;
setErrorText: (text: string) => void;
setUpdatedAt: (text: string) => void;
showOverlay: boolean;
@@ -155,21 +160,45 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
.then(({ data: { session } }) => session?.user ?? null)
.catch(() => null as User | null)
: Promise.resolve(null as User | null);
const authHeadersPromise = buildAuthedHeaders(false);
const backendPromise = authHeadersPromise.then((headers) =>
fetch("/api/auth/me", { cache: "no-store", headers }),
);
const [localUser, backendResult] = await Promise.all([userPromise, backendPromise]);
const [localUser, authHeaders] = await Promise.all([
userPromise,
buildAuthedHeaders(false),
]);
setUser(localUser);
if (!backendResult.ok) {
if (retry && backendResult.status === 401) {
const readAuthSnapshot = async (
headers: Record<string, string>,
options: Parameters<typeof buildAuthMePath>[0] = {},
) => {
const response = await fetch(buildAuthMePath(options), {
cache: "no-store",
headers,
});
if (!response.ok) {
const raw = (await response.text()).slice(0, 260);
const message = copy.httpError
.replace("{status}", String(response.status))
.replace("{raw}", raw);
const error = new Error(message) as Error & { status?: number };
error.status = response.status;
throw error;
}
return (await response.json()) as AuthMeResponse;
};
let latestHeaders = authHeaders;
let backendJson: AuthMeResponse;
try {
backendJson = await readAuthSnapshot(authHeaders, {
scope: "entitlement",
});
} catch (error) {
if (retry && (error as { status?: number }).status === 401) {
await new Promise((r) => setTimeout(r, 1200));
return fetchAuthSnapshot(false);
}
const raw = (await backendResult.text()).slice(0, 260);
throw new Error(copy.httpError.replace("{status}", String(backendResult.status)).replace("{raw}", raw));
throw error;
}
let backendJson = (await backendResult.json()) as AuthMeResponse;
if (
retry &&
supabaseReady &&
@@ -184,23 +213,31 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
refreshedSession?.access_token || "",
).trim();
if (refreshedToken) {
const retriedBackendResult = await fetch("/api/auth/me", {
cache: "no-store",
headers: { Authorization: `Bearer ${refreshedToken}` },
latestHeaders = { Authorization: `Bearer ${refreshedToken}` };
const retriedBackendJson = await readAuthSnapshot(latestHeaders, {
scope: "entitlement",
});
if (retriedBackendResult.ok) {
const retriedBackendJson =
(await retriedBackendResult.json()) as AuthMeResponse;
backendJson = retriedBackendJson;
}
backendJson = retriedBackendJson;
}
} catch {
// Keep the first response; the UI treats a logged-in local user with
// an unauthenticated backend snapshot as a temporary sync state.
}
}
setBackend(backendJson);
setBackend((previous) => mergeAccountAuthSnapshot(previous, backendJson));
setUpdatedAt(new Date().toISOString());
if (backendJson.authenticated !== false) {
void readAuthSnapshot(latestHeaders)
.then((fullJson) => {
setBackend((previous) =>
mergeAccountAuthSnapshot(previous, fullJson),
);
setUpdatedAt(new Date().toISOString());
})
.catch(() => {
// The lightweight entitlement snapshot already resolved the UI.
});
}
return backendJson;
},
[
@@ -27,6 +27,7 @@ import {
requestWalletWithTimeout,
} from "./payment-utils";
import { trackAppEvent } from "@/lib/app-analytics";
import { buildAuthMePath } from "@/lib/auth-snapshot";
import {
assertExpectedPaymentReceiver,
EXPECTED_PAYMENT_RECEIVER_ADDRESS,
@@ -189,7 +190,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};
const authRes = await fetch("/api/auth/me", {
const authRes = await fetch(buildAuthMePath({ scope: "entitlement" }), {
cache: "no-store",
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
});
@@ -68,6 +68,7 @@ import {
createAuthProfileRequestCache,
loadTerminalAuthProfile,
} from "@/components/dashboard/scan-terminal/terminal-auth-bootstrap";
import { buildAuthMePath } from "@/lib/auth-snapshot";
import {
cityListItemsToScanRows,
mergeScanRowsWithCityFallbackRows,
@@ -1217,9 +1218,10 @@ function ScanTerminalScreen() {
: null;
try {
const response = await fetch(
options?.preferSnapshot
? "/api/auth/me?prefer_snapshot=1"
: "/api/auth/me",
buildAuthMePath({
preferSnapshot: options?.preferSnapshot,
scope: "entitlement",
}),
{
cache: "no-store",
headers,