feat: implement terminal authentication bootstrap and integrate into ScanTerminalDashboard
This commit is contained in:
@@ -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));
|
||||
|
||||
+40
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user