feat: implement terminal authentication bootstrap and integrate into ScanTerminalDashboard

This commit is contained in:
2569718930@qq.com
2026-05-30 00:05:19 +08:00
parent b3a63aa9af
commit c26f89f134
5 changed files with 261 additions and 7 deletions
@@ -55,6 +55,7 @@ import {
mergeAccessStateWithAuthPayload,
type AuthProfilePayload,
} from "@/components/dashboard/scan-terminal/terminal-access-state";
import { loadTerminalAuthProfile } from "@/components/dashboard/scan-terminal/terminal-auth-bootstrap";
import {
cityListItemsToScanRows,
mergeScanRowsWithCityFallbackRows,
@@ -1034,12 +1035,15 @@ function ScanTerminalScreen() {
cancelled = true;
};
}
const sessionPromise = hasSupabasePublicEnv()
? getSupabaseBrowserClient().auth.getSession()
: Promise.resolve({ data: { session: null } });
sessionPromise
.then(({ data: { session } }) => loadAuthProfile(session?.access_token))
const supabaseEnabled = hasSupabasePublicEnv();
loadTerminalAuthProfile({
getSession: () =>
supabaseEnabled
? getSupabaseBrowserClient().auth.getSession()
: Promise.resolve({ data: { session: null } }),
hasSupabasePublicEnv: supabaseEnabled,
loadAuthProfile,
})
.then((payload) => {
if (cancelled) return;
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
@@ -1332,6 +1332,46 @@ export function runTests() {
"MGM series should not mix METAR airportCurrent points into the MGM airport-station line",
);
const ankaraMgmWithMetarLabel = __buildTemperatureChartDataForTest(
{
city: "ankara",
local_date: "2026-05-29",
local_time: "18:48",
tz_offset_seconds: 3 * 60 * 60,
current_temp: 14,
airport: "LTAC",
} as any,
{
localTime: "18:48",
times: [],
temps: [],
airportPrimary: {
source_code: "mgm",
source_label: "METAR",
temp: 14,
obs_time: "2026-05-29T15:48:00Z",
},
airportCurrent: {
source_code: "metar",
source_label: "METAR",
temp: 17,
obs_time: "18:20",
},
airportPrimaryTodayObs: [
{ time: "2026-05-29T15:00:00Z", temp: 14 },
{ time: "2026-05-29T15:30:00Z", temp: 15 },
],
metarTodayObs: [
{ time: "2026-05-29T15:20:00Z", temp: 17 },
],
} as any,
"1D",
);
const ankaraMgmSeries = seriesByKey(ankaraMgmWithMetarLabel.series as any, "madis") as any;
const ankaraMetarSeries = seriesByKey(ankaraMgmWithMetarLabel.series as any, "metar") as any;
assert(ankaraMgmSeries?.label === "MGM", "Ankara MGM airport-primary series should be labeled MGM even if payload source_label says METAR");
assert(ankaraMetarSeries?.label === "METAR", "Ankara METAR backup series should keep the METAR label");
const chengduMergedHourly = __mergePatchIntoHourlyForTest(
{
localTime: "05:25",
@@ -0,0 +1,82 @@
import {
loadTerminalAuthProfile,
type TerminalAuthProfilePayload,
} from "@/components/dashboard/scan-terminal/terminal-auth-bootstrap";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, reject, resolve };
}
async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}
export async function runTests() {
const slowCookieProfile = deferred<TerminalAuthProfilePayload>();
const fastSession = deferred<{ data: { session: { access_token: string } } }>();
const calls: string[] = [];
const resultPromise = loadTerminalAuthProfile({
hasSupabasePublicEnv: true,
getSession: () => {
calls.push("getSession");
return fastSession.promise;
},
loadAuthProfile: (accessToken) => {
const token = String(accessToken || "");
calls.push(token ? `profile:${token}` : "profile:cookie");
if (!token) return slowCookieProfile.promise;
return Promise.resolve({
authenticated: true,
user_id: "bearer-user",
subscription_active: true,
});
},
});
await flushMicrotasks();
assert(
calls.includes("profile:cookie") && calls.includes("getSession"),
"terminal auth bootstrap should start cookie profile and Supabase session in parallel",
);
fastSession.resolve({ data: { session: { access_token: "fast-token" } } });
const result = await resultPromise;
assert(
calls.includes("profile:fast-token"),
"terminal auth bootstrap should retry auth profile with the Supabase bearer token",
);
assert(
result.authenticated === true && result.user_id === "bearer-user",
"terminal auth bootstrap should not wait for a slow cookie profile when bearer auth succeeds",
);
const slowSession = deferred<{ data: { session: { access_token: string } | null } }>();
const cookieOnlyResult = await loadTerminalAuthProfile({
hasSupabasePublicEnv: true,
getSession: () => slowSession.promise,
loadAuthProfile: (accessToken) => {
assert(!accessToken, "authenticated cookie profile should finish without requiring bearer profile");
return Promise.resolve({
authenticated: true,
user_id: "cookie-user",
subscription_active: true,
});
},
});
assert(
cookieOnlyResult.user_id === "cookie-user",
"terminal auth bootstrap should accept an authenticated cookie profile immediately",
);
}
@@ -627,6 +627,43 @@ function isMgmAirportPrimary(hourly: HourlyForecast) {
return sourceTokens.some((value) => value === "mgm" || value.includes("turkey_mgm"));
}
function canonicalAirportPrimarySourceLabel(hourly: HourlyForecast) {
const primary = hourly?.airportPrimary;
const tokens = [
primary?.source_code,
(primary as any)?.source,
].map((value) => String(value || "").trim().toLowerCase());
if (tokens.some((value) => value === "mgm" || value.includes("turkey_mgm"))) return "MGM";
if (tokens.some((value) => value.includes("jma"))) return "JMA";
if (tokens.some((value) => value.includes("fmi"))) return "FMI";
if (tokens.some((value) => value.includes("knmi"))) return "KNMI";
if (tokens.some((value) => value.includes("ims"))) return "IMS";
if (tokens.some((value) => value.includes("ncm"))) return "NCM";
if (tokens.some((value) => value.includes("aeroweb"))) return "AeroWeb";
if (tokens.some((value) => value.includes("singapore_mss") || value === "mss")) return "MSS";
if (tokens.some((value) => value.includes("madis") || value.includes("noaa"))) return "NOAA MADIS";
return "";
}
function isGenericAirportPrimaryLabel(label: string) {
const normalized = label.trim().toLowerCase();
return (
!normalized ||
normalized === "metar" ||
normalized === "madis" ||
normalized === "noaa madis"
);
}
function airportPrimarySeriesLabel(hourly: HourlyForecast, isHKO: boolean) {
if (isHKO) return "HKO";
const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly);
if (canonicalLabel === "MGM") return canonicalLabel;
const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim();
if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel;
return canonicalLabel || payloadLabel || "NOAA MADIS";
}
function airportPrimaryObservationPoints(hourly: HourlyForecast) {
return appendLatestAirportObservation(
hourly?.airportPrimaryTodayObs,
@@ -1800,7 +1837,7 @@ function buildFullDayChartData(
if (madisVals.some((v) => v !== null)) {
series.push({
key: "madis",
label: isHKO ? "HKO" : (hourly?.airportPrimary?.source_label || "NOAA MADIS"),
label: airportPrimarySeriesLabel(hourly, isHKO),
source: isHKO ? "HKO" : (hourly?.airportPrimary?.station_code || row?.airport || "MADIS"),
color: "#0284c7",
dashed: isHKO ? true : false,
@@ -0,0 +1,91 @@
import type { AuthProfilePayload } from "@/components/dashboard/scan-terminal/terminal-access-state";
export type TerminalAuthProfilePayload = AuthProfilePayload;
type SupabaseSessionResult = {
data?: {
session?: {
access_token?: string | null;
} | null;
} | null;
} | null | undefined;
type LoadTerminalAuthProfileOptions = {
getSession: () => Promise<SupabaseSessionResult>;
hasSupabasePublicEnv: boolean;
loadAuthProfile: (accessToken?: string | null) => Promise<TerminalAuthProfilePayload>;
};
type SettledProfile =
| { ok: true; payload: TerminalAuthProfilePayload | null }
| { ok: false; error: unknown };
function settleProfile(
promise: Promise<TerminalAuthProfilePayload | null>,
): Promise<SettledProfile> {
return promise
.then((payload) => ({ ok: true as const, payload }))
.catch((error) => ({ ok: false as const, error }));
}
function firstKnownProfile(cookieResult: SettledProfile, bearerResult: SettledProfile) {
if (bearerResult.ok && bearerResult.payload?.authenticated) return bearerResult.payload;
if (cookieResult.ok && cookieResult.payload) return cookieResult.payload;
if (bearerResult.ok && bearerResult.payload) return bearerResult.payload;
if (!cookieResult.ok) throw cookieResult.error;
if (!bearerResult.ok) throw bearerResult.error;
return {
authenticated: false,
subscription_active: false,
points: 0,
};
}
export async function loadTerminalAuthProfile({
getSession,
hasSupabasePublicEnv,
loadAuthProfile,
}: LoadTerminalAuthProfileOptions) {
let resolvedAuthenticated = false;
let resolveAuthenticated:
| ((payload: TerminalAuthProfilePayload) => void)
| null = null;
const authenticatedProfile = new Promise<TerminalAuthProfilePayload>((resolve) => {
resolveAuthenticated = resolve;
});
const resolveIfAuthenticated = (payload: TerminalAuthProfilePayload | null) => {
if (!payload?.authenticated || resolvedAuthenticated) return;
resolvedAuthenticated = true;
resolveAuthenticated?.(payload);
};
const cookieProfile = settleProfile(
loadAuthProfile(null).then((payload) => {
resolveIfAuthenticated(payload);
return payload;
}),
);
const bearerProfile = settleProfile(
(async () => {
if (!hasSupabasePublicEnv) return null;
const sessionResult = await getSession();
if (resolvedAuthenticated) return null;
const accessToken = String(
sessionResult?.data?.session?.access_token || "",
).trim();
if (!accessToken) return null;
const payload = await loadAuthProfile(accessToken);
resolveIfAuthenticated(payload);
return payload;
})(),
);
const fallbackProfile = Promise.all([cookieProfile, bearerProfile]).then(
([cookieResult, bearerResult]) => firstKnownProfile(cookieResult, bearerResult),
);
return Promise.race([authenticatedProfile, fallbackProfile]);
}